mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
Merge branch 'develop' into feature
This commit is contained in:
commit
6f7bf5baf4
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -26,7 +26,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.0.9
|
||||
placeholder: v4.0.10
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.0.9
|
||||
placeholder: v4.0.10
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
21
.github/workflows/auto-assign-issue.yml
vendored
21
.github/workflows/auto-assign-issue.yml
vendored
@ -1,21 +0,0 @@
|
||||
# auto-assign-issue (https://github.com/marketplace/actions/auto-assign-issue)
|
||||
name: Issue assignment
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
auto-assign:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: pozil/auto-assign-issue@v2
|
||||
if: "contains(github.event.issue.labels.*.name, 'status: needs triage')"
|
||||
with:
|
||||
# Weighted assignments
|
||||
assignees: arthanson:3, jeremystretch:3, DanSheps
|
||||
numOfAssignee: 1
|
||||
abortIfPreviousAssignees: true
|
@ -5,7 +5,7 @@
|
||||
Default: False
|
||||
|
||||
This setting enables debugging. Debugging should be enabled only during development or troubleshooting. Note that only
|
||||
clients which access NetBox from a recognized [internal IP address](#internal_ips) will see debugging tools in the user
|
||||
clients which access NetBox from a recognized [internal IP address](./system.md#internal_ips) will see debugging tools in the user
|
||||
interface.
|
||||
|
||||
!!! warning
|
||||
|
@ -83,7 +83,7 @@ Default: `('127.0.0.1', '::1')`
|
||||
|
||||
A list of IP addresses recognized as internal to the system, used to control the display of debugging output. For
|
||||
example, the debugging toolbar will be viewable only when a client is accessing NetBox from one of the listed IP
|
||||
addresses (and [`DEBUG`](#debug) is true).
|
||||
addresses (and [`DEBUG`](./development.md#debug) is true).
|
||||
|
||||
---
|
||||
|
||||
@ -117,7 +117,7 @@ JINJA2_FILTERS = {
|
||||
|
||||
## LOGGING
|
||||
|
||||
By default, all messages of INFO severity or higher will be logged to the console. Additionally, if [`DEBUG`](#debug) is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in [`ADMINS`](#admins).
|
||||
By default, all messages of INFO severity or higher will be logged to the console. Additionally, if [`DEBUG`](./development.md#debug) is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in [`ADMINS`](./miscellaneous.md#admins).
|
||||
|
||||
The Django framework on which NetBox runs allows for the customization of logging format and destination. Please consult the [Django logging documentation](https://docs.djangoproject.com/en/stable/topics/logging/) for more information on configuring this setting. Below is an example which will write all INFO and higher messages to a local file:
|
||||
|
||||
|
@ -19,7 +19,7 @@ Sometimes it becomes necessary to constrain dependencies to a particular version
|
||||
djangorestframework==3.8.1
|
||||
```
|
||||
|
||||
These version constraints are added to `base_requirements.txt` to ensure that newer packages are not installed when updating the pinned dependencies in `requirements.txt` (see the [Update Requirements](#update-requirements) section below). Before each new minor version of NetBox is released, all such constraints on dependent packages should be addressed if feasible. This guards against the collection of stale constraints over time.
|
||||
These version constraints are added to `base_requirements.txt` to ensure that newer packages are not installed when updating the pinned dependencies in `requirements.txt` (see the [Update Requirements](#update-python-dependencies) section below). Before each new minor version of NetBox is released, all such constraints on dependent packages should be addressed if feasible. This guards against the collection of stale constraints over time.
|
||||
|
||||
### Close the Release Milestone
|
||||
|
||||
|
@ -41,7 +41,7 @@ Line breaks are permitted following binary operators.
|
||||
|
||||
### Enforcing Code Style
|
||||
|
||||
The [`pycodestyle`](https://pypi.org/project/pycodestyle/) utility (formerly `pep8`) is used by the CI process to enforce code style. A [pre-commit hook](./getting-started.md#2-enable-pre-commit-hooks) which runs this automatically is included with NetBox. To invoke `pycodestyle` manually, run:
|
||||
The [`pycodestyle`](https://pypi.org/project/pycodestyle/) utility (formerly `pep8`) is used by the CI process to enforce code style. A [pre-commit hook](./getting-started.md#3-enable-pre-commit-hooks) which runs this automatically is included with NetBox. To invoke `pycodestyle` manually, run:
|
||||
|
||||
```
|
||||
pycodestyle --ignore=W504,E501 netbox/
|
||||
|
@ -1,6 +1,38 @@
|
||||
# NetBox v4.0
|
||||
|
||||
## v4.0.10 (FUTURE)
|
||||
## v4.0.10 (2024-08-29)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#16857](https://github.com/netbox-community/netbox/issues/16857) - Scroll long rendered Markdown content within tables
|
||||
* [#16905](https://github.com/netbox-community/netbox/issues/16905) - Enable filtering of device components by device status
|
||||
* [#16949](https://github.com/netbox-community/netbox/issues/16949) - Add device count column to sites table
|
||||
* [#17072](https://github.com/netbox-community/netbox/issues/17072) - Linkify email addresses & phone numbers in contact assignments list
|
||||
* [#17177](https://github.com/netbox-community/netbox/issues/17177) - Add facility field to locations filter form
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#16292](https://github.com/netbox-community/netbox/issues/16292) - Ensure consistent evaluation of queryset for both individual and list GraphQL API queries
|
||||
* [#16385](https://github.com/netbox-community/netbox/issues/16385) - Restore support for white, gray, and black background colors
|
||||
* [#16640](https://github.com/netbox-community/netbox/issues/16640) - Fix potential corruption of JSON values in custom fields that are not UI-editable
|
||||
* [#16670](https://github.com/netbox-community/netbox/issues/16670) - Fix conflicts within OpenAPI schema definition regarding nested serializers
|
||||
* [#16733](https://github.com/netbox-community/netbox/issues/16733) - Fix bulk edit/delete of objects when using "select all" widget
|
||||
* [#16756](https://github.com/netbox-community/netbox/issues/16756) - Fix dynamic pagination of custom script results table
|
||||
* [#16825](https://github.com/netbox-community/netbox/issues/16825) - Avoid `NoReverseMatch` exception when displaying count of related object type with no list view
|
||||
* [#16946](https://github.com/netbox-community/netbox/issues/16946) - GraphQL API requests with an invalid filter should return an empty set
|
||||
* [#16959](https://github.com/netbox-community/netbox/issues/16959) - Fix function of "reset" button on objects filter form
|
||||
* [#16973](https://github.com/netbox-community/netbox/issues/16973) - Fix support for evaluating user token (`$user`) against custom field values in permission constraints
|
||||
* [#17007](https://github.com/netbox-community/netbox/issues/17007) - Center SSO authentication icon when backend is unnamed
|
||||
* [#17070](https://github.com/netbox-community/netbox/issues/17070) - Image height & width values should not be required when creating an image attachment via the REST API
|
||||
* [#17108](https://github.com/netbox-community/netbox/issues/17108) - Ensure template date & time filters always return localtime-aware values
|
||||
* [#17117](https://github.com/netbox-community/netbox/issues/17117) - Work around Safari rendering bug
|
||||
* [#17186](https://github.com/netbox-community/netbox/issues/17186) - Fix display of custom links with default style under dark mode
|
||||
* [#17219](https://github.com/netbox-community/netbox/issues/17219) - Fix system config view exception when custom validator classes are employed
|
||||
* [#17230](https://github.com/netbox-community/netbox/issues/17230) - Ensure consistent rendering for all dashboard widget colors
|
||||
* [#17256](https://github.com/netbox-community/netbox/issues/17256) - Fix VLAN group scope selection for non-English languages
|
||||
* [#17278](https://github.com/netbox-community/netbox/issues/17278) - Ensure hierarchy is recalculated when bulk editing recursively nested object types (e.g. tenant groups)
|
||||
* [#17279](https://github.com/netbox-community/netbox/issues/17279) - Do not regenerate key when updating a token via REST API
|
||||
* [#17286](https://github.com/netbox-community/netbox/issues/17286) - Fix exception when adding member device to virtual chassis via web UI
|
||||
|
||||
---
|
||||
|
||||
|
@ -3,48 +3,31 @@ from typing import List
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from circuits import models
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class CircuitsQuery:
|
||||
@strawberry.field
|
||||
def circuit(self, id: int) -> CircuitType:
|
||||
return models.Circuit.objects.get(pk=id)
|
||||
circuit: CircuitType = strawberry_django.field()
|
||||
circuit_list: List[CircuitType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def circuit_termination(self, id: int) -> CircuitTerminationType:
|
||||
return models.CircuitTermination.objects.get(pk=id)
|
||||
circuit_termination: CircuitTerminationType = strawberry_django.field()
|
||||
circuit_termination_list: List[CircuitTerminationType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def circuit_type(self, id: int) -> CircuitTypeType:
|
||||
return models.CircuitType.objects.get(pk=id)
|
||||
circuit_type: CircuitTypeType = strawberry_django.field()
|
||||
circuit_type_list: List[CircuitTypeType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def circuit_group(self, id: int) -> CircuitGroupType:
|
||||
return models.CircuitGroup.objects.get(pk=id)
|
||||
circuit_group: CircuitGroupType = strawberry_django.field()
|
||||
circuit_group_list: List[CircuitGroupType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def circuit_group_assignment(self, id: int) -> CircuitGroupAssignmentType:
|
||||
return models.CircuitGroupAssignment.objects.get(pk=id)
|
||||
circuit_group_assignment: CircuitGroupAssignmentType = strawberry_django.field()
|
||||
circuit_group_assignment_list: List[CircuitGroupAssignmentType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def provider(self, id: int) -> ProviderType:
|
||||
return models.Provider.objects.get(pk=id)
|
||||
provider: ProviderType = strawberry_django.field()
|
||||
provider_list: List[ProviderType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def provider_account(self, id: int) -> ProviderAccountType:
|
||||
return models.ProviderAccount.objects.get(pk=id)
|
||||
provider_account: ProviderAccountType = strawberry_django.field()
|
||||
provider_account_list: List[ProviderAccountType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def provider_network(self, id: int) -> ProviderNetworkType:
|
||||
return models.ProviderNetwork.objects.get(pk=id)
|
||||
provider_network: ProviderNetworkType = strawberry_django.field()
|
||||
provider_network_list: List[ProviderNetworkType] = strawberry_django.field()
|
||||
|
@ -1,5 +1,3 @@
|
||||
# Generated by Django 5.0.7 on 2024-07-22 06:27
|
||||
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
import utilities.json
|
||||
@ -10,7 +8,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0043_circuittype_color'),
|
||||
('extras', '0118_notifications'),
|
||||
('extras', '0119_notifications'),
|
||||
('tenancy', '0015_contactassignment_rename_content_type'),
|
||||
]
|
||||
|
||||
|
@ -126,9 +126,18 @@ class NetBoxAutoSchema(AutoSchema):
|
||||
|
||||
return response_serializers
|
||||
|
||||
def _get_serializer_name(self, serializer, direction, bypass_extensions=False) -> str:
|
||||
name = super()._get_serializer_name(serializer, direction, bypass_extensions)
|
||||
|
||||
# If this serializer is nested, prepend its name with "Brief"
|
||||
if getattr(serializer, 'nested', False):
|
||||
name = f'Brief{name}'
|
||||
|
||||
return name
|
||||
|
||||
def get_serializer_ref_name(self, serializer):
|
||||
# from drf-yasg.utils
|
||||
"""Get serializer's ref_name (or None for ModelSerializer if it is named 'NestedSerializer')
|
||||
"""Get serializer's ref_name
|
||||
:param serializer: Serializer instance
|
||||
:return: Serializer's ``ref_name`` or ``None`` for inline serializer
|
||||
:rtype: str or None
|
||||
@ -137,8 +146,6 @@ class NetBoxAutoSchema(AutoSchema):
|
||||
serializer_name = type(serializer).__name__
|
||||
if hasattr(serializer_meta, 'ref_name'):
|
||||
ref_name = serializer_meta.ref_name
|
||||
elif serializer_name == 'NestedSerializer' and isinstance(serializer, serializers.ModelSerializer):
|
||||
ref_name = None
|
||||
else:
|
||||
ref_name = serializer_name
|
||||
if ref_name.endswith('Serializer'):
|
||||
|
@ -3,18 +3,13 @@ from typing import List
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from core import models
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class CoreQuery:
|
||||
@strawberry.field
|
||||
def data_file(self, id: int) -> DataFileType:
|
||||
return models.DataFile.objects.get(pk=id)
|
||||
data_file: DataFileType = strawberry_django.field()
|
||||
data_file_list: List[DataFileType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def data_source(self, id: int) -> DataSourceType:
|
||||
return models.DataSource.objects.get(pk=id)
|
||||
data_source: DataSourceType = strawberry_django.field()
|
||||
data_source_list: List[DataSourceType] = strawberry_django.field()
|
||||
|
@ -2,7 +2,6 @@ import json
|
||||
import platform
|
||||
|
||||
from django import __version__ as DJANGO_VERSION
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
@ -32,6 +31,7 @@ from netbox.views.generic.mixins import TableMixin
|
||||
from utilities.data import shallow_compare_dict
|
||||
from utilities.forms import ConfirmationForm
|
||||
from utilities.htmx import htmx_partial
|
||||
from utilities.json import ConfigJSONEncoder
|
||||
from utilities.query import count_related
|
||||
from utilities.views import ContentTypePermissionRequiredMixin, GetRelatedModelsMixin, register_model_view
|
||||
from . import filtersets, forms, tables
|
||||
@ -643,10 +643,14 @@ class SystemView(UserPassesTestMixin, View):
|
||||
k: getattr(config, k) for k in sorted(params)
|
||||
},
|
||||
}
|
||||
response = HttpResponse(json.dumps(data, indent=4), content_type='text/json')
|
||||
response = HttpResponse(json.dumps(data, cls=ConfigJSONEncoder, indent=4), content_type='text/json')
|
||||
response['Content-Disposition'] = 'attachment; filename="netbox.json"'
|
||||
return response
|
||||
|
||||
# Serialize any CustomValidator classes
|
||||
if hasattr(config, 'CUSTOM_VALIDATORS') and config.CUSTOM_VALIDATORS:
|
||||
config.CUSTOM_VALIDATORS = json.dumps(config.CUSTOM_VALIDATORS, cls=ConfigJSONEncoder, indent=4)
|
||||
|
||||
return render(request, 'core/system.html', {
|
||||
'stats': stats,
|
||||
'config': config,
|
||||
|
@ -1463,6 +1463,10 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
to_field_name='name',
|
||||
label=_('Virtual Chassis'),
|
||||
)
|
||||
device_status = django_filters.MultipleChoiceFilter(
|
||||
choices=DeviceStatusChoices,
|
||||
field_name='device__status',
|
||||
)
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
|
@ -130,6 +130,11 @@ class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
|
||||
},
|
||||
label=_('Device')
|
||||
)
|
||||
device_status = forms.MultipleChoiceField(
|
||||
choices=DeviceStatusChoices,
|
||||
required=False,
|
||||
label=_('Device Status'),
|
||||
)
|
||||
|
||||
|
||||
class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
|
||||
@ -196,7 +201,7 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF
|
||||
model = Location
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'parent_id', 'status', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'parent_id', 'status', 'facility', name=_('Attributes')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
@ -233,6 +238,10 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF
|
||||
choices=LocationStatusChoices,
|
||||
required=False
|
||||
)
|
||||
facility = forms.CharField(
|
||||
label=_('Facility'),
|
||||
required=False
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
@ -1229,7 +1238,9 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
@ -1251,7 +1262,10 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', 'speed', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
@ -1273,7 +1287,9 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
@ -1290,7 +1306,10 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
@ -1310,7 +1329,10 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
|
||||
FieldSet('poe_mode', 'poe_type', name=_('PoE')),
|
||||
FieldSet('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power', name=_('Wireless')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', 'vdc_id',
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'connected', 'occupied', name=_('Connection')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'device_id')
|
||||
@ -1418,7 +1440,9 @@ class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id', name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'occupied', name=_('Cable')),
|
||||
)
|
||||
model = FrontPort
|
||||
@ -1440,7 +1464,10 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'type', 'color', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
FieldSet('cabled', 'occupied', name=_('Cable')),
|
||||
)
|
||||
type = forms.MultipleChoiceField(
|
||||
@ -1461,7 +1488,10 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', 'position', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
position = forms.CharField(
|
||||
@ -1476,7 +1506,10 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('name', 'label', name=_('Attributes')),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
)
|
||||
tag = TagFilterField(model)
|
||||
|
||||
@ -1490,7 +1523,10 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
|
||||
name=_('Attributes')
|
||||
),
|
||||
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id', name=_('Location')),
|
||||
FieldSet('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', name=_('Device')),
|
||||
FieldSet(
|
||||
'device_type_id', 'device_role_id', 'device_id', 'device_status', 'virtual_chassis_id',
|
||||
name=_('Device')
|
||||
),
|
||||
)
|
||||
role_id = DynamicModelMultipleChoiceField(
|
||||
queryset=InventoryItemRole.objects.all(),
|
||||
|
@ -3,213 +3,130 @@ from typing import List
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from dcim import models
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class DCIMQuery:
|
||||
@strawberry.field
|
||||
def cable(self, id: int) -> CableType:
|
||||
return models.Cable.objects.get(pk=id)
|
||||
cable: CableType = strawberry_django.field()
|
||||
cable_list: List[CableType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def console_port(self, id: int) -> ConsolePortType:
|
||||
return models.ConsolePort.objects.get(pk=id)
|
||||
console_port: ConsolePortType = strawberry_django.field()
|
||||
console_port_list: List[ConsolePortType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def console_port_template(self, id: int) -> ConsolePortTemplateType:
|
||||
return models.ConsolePortTemplate.objects.get(pk=id)
|
||||
console_port_template: ConsolePortTemplateType = strawberry_django.field()
|
||||
console_port_template_list: List[ConsolePortTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def console_server_port(self, id: int) -> ConsoleServerPortType:
|
||||
return models.ConsoleServerPort.objects.get(pk=id)
|
||||
console_server_port: ConsoleServerPortType = strawberry_django.field()
|
||||
console_server_port_list: List[ConsoleServerPortType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def console_server_port_template(self, id: int) -> ConsoleServerPortTemplateType:
|
||||
return models.ConsoleServerPortTemplate.objects.get(pk=id)
|
||||
console_server_port_template: ConsoleServerPortTemplateType = strawberry_django.field()
|
||||
console_server_port_template_list: List[ConsoleServerPortTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def device(self, id: int) -> DeviceType:
|
||||
return models.Device.objects.get(pk=id)
|
||||
device: DeviceType = strawberry_django.field()
|
||||
device_list: List[DeviceType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def device_bay(self, id: int) -> DeviceBayType:
|
||||
return models.DeviceBay.objects.get(pk=id)
|
||||
device_bay: DeviceBayType = strawberry_django.field()
|
||||
device_bay_list: List[DeviceBayType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def device_bay_template(self, id: int) -> DeviceBayTemplateType:
|
||||
return models.DeviceBayTemplate.objects.get(pk=id)
|
||||
device_bay_template: DeviceBayTemplateType = strawberry_django.field()
|
||||
device_bay_template_list: List[DeviceBayTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def device_role(self, id: int) -> DeviceRoleType:
|
||||
return models.DeviceRole.objects.get(pk=id)
|
||||
device_role: DeviceRoleType = strawberry_django.field()
|
||||
device_role_list: List[DeviceRoleType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def device_type(self, id: int) -> DeviceTypeType:
|
||||
return models.DeviceType.objects.get(pk=id)
|
||||
device_type: DeviceTypeType = strawberry_django.field()
|
||||
device_type_list: List[DeviceTypeType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def front_port(self, id: int) -> FrontPortType:
|
||||
return models.FrontPort.objects.get(pk=id)
|
||||
front_port: FrontPortType = strawberry_django.field()
|
||||
front_port_list: List[FrontPortType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def front_port_template(self, id: int) -> FrontPortTemplateType:
|
||||
return models.FrontPortTemplate.objects.get(pk=id)
|
||||
front_port_template: FrontPortTemplateType = strawberry_django.field()
|
||||
front_port_template_list: List[FrontPortTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def interface(self, id: int) -> InterfaceType:
|
||||
return models.Interface.objects.get(pk=id)
|
||||
interface: InterfaceType = strawberry_django.field()
|
||||
interface_list: List[InterfaceType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def interface_template(self, id: int) -> InterfaceTemplateType:
|
||||
return models.InterfaceTemplate.objects.get(pk=id)
|
||||
interface_template: InterfaceTemplateType = strawberry_django.field()
|
||||
interface_template_list: List[InterfaceTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def inventory_item(self, id: int) -> InventoryItemType:
|
||||
return models.InventoryItem.objects.get(pk=id)
|
||||
inventory_item: InventoryItemType = strawberry_django.field()
|
||||
inventory_item_list: List[InventoryItemType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def inventory_item_role(self, id: int) -> InventoryItemRoleType:
|
||||
return models.InventoryItemRole.objects.get(pk=id)
|
||||
inventory_item_role: InventoryItemRoleType = strawberry_django.field()
|
||||
inventory_item_role_list: List[InventoryItemRoleType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def inventory_item_template(self, id: int) -> InventoryItemTemplateType:
|
||||
return models.InventoryItemTemplate.objects.get(pk=id)
|
||||
inventory_item_template: InventoryItemTemplateType = strawberry_django.field()
|
||||
inventory_item_template_list: List[InventoryItemTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def location(self, id: int) -> LocationType:
|
||||
return models.Location.objects.get(pk=id)
|
||||
location: LocationType = strawberry_django.field()
|
||||
location_list: List[LocationType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def manufacturer(self, id: int) -> ManufacturerType:
|
||||
return models.Manufacturer.objects.get(pk=id)
|
||||
manufacturer: ManufacturerType = strawberry_django.field()
|
||||
manufacturer_list: List[ManufacturerType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def module(self, id: int) -> ModuleType:
|
||||
return models.Module.objects.get(pk=id)
|
||||
module: ModuleType = strawberry_django.field()
|
||||
module_list: List[ModuleType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def module_bay(self, id: int) -> ModuleBayType:
|
||||
return models.ModuleBay.objects.get(pk=id)
|
||||
module_bay: ModuleBayType = strawberry_django.field()
|
||||
module_bay_list: List[ModuleBayType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def module_bay_template(self, id: int) -> ModuleBayTemplateType:
|
||||
return models.ModuleBayTemplate.objects.get(pk=id)
|
||||
module_bay_template: ModuleBayTemplateType = strawberry_django.field()
|
||||
module_bay_template_list: List[ModuleBayTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def module_type(self, id: int) -> ModuleTypeType:
|
||||
return models.ModuleType.objects.get(pk=id)
|
||||
module_type: ModuleTypeType = strawberry_django.field()
|
||||
module_type_list: List[ModuleTypeType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def platform(self, id: int) -> PlatformType:
|
||||
return models.Platform.objects.get(pk=id)
|
||||
platform: PlatformType = strawberry_django.field()
|
||||
platform_list: List[PlatformType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_feed(self, id: int) -> PowerFeedType:
|
||||
return models.PowerFeed.objects.get(pk=id)
|
||||
power_feed: PowerFeedType = strawberry_django.field()
|
||||
power_feed_list: List[PowerFeedType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_outlet(self, id: int) -> PowerOutletType:
|
||||
return models.PowerOutlet.objects.get(pk=id)
|
||||
power_outlet: PowerOutletType = strawberry_django.field()
|
||||
power_outlet_list: List[PowerOutletType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_outlet_template(self, id: int) -> PowerOutletTemplateType:
|
||||
return models.PowerOutletTemplate.objects.get(pk=id)
|
||||
power_outlet_template: PowerOutletTemplateType = strawberry_django.field()
|
||||
power_outlet_template_list: List[PowerOutletTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_panel(self, id: int) -> PowerPanelType:
|
||||
return models.PowerPanel.objects.get(id=id)
|
||||
power_panel: PowerPanelType = strawberry_django.field()
|
||||
power_panel_list: List[PowerPanelType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_port(self, id: int) -> PowerPortType:
|
||||
return models.PowerPort.objects.get(id=id)
|
||||
power_port: PowerPortType = strawberry_django.field()
|
||||
power_port_list: List[PowerPortType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def power_port_template(self, id: int) -> PowerPortTemplateType:
|
||||
return models.PowerPortTemplate.objects.get(id=id)
|
||||
power_port_template: PowerPortTemplateType = strawberry_django.field()
|
||||
power_port_template_list: List[PowerPortTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rack_type(self, id: int) -> RackTypeType:
|
||||
return models.RackType.objects.get(id=id)
|
||||
rack_type: RackTypeType = strawberry_django.field()
|
||||
rack_type_list: List[RackTypeType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rack(self, id: int) -> RackType:
|
||||
return models.Rack.objects.get(id=id)
|
||||
rack: RackType = strawberry_django.field()
|
||||
rack_list: List[RackType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rack_reservation(self, id: int) -> RackReservationType:
|
||||
return models.RackReservation.objects.get(id=id)
|
||||
rack_reservation: RackReservationType = strawberry_django.field()
|
||||
rack_reservation_list: List[RackReservationType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rack_role(self, id: int) -> RackRoleType:
|
||||
return models.RackRole.objects.get(id=id)
|
||||
rack_role: RackRoleType = strawberry_django.field()
|
||||
rack_role_list: List[RackRoleType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rear_port(self, id: int) -> RearPortType:
|
||||
return models.RearPort.objects.get(id=id)
|
||||
rear_port: RearPortType = strawberry_django.field()
|
||||
rear_port_list: List[RearPortType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rear_port_template(self, id: int) -> RearPortTemplateType:
|
||||
return models.RearPortTemplate.objects.get(id=id)
|
||||
rear_port_template: RearPortTemplateType = strawberry_django.field()
|
||||
rear_port_template_list: List[RearPortTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def region(self, id: int) -> RegionType:
|
||||
return models.Region.objects.get(id=id)
|
||||
region: RegionType = strawberry_django.field()
|
||||
region_list: List[RegionType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def site(self, id: int) -> SiteType:
|
||||
return models.Site.objects.get(id=id)
|
||||
site: SiteType = strawberry_django.field()
|
||||
site_list: List[SiteType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def site_group(self, id: int) -> SiteGroupType:
|
||||
return models.SiteGroup.objects.get(id=id)
|
||||
site_group: SiteGroupType = strawberry_django.field()
|
||||
site_group_list: List[SiteGroupType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def virtual_chassis(self, id: int) -> VirtualChassisType:
|
||||
return models.VirtualChassis.objects.get(id=id)
|
||||
virtual_chassis: VirtualChassisType = strawberry_django.field()
|
||||
virtual_chassis_list: List[VirtualChassisType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def virtual_device_context(self, id: int) -> VirtualDeviceContextType:
|
||||
return models.VirtualDeviceContext.objects.get(id=id)
|
||||
virtual_device_context: VirtualDeviceContextType = strawberry_django.field()
|
||||
virtual_device_context_list: List[VirtualDeviceContextType] = strawberry_django.field()
|
||||
|
@ -11,7 +11,7 @@ import utilities.ordering
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0117_customfield_uniqueness'),
|
||||
('extras', '0118_customfield_uniqueness'),
|
||||
('dcim', '0187_alter_device_vc_position'),
|
||||
]
|
||||
|
||||
|
@ -7,7 +7,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0189_moduletype_airflow_rack_airflow_racktype_airflow'),
|
||||
('extras', '0120_customfield_related_object_filter'),
|
||||
('extras', '0121_customfield_related_object_filter'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
@ -290,6 +290,11 @@ class DeviceComponentTable(NetBoxTable):
|
||||
linkify=True,
|
||||
order_by=('_name',)
|
||||
)
|
||||
device_status = columns.ChoiceFieldColumn(
|
||||
accessor=tables.A('device__status'),
|
||||
verbose_name=_('Device Status'),
|
||||
color=lambda x: x.device.get_status_color(),
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
order_by = ('device', 'name')
|
||||
|
@ -99,6 +99,11 @@ class SiteTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||
url_params={'site_id': 'pk'},
|
||||
verbose_name=_('ASN Count')
|
||||
)
|
||||
device_count = columns.LinkedCountColumn(
|
||||
viewname='dcim:device_list',
|
||||
url_params={'site_id': 'pk'},
|
||||
verbose_name=_('Devices')
|
||||
)
|
||||
comments = columns.MarkdownColumn(
|
||||
verbose_name=_('Comments'),
|
||||
)
|
||||
|
@ -37,6 +37,10 @@ class DeviceComponentFilterSetTests:
|
||||
params = {'device_role': [role[0].slug, role[1].slug]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_device_status(self):
|
||||
params = {'device_status': ['active', 'planned']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class DeviceComponentTemplateFilterSetTests:
|
||||
|
||||
@ -2825,10 +2829,10 @@ class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_types[0], role=roles[0], site=sites[3]), # For cable connections
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
Device(name=None, device_type=device_types[0], role=roles[0], site=sites[3], status='offline'), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@ -3006,10 +3010,10 @@ class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeL
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3], status='offline'), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@ -3187,10 +3191,10 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3], status='offline'), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@ -3376,10 +3380,10 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3], status='offline'), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@ -3575,7 +3579,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
rack=racks[0],
|
||||
virtual_chassis=virtual_chassis,
|
||||
vc_position=1,
|
||||
vc_priority=1
|
||||
vc_priority=1,
|
||||
status='active',
|
||||
),
|
||||
Device(
|
||||
name='Device 1B',
|
||||
@ -3586,7 +3591,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
rack=racks[2],
|
||||
virtual_chassis=virtual_chassis,
|
||||
vc_position=2,
|
||||
vc_priority=1
|
||||
vc_priority=1,
|
||||
status='planned',
|
||||
),
|
||||
Device(
|
||||
name='Device 2',
|
||||
@ -3594,7 +3600,8 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
role=roles[1],
|
||||
site=sites[1],
|
||||
location=locations[1],
|
||||
rack=racks[1]
|
||||
rack=racks[1],
|
||||
status='offline',
|
||||
),
|
||||
Device(
|
||||
name='Device 3',
|
||||
@ -3602,14 +3609,16 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
role=roles[2],
|
||||
site=sites[2],
|
||||
location=locations[2],
|
||||
rack=racks[2]
|
||||
rack=racks[2],
|
||||
status='offline',
|
||||
),
|
||||
# For cable connections
|
||||
Device(
|
||||
name=None,
|
||||
device_type=device_types[2],
|
||||
role=roles[2],
|
||||
site=sites[3]
|
||||
site=sites[3],
|
||||
status='offline',
|
||||
),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
@ -4056,10 +4065,10 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3], status='offline'), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@ -4246,10 +4255,10 @@ class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilt
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3]), # For cable connections
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
Device(name=None, device_type=device_types[2], role=roles[2], site=sites[3], status='offline'), # For cable connections
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@ -4428,9 +4437,9 @@ class ModuleBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
@ -4576,9 +4585,9 @@ class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
devices = (
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0]),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1]),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2]),
|
||||
Device(name='Device 1', device_type=device_types[0], role=roles[0], site=sites[0], location=locations[0], rack=racks[0], status='active'),
|
||||
Device(name='Device 2', device_type=device_types[1], role=roles[1], site=sites[1], location=locations[1], rack=racks[1], status='planned'),
|
||||
Device(name='Device 3', device_type=device_types[2], role=roles[2], site=sites[2], location=locations[2], rack=racks[2], status='offline'),
|
||||
)
|
||||
Device.objects.bulk_create(devices)
|
||||
|
||||
|
@ -380,7 +380,9 @@ class SiteGroupContactsView(ObjectContactsView):
|
||||
#
|
||||
|
||||
class SiteListView(generic.ObjectListView):
|
||||
queryset = Site.objects.all()
|
||||
queryset = Site.objects.annotate(
|
||||
device_count=count_related(Device, 'site')
|
||||
)
|
||||
filterset = filtersets.SiteFilterSet
|
||||
filterset_form = forms.SiteFilterForm
|
||||
table = tables.SiteTable
|
||||
@ -3505,7 +3507,7 @@ class VirtualChassisAddMemberView(ObjectPermissionRequiredMixin, GetReturnURLMix
|
||||
|
||||
membership_form.save()
|
||||
messages.success(request, mark_safe(
|
||||
_('Added member <a href="{url}">{escape(device)}</a>').format(url=device.get_absolute_url())
|
||||
_('Added member <a href="{url}">{device}</a>').format(url=device.get_absolute_url(), device=escape(device))
|
||||
))
|
||||
|
||||
if '_addanother' in request.POST:
|
||||
|
@ -18,6 +18,8 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||
queryset=ObjectType.objects.all()
|
||||
)
|
||||
parent = serializers.SerializerMethodField(read_only=True)
|
||||
image_width = serializers.IntegerField(read_only=True)
|
||||
image_height = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ImageAttachment
|
||||
|
@ -131,22 +131,6 @@ class DashboardWidget:
|
||||
def name(self):
|
||||
return f'{self.__class__.__module__.split(".")[0]}.{self.__class__.__name__}'
|
||||
|
||||
@property
|
||||
def fg_color(self):
|
||||
"""
|
||||
Return the appropriate foreground (text) color for the widget's color.
|
||||
"""
|
||||
if self.color in (
|
||||
ButtonColorChoices.CYAN,
|
||||
ButtonColorChoices.GRAY,
|
||||
ButtonColorChoices.GREY,
|
||||
ButtonColorChoices.TEAL,
|
||||
ButtonColorChoices.WHITE,
|
||||
ButtonColorChoices.YELLOW,
|
||||
):
|
||||
return ButtonColorChoices.BLACK
|
||||
return ButtonColorChoices.WHITE
|
||||
|
||||
@property
|
||||
def form_data(self):
|
||||
return {
|
||||
|
@ -31,7 +31,7 @@ class ReportForm(forms.Form):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Annotate the current system time for reference
|
||||
now = local_now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
now = local_now().strftime('%Y-%m-%d %H:%M:%S %Z')
|
||||
self.fields['schedule_at'].help_text += _(' (current time: <strong>{now}</strong>)').format(now=now)
|
||||
|
||||
# Remove scheduling fields if scheduling is disabled
|
||||
|
@ -37,7 +37,7 @@ class ScriptForm(forms.Form):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Annotate the current system time for reference
|
||||
now = local_now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
now = local_now().strftime('%Y-%m-%d %H:%M:%S %Z')
|
||||
self.fields['_schedule_at'].help_text += _(' (current time: <strong>{now}</strong>)').format(now=now)
|
||||
|
||||
# Remove scheduling fields if scheduling is disabled
|
||||
|
@ -3,83 +3,52 @@ from typing import List
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from extras import models
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class ExtrasQuery:
|
||||
@strawberry.field
|
||||
def config_context(self, id: int) -> ConfigContextType:
|
||||
return models.ConfigContext.objects.get(pk=id)
|
||||
config_context: ConfigContextType = strawberry_django.field()
|
||||
config_context_list: List[ConfigContextType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def config_template(self, id: int) -> ConfigTemplateType:
|
||||
return models.ConfigTemplate.objects.get(pk=id)
|
||||
config_template: ConfigTemplateType = strawberry_django.field()
|
||||
config_template_list: List[ConfigTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def custom_field(self, id: int) -> CustomFieldType:
|
||||
return models.CustomField.objects.get(pk=id)
|
||||
custom_field: CustomFieldType = strawberry_django.field()
|
||||
custom_field_list: List[CustomFieldType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def custom_field_choice_set(self, id: int) -> CustomFieldChoiceSetType:
|
||||
return models.CustomFieldChoiceSet.objects.get(pk=id)
|
||||
custom_field_choice_set: CustomFieldChoiceSetType = strawberry_django.field()
|
||||
custom_field_choice_set_list: List[CustomFieldChoiceSetType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def custom_link(self, id: int) -> CustomLinkType:
|
||||
return models.CustomLink.objects.get(pk=id)
|
||||
custom_link: CustomLinkType = strawberry_django.field()
|
||||
custom_link_list: List[CustomLinkType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def export_template(self, id: int) -> ExportTemplateType:
|
||||
return models.ExportTemplate.objects.get(pk=id)
|
||||
export_template: ExportTemplateType = strawberry_django.field()
|
||||
export_template_list: List[ExportTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def image_attachment(self, id: int) -> ImageAttachmentType:
|
||||
return models.ImageAttachment.objects.get(pk=id)
|
||||
image_attachment: ImageAttachmentType = strawberry_django.field()
|
||||
image_attachment_list: List[ImageAttachmentType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def saved_filter(self, id: int) -> SavedFilterType:
|
||||
return models.SavedFilter.objects.get(pk=id)
|
||||
saved_filter: SavedFilterType = strawberry_django.field()
|
||||
saved_filter_list: List[SavedFilterType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def journal_entry(self, id: int) -> JournalEntryType:
|
||||
return models.JournalEntry.objects.get(pk=id)
|
||||
journal_entry: JournalEntryType = strawberry_django.field()
|
||||
journal_entry_list: List[JournalEntryType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def notification(self, id: int) -> NotificationType:
|
||||
return models.Notification.objects.get(pk=id)
|
||||
notification: NotificationType = strawberry_django.field()
|
||||
notification_list: List[NotificationType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def notification_group(self, id: int) -> NotificationGroupType:
|
||||
return models.NotificationGroup.objects.get(pk=id)
|
||||
notification_group: NotificationGroupType = strawberry_django.field()
|
||||
notification_group_list: List[NotificationGroupType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def subscription(self, id: int) -> SubscriptionType:
|
||||
return models.Subscription.objects.get(pk=id)
|
||||
subscription: SubscriptionType = strawberry_django.field()
|
||||
subscription_list: List[SubscriptionType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def tag(self, id: int) -> TagType:
|
||||
return models.Tag.objects.get(pk=id)
|
||||
tag: TagType = strawberry_django.field()
|
||||
tag_list: List[TagType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def webhook(self, id: int) -> WebhookType:
|
||||
return models.Webhook.objects.get(pk=id)
|
||||
webhook: WebhookType = strawberry_django.field()
|
||||
webhook_list: List[WebhookType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def event_rule(self, id: int) -> EventRuleType:
|
||||
return models.EventRule.objects.get(pk=id)
|
||||
event_rule: EventRuleType = strawberry_django.field()
|
||||
event_rule_list: List[EventRuleType] = strawberry_django.field()
|
||||
|
25
netbox/extras/migrations/0116_custom_link_button_color.py
Normal file
25
netbox/extras/migrations/0116_custom_link_button_color.py
Normal file
@ -0,0 +1,25 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def update_link_buttons(apps, schema_editor):
|
||||
CustomLink = apps.get_model('extras', 'CustomLink')
|
||||
CustomLink.objects.filter(button_class='outline-dark').update(button_class='default')
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0115_convert_dashboard_widgets'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='customlink',
|
||||
name='button_class',
|
||||
field=models.CharField(default='default', max_length=30),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=update_link_buttons,
|
||||
reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
@ -28,7 +28,7 @@ def update_dashboard_widgets(apps, schema_editor):
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0115_convert_dashboard_widgets'),
|
||||
('extras', '0116_custom_link_button_color'),
|
||||
('core', '0011_move_objectchange'),
|
||||
]
|
||||
|
@ -4,7 +4,7 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0116_move_objectchange'),
|
||||
('extras', '0117_move_objectchange'),
|
||||
]
|
||||
|
||||
operations = [
|
@ -7,7 +7,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('extras', '0117_customfield_uniqueness'),
|
||||
('extras', '0118_customfield_uniqueness'),
|
||||
('users', '0009_update_group_perms'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
@ -28,7 +28,7 @@ def set_event_types(apps, schema_editor):
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0118_notifications'),
|
||||
('extras', '0119_notifications'),
|
||||
]
|
||||
|
||||
operations = [
|
@ -1,12 +1,10 @@
|
||||
# Generated by Django 5.0.7 on 2024-07-26 01:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('extras', '0119_eventrule_event_types'),
|
||||
('extras', '0120_eventrule_event_types'),
|
||||
]
|
||||
|
||||
operations = [
|
@ -4,6 +4,7 @@ from django.utils.safestring import mark_safe
|
||||
|
||||
from core.models import ObjectType
|
||||
from extras.models import CustomLink
|
||||
from netbox.choices import ButtonColorChoices
|
||||
|
||||
|
||||
register = template.Library()
|
||||
@ -59,10 +60,11 @@ def custom_links(context, obj):
|
||||
|
||||
# Add non-grouped links
|
||||
else:
|
||||
button_class = 'outline-secondary' if cl.button_class == ButtonColorChoices.DEFAULT else cl.button_class
|
||||
try:
|
||||
if rendered := cl.render(link_context):
|
||||
template_code += LINK_BUTTON.format(
|
||||
rendered['link'], rendered['link_target'], cl.button_class, rendered['text']
|
||||
rendered['link'], rendered['link_target'], button_class, rendered['text']
|
||||
)
|
||||
except Exception as e:
|
||||
template_code += f'<a class="btn btn-sm btn-outline-secondary" disabled="disabled" title="{e}">' \
|
||||
|
@ -1326,6 +1326,9 @@ class ScriptResultView(TableMixin, generic.ObjectView):
|
||||
|
||||
# If this is an HTMX request, return only the result HTML
|
||||
if htmx_partial(request):
|
||||
if request.GET.get('log'):
|
||||
# If log=True, render only the log table
|
||||
return render(request, 'htmx/table.html', context)
|
||||
response = render(request, 'extras/htmx/script_result.html', context)
|
||||
if job.completed or not job.started:
|
||||
response.status_code = 286
|
||||
|
@ -1,10 +1,9 @@
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.postgres.forms import IntegerRangeField, SimpleArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.models import Device, Interface, Location, Rack, Region, Site, SiteGroup
|
||||
from dcim.models import Device, Interface, Site
|
||||
from ipam.choices import *
|
||||
from ipam.constants import *
|
||||
from ipam.formfields import IPNetworkFormField
|
||||
@ -18,8 +17,10 @@ from utilities.forms.fields import (
|
||||
NumericRangeArrayField, SlugField
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet, InlineFields, ObjectAttribute, TabbedGroups
|
||||
from utilities.forms.widgets import DatePicker
|
||||
from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface
|
||||
from utilities.forms.utils import get_field_value
|
||||
from utilities.forms.widgets import DatePicker, HTMXSelect
|
||||
from utilities.templatetags.builtins.filters import bettertitle
|
||||
from virtualization.models import VirtualMachine, VMInterface
|
||||
|
||||
__all__ = (
|
||||
'AggregateForm',
|
||||
@ -563,94 +564,34 @@ class FHRPGroupAssignmentForm(forms.ModelForm):
|
||||
|
||||
|
||||
class VLANGroupForm(NetBoxModelForm):
|
||||
scope_type = ContentTypeChoiceField(
|
||||
label=_('Scope type'),
|
||||
queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
|
||||
required=False
|
||||
)
|
||||
region = DynamicModelChoiceField(
|
||||
label=_('Region'),
|
||||
queryset=Region.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
}
|
||||
)
|
||||
sitegroup = DynamicModelChoiceField(
|
||||
queryset=SiteGroup.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'sites': '$site'
|
||||
},
|
||||
label=_('Site group')
|
||||
)
|
||||
site = DynamicModelChoiceField(
|
||||
label=_('Site'),
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'locations': '$location'
|
||||
},
|
||||
query_params={
|
||||
'region_id': '$region',
|
||||
'group_id': '$sitegroup',
|
||||
}
|
||||
)
|
||||
location = DynamicModelChoiceField(
|
||||
label=_('Location'),
|
||||
queryset=Location.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'racks': '$rack'
|
||||
},
|
||||
query_params={
|
||||
'site_id': '$site',
|
||||
}
|
||||
)
|
||||
rack = DynamicModelChoiceField(
|
||||
label=_('Rack'),
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'site_id': '$site',
|
||||
'location_id': '$location',
|
||||
}
|
||||
)
|
||||
clustergroup = DynamicModelChoiceField(
|
||||
queryset=ClusterGroup.objects.all(),
|
||||
required=False,
|
||||
initial_params={
|
||||
'clusters': '$cluster'
|
||||
},
|
||||
label=_('Cluster group')
|
||||
)
|
||||
cluster = DynamicModelChoiceField(
|
||||
label=_('Cluster'),
|
||||
queryset=Cluster.objects.all(),
|
||||
required=False,
|
||||
query_params={
|
||||
'group_id': '$clustergroup',
|
||||
}
|
||||
)
|
||||
slug = SlugField()
|
||||
vid_ranges = NumericRangeArrayField(
|
||||
label=_('VLAN IDs')
|
||||
)
|
||||
scope_type = ContentTypeChoiceField(
|
||||
queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
|
||||
widget=HTMXSelect(),
|
||||
required=False,
|
||||
label=_('Scope type')
|
||||
)
|
||||
scope = DynamicModelChoiceField(
|
||||
label=_('Scope'),
|
||||
queryset=Site.objects.none(), # Initial queryset
|
||||
required=False,
|
||||
disabled=True,
|
||||
selector=True
|
||||
)
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('name', 'slug', 'description', 'tags', name=_('VLAN Group')),
|
||||
FieldSet('vid_ranges', name=_('Child VLANs')),
|
||||
FieldSet(
|
||||
'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster',
|
||||
name=_('Scope')
|
||||
),
|
||||
FieldSet('scope_type', 'scope', name=_('Scope')),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = VLANGroup
|
||||
fields = [
|
||||
'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack',
|
||||
'clustergroup', 'cluster', 'vid_ranges', 'tags',
|
||||
'name', 'slug', 'description', 'vid_ranges', 'scope_type', 'scope', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -658,21 +599,30 @@ class VLANGroupForm(NetBoxModelForm):
|
||||
initial = kwargs.get('initial', {})
|
||||
|
||||
if instance is not None and instance.scope:
|
||||
initial[instance.scope_type.model] = instance.scope
|
||||
|
||||
initial['scope'] = instance.scope
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if scope_type_id := get_field_value(self, 'scope_type'):
|
||||
try:
|
||||
scope_type = ContentType.objects.get(pk=scope_type_id)
|
||||
model = scope_type.model_class()
|
||||
self.fields['scope'].queryset = model.objects.all()
|
||||
self.fields['scope'].widget.attrs['selector'] = model._meta.label_lower
|
||||
self.fields['scope'].disabled = False
|
||||
self.fields['scope'].label = _(bettertitle(model._meta.verbose_name))
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
|
||||
if self.instance and scope_type_id != self.instance.scope_type_id:
|
||||
self.initial['scope'] = None
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Assign scope based on scope_type
|
||||
if self.cleaned_data.get('scope_type'):
|
||||
scope_field = self.cleaned_data['scope_type'].model
|
||||
self.instance.scope = self.cleaned_data.get(scope_field)
|
||||
else:
|
||||
self.instance.scope_id = None
|
||||
# Assign the selected scope (if any)
|
||||
self.instance.scope = self.cleaned_data.get('scope')
|
||||
|
||||
|
||||
class VLANForm(TenancyForm, NetBoxModelForm):
|
||||
|
@ -3,88 +3,55 @@ from typing import List
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from ipam import models
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class IPAMQuery:
|
||||
@strawberry.field
|
||||
def asn(self, id: int) -> ASNType:
|
||||
return models.ASN.objects.get(pk=id)
|
||||
asn: ASNType = strawberry_django.field()
|
||||
asn_list: List[ASNType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def asn_range(self, id: int) -> ASNRangeType:
|
||||
return models.ASNRange.objects.get(pk=id)
|
||||
asn_range: ASNRangeType = strawberry_django.field()
|
||||
asn_range_list: List[ASNRangeType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def aggregate(self, id: int) -> AggregateType:
|
||||
return models.Aggregate.objects.get(pk=id)
|
||||
aggregate: AggregateType = strawberry_django.field()
|
||||
aggregate_list: List[AggregateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def ip_address(self, id: int) -> IPAddressType:
|
||||
return models.IPAddress.objects.get(pk=id)
|
||||
ip_address: IPAddressType = strawberry_django.field()
|
||||
ip_address_list: List[IPAddressType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def ip_range(self, id: int) -> IPRangeType:
|
||||
return models.IPRange.objects.get(pk=id)
|
||||
ip_range: IPRangeType = strawberry_django.field()
|
||||
ip_range_list: List[IPRangeType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def prefix(self, id: int) -> PrefixType:
|
||||
return models.Prefix.objects.get(pk=id)
|
||||
prefix: PrefixType = strawberry_django.field()
|
||||
prefix_list: List[PrefixType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def rir(self, id: int) -> RIRType:
|
||||
return models.RIR.objects.get(pk=id)
|
||||
rir: RIRType = strawberry_django.field()
|
||||
rir_list: List[RIRType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def role(self, id: int) -> RoleType:
|
||||
return models.Role.objects.get(pk=id)
|
||||
role: RoleType = strawberry_django.field()
|
||||
role_list: List[RoleType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def route_target(self, id: int) -> RouteTargetType:
|
||||
return models.RouteTarget.objects.get(pk=id)
|
||||
route_target: RouteTargetType = strawberry_django.field()
|
||||
route_target_list: List[RouteTargetType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def service(self, id: int) -> ServiceType:
|
||||
return models.Service.objects.get(pk=id)
|
||||
service: ServiceType = strawberry_django.field()
|
||||
service_list: List[ServiceType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def service_template(self, id: int) -> ServiceTemplateType:
|
||||
return models.ServiceTemplate.objects.get(pk=id)
|
||||
service_template: ServiceTemplateType = strawberry_django.field()
|
||||
service_template_list: List[ServiceTemplateType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def fhrp_group(self, id: int) -> FHRPGroupType:
|
||||
return models.FHRPGroup.objects.get(pk=id)
|
||||
fhrp_group: FHRPGroupType = strawberry_django.field()
|
||||
fhrp_group_list: List[FHRPGroupType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def fhrp_group_assignment(self, id: int) -> FHRPGroupAssignmentType:
|
||||
return models.FHRPGroupAssignment.objects.get(pk=id)
|
||||
fhrp_group_assignment: FHRPGroupAssignmentType = strawberry_django.field()
|
||||
fhrp_group_assignment_list: List[FHRPGroupAssignmentType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def vlan(self, id: int) -> VLANType:
|
||||
return models.VLAN.objects.get(pk=id)
|
||||
vlan: VLANType = strawberry_django.field()
|
||||
vlan_list: List[VLANType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def vlan_group(self, id: int) -> VLANGroupType:
|
||||
return models.VLANGroup.objects.get(pk=id)
|
||||
vlan_group: VLANGroupType = strawberry_django.field()
|
||||
vlan_group_list: List[VLANGroupType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def vrf(self, id: int) -> VRFType:
|
||||
return models.VRF.objects.get(pk=id)
|
||||
vrf: VRFType = strawberry_django.field()
|
||||
vrf_list: List[VRFType] = strawberry_django.field()
|
||||
|
@ -81,10 +81,7 @@ class ColorChoices(ChoiceSet):
|
||||
#
|
||||
|
||||
class ButtonColorChoices(ChoiceSet):
|
||||
"""
|
||||
Map standard button color choices to Bootstrap 3 button classes
|
||||
"""
|
||||
DEFAULT = 'outline-dark'
|
||||
DEFAULT = 'default'
|
||||
BLUE = 'blue'
|
||||
INDIGO = 'indigo'
|
||||
PURPLE = 'purple'
|
||||
|
@ -60,6 +60,8 @@ class NetBoxModelForm(CheckLastUpdatedMixin, CustomFieldsMixin, TagsMixin, forms
|
||||
if value in self.fields[cf_name].empty_values:
|
||||
self.instance.custom_field_data[key] = None
|
||||
else:
|
||||
if customfield.type == CustomFieldTypeChoices.TYPE_JSON and type(value) is str:
|
||||
value = json.loads(value)
|
||||
self.instance.custom_field_data[key] = customfield.serialize(value)
|
||||
|
||||
return super().clean()
|
||||
|
@ -4,7 +4,7 @@ from typing import List
|
||||
import django_filters
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
from django.core.exceptions import FieldDoesNotExist, ValidationError
|
||||
from strawberry import auto
|
||||
from ipam.fields import ASNField
|
||||
from netbox.graphql.scalars import BigInt
|
||||
@ -201,4 +201,9 @@ def autotype_decorator(filterset):
|
||||
class BaseFilterMixin:
|
||||
|
||||
def filter_by_filterset(self, queryset, key):
|
||||
return self.filterset(data={key: getattr(self, key)}, queryset=queryset).qs
|
||||
filterset = self.filterset(data={key: getattr(self, key)}, queryset=queryset)
|
||||
if not filterset.is_valid():
|
||||
# We could raise validation error but strawberry logs it all to the
|
||||
# console i.e. raise ValidationError(f"{k}: {v[0]}")
|
||||
return filterset.qs.none()
|
||||
return filterset.qs
|
||||
|
@ -767,6 +767,7 @@ LOCALE_PATHS = (
|
||||
# Strawberry (GraphQL)
|
||||
#
|
||||
STRAWBERRY_DJANGO = {
|
||||
"DEFAULT_PK_FIELD_NAME": "id",
|
||||
"TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING": True,
|
||||
"USE_DEPRECATED_FILTERS": True,
|
||||
}
|
||||
|
@ -331,15 +331,22 @@ class ActionsColumn(tables.Column):
|
||||
class ChoiceFieldColumn(tables.Column):
|
||||
"""
|
||||
Render a model's static ChoiceField with its value from `get_FOO_display()` as a colored badge. Background color is
|
||||
set by the instance's get_FOO_color() method, if defined.
|
||||
set by the instance's get_FOO_color() method, if defined, or can be overridden by a "color" callable.
|
||||
"""
|
||||
DEFAULT_BG_COLOR = 'secondary'
|
||||
|
||||
def __init__(self, *args, color=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.color = color
|
||||
|
||||
def render(self, record, bound_column, value):
|
||||
if value in self.empty_values:
|
||||
return self.default
|
||||
|
||||
# Determine the background color to use (try calling object.get_FOO_color())
|
||||
# Determine the background color to use (use "color" callable if given, else try calling object.get_FOO_color())
|
||||
if self.color:
|
||||
bg_color = self.color(record)
|
||||
else:
|
||||
try:
|
||||
bg_color = getattr(record, f'get_{bound_column.name}_color')() or self.DEFAULT_BG_COLOR
|
||||
except AttributeError:
|
||||
|
@ -13,11 +13,9 @@ class DummyModelType:
|
||||
pass
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class DummyQuery:
|
||||
@strawberry.field
|
||||
def dummymodel(self, id: int) -> DummyModelType:
|
||||
return None
|
||||
dummymodel: DummyModelType = strawberry_django.field()
|
||||
dummymodel_list: List[DummyModelType] = strawberry_django.field()
|
||||
|
||||
|
||||
|
@ -1,7 +1,13 @@
|
||||
import json
|
||||
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
from utilities.testing import disable_warnings, TestCase
|
||||
from core.models import ObjectType
|
||||
from dcim.models import Site, Location
|
||||
from users.models import ObjectPermission
|
||||
from utilities.testing import disable_warnings, APITestCase, TestCase
|
||||
|
||||
|
||||
class GraphQLTestCase(TestCase):
|
||||
@ -34,3 +40,45 @@ class GraphQLTestCase(TestCase):
|
||||
response = self.client.get(url, **header)
|
||||
with disable_warnings('django.request'):
|
||||
self.assertHttpStatus(response, 302) # Redirect to login page
|
||||
|
||||
|
||||
class GraphQLAPITestCase(APITestCase):
|
||||
|
||||
@override_settings(LOGIN_REQUIRED=True)
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*', 'auth.user'])
|
||||
def test_graphql_filter_objects(self):
|
||||
"""
|
||||
Test the operation of filters for GraphQL API requests.
|
||||
"""
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1'),
|
||||
Site(name='Site 2', slug='site-2'),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
Location.objects.create(site=sites[0], name='Location 1', slug='location-1'),
|
||||
Location.objects.create(site=sites[1], name='Location 2', slug='location-2'),
|
||||
|
||||
# Add object-level permission
|
||||
obj_perm = ObjectPermission(
|
||||
name='Test permission',
|
||||
actions=['view']
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ObjectType.objects.get_for_model(Location))
|
||||
|
||||
# A valid request should return the filtered list
|
||||
url = reverse('graphql')
|
||||
query = '{location_list(filters: {site_id: "' + str(sites[0].pk) + '"}) {id site {id}}}'
|
||||
response = self.client.post(url, data={'query': query}, format="json", **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
data = json.loads(response.content)
|
||||
self.assertNotIn('errors', data)
|
||||
self.assertEqual(len(data['data']['location_list']), 1)
|
||||
|
||||
# An invalid request should return an empty list
|
||||
query = '{location_list(filters: {site_id: "99999"}) {id site {id}}}' # Invalid site ID
|
||||
response = self.client.post(url, data={'query': query}, format="json", **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
data = json.loads(response.content)
|
||||
self.assertEqual(len(data['data']['location_list']), 0)
|
||||
|
@ -16,6 +16,7 @@ from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext as _
|
||||
from django_tables2.export import TableExport
|
||||
from mptt.models import MPTTModel
|
||||
|
||||
from core.models import ObjectType
|
||||
from core.signals import clear_events
|
||||
@ -178,6 +179,8 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
||||
table.columns.hide('pk')
|
||||
return render(request, 'htmx/table.html', {
|
||||
'table': table,
|
||||
'model': model,
|
||||
'actions': actions,
|
||||
})
|
||||
|
||||
context = {
|
||||
@ -612,6 +615,10 @@ class BulkEditView(GetReturnURLMixin, BaseMultiObjectView):
|
||||
if form.cleaned_data.get('remove_tags', None):
|
||||
obj.tags.remove(*form.cleaned_data['remove_tags'])
|
||||
|
||||
# Rebuild the tree for MPTT models
|
||||
if issubclass(self.queryset.model, MPTTModel):
|
||||
self.queryset.model.objects.rebuild()
|
||||
|
||||
return updated_objects
|
||||
|
||||
#
|
||||
|
BIN
netbox/project-static/dist/netbox.css
vendored
BIN
netbox/project-static/dist/netbox.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -39,10 +39,17 @@ export function initFormElements(): void {
|
||||
// Find each of the form's submitters. Most object edit forms have a "Create" and
|
||||
// a "Create & Add", so we need to add a listener to both.
|
||||
const submitters = form.querySelectorAll<HTMLButtonElement>('button[type=submit]');
|
||||
|
||||
for (const submitter of submitters) {
|
||||
// Add the event listener to each submitter.
|
||||
submitter.addEventListener('click', (event: Event) => handleFormSubmit(event, form));
|
||||
}
|
||||
|
||||
// Initialize any reset buttons so that when clicked, the page is reloaded without query parameters.
|
||||
const resetButton = document.querySelector<HTMLButtonElement>('button[data-reset-select]');
|
||||
if (resetButton !== null) {
|
||||
resetButton.addEventListener('click', () => {
|
||||
window.location.assign(window.location.origin + window.location.pathname);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { initFormElements } from './elements';
|
||||
import { initSpeedSelector } from './speedSelector';
|
||||
import { initScopeSelector } from './scopeSelector';
|
||||
|
||||
export function initForms(): void {
|
||||
for (const func of [initFormElements, initSpeedSelector, initScopeSelector]) {
|
||||
for (const func of [initFormElements, initSpeedSelector]) {
|
||||
func();
|
||||
}
|
||||
}
|
||||
|
@ -1,153 +0,0 @@
|
||||
import { getElements, toggleVisibility } from '../util';
|
||||
|
||||
type ShowHideMap = {
|
||||
/**
|
||||
* Name of view to which this map should apply.
|
||||
*
|
||||
* @example vlangroup_edit
|
||||
*/
|
||||
[view: string]: string;
|
||||
};
|
||||
|
||||
type ShowHideLayout = {
|
||||
/**
|
||||
* Name of layout config
|
||||
*
|
||||
* @example vlangroup
|
||||
*/
|
||||
[config: string]: {
|
||||
/**
|
||||
* Default layout.
|
||||
*/
|
||||
default: { hide: string[]; show: string[] };
|
||||
/**
|
||||
* Field name to layout mapping.
|
||||
*/
|
||||
[fieldName: string]: { hide: string[]; show: string[] };
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Mapping of layout names to arrays of object types whose fields should be hidden or shown when
|
||||
* the scope type (key) is selected.
|
||||
*
|
||||
* For example, if `region` is the scope type, the fields with IDs listed in
|
||||
* showHideMap.region.hide should be hidden, and the fields with IDs listed in
|
||||
* showHideMap.region.show should be shown.
|
||||
*/
|
||||
const showHideLayout: ShowHideLayout = {
|
||||
vlangroup: {
|
||||
region: {
|
||||
hide: ['id_sitegroup', 'id_site', 'id_location', 'id_rack', 'id_clustergroup', 'id_cluster'],
|
||||
show: ['id_region'],
|
||||
},
|
||||
'site group': {
|
||||
hide: ['id_region', 'id_site', 'id_location', 'id_rack', 'id_clustergroup', 'id_cluster'],
|
||||
show: ['id_sitegroup'],
|
||||
},
|
||||
site: {
|
||||
hide: ['id_location', 'id_rack', 'id_clustergroup', 'id_cluster'],
|
||||
show: ['id_region', 'id_sitegroup', 'id_site'],
|
||||
},
|
||||
location: {
|
||||
hide: ['id_rack', 'id_clustergroup', 'id_cluster'],
|
||||
show: ['id_region', 'id_sitegroup', 'id_site', 'id_location'],
|
||||
},
|
||||
rack: {
|
||||
hide: ['id_clustergroup', 'id_cluster'],
|
||||
show: ['id_region', 'id_sitegroup', 'id_site', 'id_location', 'id_rack'],
|
||||
},
|
||||
'cluster group': {
|
||||
hide: ['id_region', 'id_sitegroup', 'id_site', 'id_location', 'id_rack', 'id_cluster'],
|
||||
show: ['id_clustergroup'],
|
||||
},
|
||||
cluster: {
|
||||
hide: ['id_region', 'id_sitegroup', 'id_site', 'id_location', 'id_rack'],
|
||||
show: ['id_clustergroup', 'id_cluster'],
|
||||
},
|
||||
default: {
|
||||
hide: [
|
||||
'id_region',
|
||||
'id_sitegroup',
|
||||
'id_site',
|
||||
'id_location',
|
||||
'id_rack',
|
||||
'id_clustergroup',
|
||||
'id_cluster',
|
||||
],
|
||||
show: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Mapping of view names to layout configurations
|
||||
*
|
||||
* For example, if `vlangroup_add` is the view, use the layout configuration `vlangroup`.
|
||||
*/
|
||||
const showHideMap: ShowHideMap = {
|
||||
vlangroup_add: 'vlangroup',
|
||||
vlangroup_edit: 'vlangroup',
|
||||
vlangroup_bulk_edit: 'vlangroup',
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle visibility of a given element's parent.
|
||||
* @param query CSS Query.
|
||||
* @param action Show or Hide the Parent.
|
||||
*/
|
||||
function toggleParentVisibility(query: string, action: 'show' | 'hide') {
|
||||
for (const element of getElements(query)) {
|
||||
const parent = element.parentElement?.parentElement as Nullable<HTMLDivElement>;
|
||||
if (parent !== null) {
|
||||
if (action === 'show') {
|
||||
toggleVisibility(parent, 'show');
|
||||
} else {
|
||||
toggleVisibility(parent, 'hide');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle changes to the Scope Type field.
|
||||
*/
|
||||
function handleScopeChange<P extends keyof ShowHideMap>(view: P, element: HTMLSelectElement) {
|
||||
// Scope type's innerText looks something like `DCIM > region`.
|
||||
const scopeType = element.options[element.selectedIndex].innerText.toLowerCase();
|
||||
const layoutConfig = showHideMap[view];
|
||||
|
||||
for (const [scope, fields] of Object.entries(showHideLayout[layoutConfig])) {
|
||||
// If the scope type ends with the specified scope, toggle its field visibility according to
|
||||
// the show/hide values.
|
||||
if (scopeType.endsWith(scope)) {
|
||||
for (const field of fields.hide) {
|
||||
toggleParentVisibility(`#${field}`, 'hide');
|
||||
}
|
||||
for (const field of fields.show) {
|
||||
toggleParentVisibility(`#${field}`, 'show');
|
||||
}
|
||||
// Stop on first match.
|
||||
break;
|
||||
} else {
|
||||
// Otherwise, hide all fields.
|
||||
for (const field of showHideLayout[layoutConfig].default.hide) {
|
||||
toggleParentVisibility(`#${field}`, 'hide');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize scope type select event listeners.
|
||||
*/
|
||||
export function initScopeSelector(): void {
|
||||
for (const view of Object.keys(showHideMap)) {
|
||||
for (const element of getElements<HTMLSelectElement>(
|
||||
`html[data-netbox-url-name="${view}"] #id_scope_type`,
|
||||
)) {
|
||||
handleScopeChange(view, element);
|
||||
element.addEventListener('change', () => handleScopeChange(view, element));
|
||||
}
|
||||
}
|
||||
}
|
@ -30,6 +30,9 @@
|
||||
|
||||
// Remove the bottom margin of <p> elements inside a table cell
|
||||
td > .rendered-markdown {
|
||||
max-height: 200px;
|
||||
overflow-y: scroll;
|
||||
|
||||
p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
@ -25,3 +25,14 @@ hr.dropdown-divider {
|
||||
.dropdown-item {
|
||||
font-weight: $font-weight-base;
|
||||
}
|
||||
|
||||
// Restore support for old Bootstrap v3 colors
|
||||
.text-bg-black {
|
||||
@extend .text-bg-dark;
|
||||
}
|
||||
.text-bg-gray {
|
||||
@extend .text-bg-secondary;
|
||||
}
|
||||
.text-bg-white {
|
||||
@extend .text-bg-light;
|
||||
}
|
||||
|
@ -130,6 +130,19 @@ body[data-bs-theme=dark] {
|
||||
}
|
||||
}
|
||||
|
||||
// Do not apply padding to <code> elements inside a <pre>
|
||||
pre code {
|
||||
padding: unset;
|
||||
}
|
||||
|
||||
// Use an icon instead of Tabler's native "caret" for dropdowns (avoids a Safari bug)
|
||||
.dropdown-toggle:after{
|
||||
font-family: "Material Design Icons";
|
||||
content: '\F0140';
|
||||
padding-right: 9px;
|
||||
border-bottom: none;
|
||||
border-left: none;
|
||||
transform: none;
|
||||
vertical-align: .05em;
|
||||
height: auto;
|
||||
}
|
||||
|
@ -95,7 +95,7 @@
|
||||
<tr>
|
||||
<th scope="row" class="ps-3">{% trans "Custom validators" %}</th>
|
||||
{% if config.CUSTOM_VALIDATORS %}
|
||||
<td><pre>{{ config.CUSTOM_VALIDATORS|json }}</pre></td>
|
||||
<td><pre>{{ config.CUSTOM_VALIDATORS }}</pre></td>
|
||||
{% else %}
|
||||
<td>{{ ''|placeholder }}</td>
|
||||
{% endif %}
|
||||
|
@ -10,15 +10,17 @@
|
||||
gs-id="{{ widget.id }}"
|
||||
>
|
||||
<div class="card grid-stack-item-content">
|
||||
<div class="card-header {% if widget.color %} text-{{ widget.fg_color }} {% endif %} bg-{{ widget.color|default:'default' }} px-2 py-1 d-flex flex-row">
|
||||
{% with bg_color=widget.color|default:"secondary" %}
|
||||
<div class="card-header text-bg-{{ bg_color }} px-2 py-1 d-flex flex-row">
|
||||
<a href="#"
|
||||
hx-get="{% url 'extras:dashboardwidget_config' id=widget.id %}"
|
||||
hx-target="#htmx-modal-content"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#htmx-modal"
|
||||
class="text-bg-{{ bg_color }}"
|
||||
aria-label="{{ widget.title }} {% trans "widget configuration" %}"
|
||||
>
|
||||
<i class="mdi mdi-cog {% if widget.color %} text-{{ widget.fg_color }} {% endif %}"></i>
|
||||
<i class="mdi mdi-cog"></i>
|
||||
</a>
|
||||
<div class="card-title flex-fill text-center">
|
||||
{% if widget.title %}
|
||||
@ -30,11 +32,13 @@
|
||||
hx-target="#htmx-modal-content"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#htmx-modal"
|
||||
class="text-bg-{{ bg_color }}"
|
||||
aria-label="{% trans "Close widget" %} {{ widget.title }}"
|
||||
>
|
||||
<i class="mdi mdi-close {% if widget.color %} text-{{ widget.fg_color }} {% endif %}"></i>
|
||||
<i class="mdi mdi-close"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% endwith %}
|
||||
<div class="card-body p-2 pt-1 overflow-auto">
|
||||
{% render_widget widget %}
|
||||
</div>
|
||||
|
@ -41,7 +41,11 @@
|
||||
<div class="card">
|
||||
<div class="table-responsive" id="object_list">
|
||||
<h2 class="card-header">{% trans "Log" %}</h2>
|
||||
{% include 'htmx/table.html' %}
|
||||
<div class="htmx-container table-responsive"
|
||||
hx-get="{% url 'extras:script_result' job_pk=job.pk %}?embedded=True&log=True"
|
||||
hx-target="this"
|
||||
hx-trigger="load" hx-select=".htmx-container" hx-swap="outerHTML"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -108,14 +108,6 @@
|
||||
</div>
|
||||
{# /Object list tab #}
|
||||
|
||||
{# Filters tab #}
|
||||
{% if filter_form %}
|
||||
<div class="tab-pane show" id="filters-form" role="tabpanel" aria-labelledby="filters-form-tab">
|
||||
{% include 'inc/filter_list.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{# /Filters tab #}
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
{% block modals %}
|
||||
|
@ -81,15 +81,7 @@ Context:
|
||||
{% if table.paginator.num_pages > 1 %}
|
||||
<div id="select-all-box" class="d-none card d-print-none">
|
||||
<div class="form col-md-12">
|
||||
<div class="card-body">
|
||||
<div class="float-end">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
{% bulk_edit_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
{% bulk_delete_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body d-flex justify-content-between">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="select-all" name="_all" class="form-check-input" />
|
||||
<label for="select-all" class="form-check-label">
|
||||
@ -98,6 +90,14 @@ Context:
|
||||
{% endblocktrans %}
|
||||
</label>
|
||||
</div>
|
||||
<div class="bulk-action-buttons">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
{% bulk_edit_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
{% bulk_delete_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -123,12 +123,14 @@ Context:
|
||||
{# Form buttons #}
|
||||
<div class="btn-list d-print-none mt-2">
|
||||
{% block bulk_buttons %}
|
||||
<div class="bulk-action-buttons">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
{% bulk_edit_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
{% bulk_delete_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
{# /Form buttons #}
|
||||
|
@ -1,5 +1,6 @@
|
||||
{# Render an HTMX-enabled table with paginator #}
|
||||
{% load helpers %}
|
||||
{% load buttons %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
<div class="htmx-container table-responsive">
|
||||
@ -14,5 +15,19 @@
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
{% if request.htmx %}
|
||||
{# Include the updated object count for display elsewhere on the page #}
|
||||
<div class="d-none" hx-swap-oob="innerHTML:.total-object-count">{{ table.rows|length }}</div>
|
||||
<div hx-swap-oob="innerHTML:.total-object-count">{{ table.rows|length }}</div>
|
||||
|
||||
{# Update the bulk action buttons with new query parameters #}
|
||||
{% if actions %}
|
||||
<div class="bulk-action-buttons" hx-swap-oob="outerHTML:.bulk-action-buttons">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
{% bulk_edit_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
{% bulk_delete_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
@ -5,7 +5,8 @@
|
||||
<h2 class="card-header">{% trans "Related Objects" %}</h2>
|
||||
<ul class="list-group list-group-flush" role="presentation">
|
||||
{% for qs, filter_param in related_models %}
|
||||
{% with viewname=qs.model|viewname:"list" %}
|
||||
{% with viewname=qs.model|validated_viewname:"list" %}
|
||||
{% if viewname is not None %}
|
||||
<a href="{% url viewname %}?{{ filter_param }}={{ object.pk }}" class="list-group-item list-group-item-action d-flex justify-content-between">
|
||||
{{ qs.model|meta:"verbose_name_plural"|bettertitle }}
|
||||
{% with count=qs.count %}
|
||||
@ -16,6 +17,7 @@
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
@ -81,7 +81,7 @@
|
||||
<div class="col">
|
||||
<a href="{{ backend.url }}" class="btn w-100">
|
||||
{% if backend.icon_name %}<i class="mdi mdi-{{ backend.icon_name }}"></i>
|
||||
{% elif backend.icon_img %}<img src="{{ backend.icon_img }}" height="24" class="me-2" />{% endif %}
|
||||
{% elif backend.icon_img %}<img src="{{ backend.icon_img }}" height="24" {% if backend.display_name %}class="me-2" {% endif %}/>{% endif %}
|
||||
{{ backend.display_name }}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -3,38 +3,25 @@ from typing import List
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from tenancy import models
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class TenancyQuery:
|
||||
@strawberry.field
|
||||
def tenant(self, id: int) -> TenantType:
|
||||
return models.Tenant.objects.get(pk=id)
|
||||
tenant: TenantType = strawberry_django.field()
|
||||
tenant_list: List[TenantType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def tenant_group(self, id: int) -> TenantGroupType:
|
||||
return models.TenantGroup.objects.get(pk=id)
|
||||
tenant_group: TenantGroupType = strawberry_django.field()
|
||||
tenant_group_list: List[TenantGroupType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def contact(self, id: int) -> ContactType:
|
||||
return models.Contact.objects.get(pk=id)
|
||||
contact: ContactType = strawberry_django.field()
|
||||
contact_list: List[ContactType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def contact_role(self, id: int) -> ContactRoleType:
|
||||
return models.ContactRole.objects.get(pk=id)
|
||||
contact_role: ContactRoleType = strawberry_django.field()
|
||||
contact_role_list: List[ContactRoleType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def contact_group(self, id: int) -> ContactGroupType:
|
||||
return models.ContactGroup.objects.get(pk=id)
|
||||
contact_group: ContactGroupType = strawberry_django.field()
|
||||
contact_group_list: List[ContactGroupType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def contact_assignment(self, id: int) -> ContactAssignmentType:
|
||||
return models.ContactAssignment.objects.get(pk=id)
|
||||
contact_assignment: ContactAssignmentType = strawberry_django.field()
|
||||
contact_assignment_list: List[ContactAssignmentType] = strawberry_django.field()
|
||||
|
@ -113,11 +113,12 @@ class ContactAssignmentTable(NetBoxTable):
|
||||
)
|
||||
contact_phone = tables.Column(
|
||||
accessor=Accessor('contact__phone'),
|
||||
verbose_name=_('Contact Phone')
|
||||
verbose_name=_('Contact Phone'),
|
||||
linkify=linkify_phone,
|
||||
)
|
||||
contact_email = tables.Column(
|
||||
contact_email = tables.EmailColumn(
|
||||
accessor=Accessor('contact__email'),
|
||||
verbose_name=_('Contact Email')
|
||||
verbose_name=_('Contact Email'),
|
||||
)
|
||||
contact_address = tables.Column(
|
||||
accessor=Accessor('contact__address'),
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -39,7 +39,7 @@ class TokenSerializer(ValidatedModelSerializer):
|
||||
brief_fields = ('id', 'url', 'display', 'key', 'write_enabled', 'description')
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if 'key' not in data:
|
||||
if not getattr(self.instance, 'key', None) and 'key' not in data:
|
||||
data['key'] = Token.generate_key()
|
||||
return super().to_internal_value(data)
|
||||
|
||||
|
@ -3,18 +3,13 @@ from typing import List
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from users import models
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class UsersQuery:
|
||||
@strawberry.field
|
||||
def group(self, id: int) -> GroupType:
|
||||
return models.Group.objects.get(pk=id)
|
||||
group: GroupType = strawberry_django.field()
|
||||
group_list: List[GroupType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def user(self, id: int) -> UserType:
|
||||
return models.User.objects.get(pk=id)
|
||||
user: UserType = strawberry_django.field()
|
||||
user_list: List[UserType] = strawberry_django.field()
|
||||
|
@ -3,6 +3,7 @@ import decimal
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
__all__ = (
|
||||
'ConfigJSONEncoder',
|
||||
'CustomFieldJSONEncoder',
|
||||
)
|
||||
|
||||
@ -15,3 +16,16 @@ class CustomFieldJSONEncoder(DjangoJSONEncoder):
|
||||
if isinstance(o, decimal.Decimal):
|
||||
return float(o)
|
||||
return super().default(o)
|
||||
|
||||
|
||||
class ConfigJSONEncoder(DjangoJSONEncoder):
|
||||
"""
|
||||
Override Django's built-in JSON encoder to serialize CustomValidator classes as strings.
|
||||
"""
|
||||
def default(self, o):
|
||||
from extras.validators import CustomValidator
|
||||
|
||||
if issubclass(type(o), CustomValidator):
|
||||
return type(o).__name__
|
||||
|
||||
return super().default(o)
|
||||
|
@ -1,7 +1,10 @@
|
||||
from django.conf import settings
|
||||
from django.apps import apps
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from users.constants import CONSTRAINT_TOKEN_USER
|
||||
|
||||
__all__ = (
|
||||
'get_permission_for_model',
|
||||
'permission_is_exempt',
|
||||
@ -90,6 +93,11 @@ def qs_filter_from_constraints(constraints, tokens=None):
|
||||
if tokens is None:
|
||||
tokens = {}
|
||||
|
||||
User = apps.get_model('users.User')
|
||||
for token, value in tokens.items():
|
||||
if token == CONSTRAINT_TOKEN_USER and isinstance(value, User):
|
||||
tokens[token] = value.id
|
||||
|
||||
def _replace_tokens(value, tokens):
|
||||
if type(value) is list:
|
||||
return list(map(lambda v: tokens.get(v, v), value))
|
||||
|
@ -8,6 +8,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.humanize.templatetags.humanize import naturalday, naturaltime
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.timezone import localtime
|
||||
from markdown import markdown
|
||||
from markdown.extensions.tables import TableExtension
|
||||
|
||||
@ -218,7 +219,8 @@ def isodate(value):
|
||||
text = value.isoformat()
|
||||
return mark_safe(f'<span title="{naturalday(value)}">{text}</span>')
|
||||
elif type(value) is datetime.datetime:
|
||||
text = value.date().isoformat()
|
||||
local_value = localtime(value) if value.tzinfo else value
|
||||
text = local_value.date().isoformat()
|
||||
return mark_safe(f'<span title="{naturaltime(value)}">{text}</span>')
|
||||
else:
|
||||
return ''
|
||||
@ -229,7 +231,8 @@ def isotime(value, spec='seconds'):
|
||||
if type(value) is datetime.time:
|
||||
return value.isoformat(timespec=spec)
|
||||
if type(value) is datetime.datetime:
|
||||
return value.time().isoformat(timespec=spec)
|
||||
local_value = localtime(value) if value.tzinfo else value
|
||||
return local_value.time().isoformat(timespec=spec)
|
||||
return ''
|
||||
|
||||
|
||||
|
@ -18,7 +18,7 @@ from ipam.graphql.types import IPAddressFamilyType
|
||||
from users.models import ObjectPermission, Token, User
|
||||
from utilities.api import get_graphql_type_for_model
|
||||
from .base import ModelTestCase
|
||||
from .utils import disable_warnings
|
||||
from .utils import disable_logging, disable_warnings
|
||||
|
||||
__all__ = (
|
||||
'APITestCase',
|
||||
@ -517,7 +517,6 @@ class APIViewTestCases:
|
||||
return self._build_query_with_filter(name, filter_string)
|
||||
|
||||
@override_settings(LOGIN_REQUIRED=True)
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*', 'auth.user'])
|
||||
def test_graphql_get_object(self):
|
||||
url = reverse('graphql')
|
||||
field_name = self._get_graphql_base_name()
|
||||
@ -525,57 +524,85 @@ class APIViewTestCases:
|
||||
query = self._build_query(field_name, id=object_id)
|
||||
|
||||
# Non-authenticated requests should fail
|
||||
with disable_warnings('django.request'):
|
||||
header = {
|
||||
'HTTP_ACCEPT': 'application/json',
|
||||
}
|
||||
self.assertHttpStatus(self.client.post(url, data={'query': query}, format="json", **header), status.HTTP_403_FORBIDDEN)
|
||||
with disable_warnings('django.request'):
|
||||
response = self.client.post(url, data={'query': query}, format="json", **header)
|
||||
self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# Add object-level permission
|
||||
# Add constrained permission
|
||||
obj_perm = ObjectPermission(
|
||||
name='Test permission',
|
||||
actions=['view']
|
||||
actions=['view'],
|
||||
constraints={'id': 0} # Impossible constraint
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
|
||||
|
||||
# Request should succeed but return empty result
|
||||
with disable_logging():
|
||||
response = self.client.post(url, data={'query': query}, format="json", **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
data = json.loads(response.content)
|
||||
self.assertIn('errors', data)
|
||||
self.assertIsNone(data['data'])
|
||||
|
||||
# Remove permission constraint
|
||||
obj_perm.constraints = None
|
||||
obj_perm.save()
|
||||
|
||||
# Request should return requested object
|
||||
response = self.client.post(url, data={'query': query}, format="json", **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
data = json.loads(response.content)
|
||||
self.assertNotIn('errors', data)
|
||||
self.assertIsNotNone(data['data'])
|
||||
|
||||
@override_settings(LOGIN_REQUIRED=True)
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*', 'auth.user'])
|
||||
def test_graphql_list_objects(self):
|
||||
url = reverse('graphql')
|
||||
field_name = f'{self._get_graphql_base_name()}_list'
|
||||
query = self._build_query(field_name)
|
||||
|
||||
# Non-authenticated requests should fail
|
||||
with disable_warnings('django.request'):
|
||||
header = {
|
||||
'HTTP_ACCEPT': 'application/json',
|
||||
}
|
||||
self.assertHttpStatus(self.client.post(url, data={'query': query}, format="json", **header), status.HTTP_403_FORBIDDEN)
|
||||
with disable_warnings('django.request'):
|
||||
response = self.client.post(url, data={'query': query}, format="json", **header)
|
||||
self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# Add object-level permission
|
||||
# Add constrained permission
|
||||
obj_perm = ObjectPermission(
|
||||
name='Test permission',
|
||||
actions=['view']
|
||||
actions=['view'],
|
||||
constraints={'id': 0} # Impossible constraint
|
||||
)
|
||||
obj_perm.save()
|
||||
obj_perm.users.add(self.user)
|
||||
obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
|
||||
|
||||
# Request should succeed but return empty results list
|
||||
response = self.client.post(url, data={'query': query}, format="json", **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
data = json.loads(response.content)
|
||||
self.assertNotIn('errors', data)
|
||||
self.assertGreater(len(data['data'][field_name]), 0)
|
||||
self.assertEqual(len(data['data'][field_name]), 0)
|
||||
|
||||
# Remove permission constraint
|
||||
obj_perm.constraints = None
|
||||
obj_perm.save()
|
||||
|
||||
# Request should return all objects
|
||||
response = self.client.post(url, data={'query': query}, format="json", **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
data = json.loads(response.content)
|
||||
self.assertNotIn('errors', data)
|
||||
self.assertEqual(len(data['data'][field_name]), self.model.objects.count())
|
||||
|
||||
@override_settings(LOGIN_REQUIRED=True)
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*', 'auth.user'])
|
||||
def test_graphql_filter_objects(self):
|
||||
if not hasattr(self, 'graphql_filter'):
|
||||
return
|
||||
|
@ -107,6 +107,16 @@ def disable_warnings(logger_name):
|
||||
logger.setLevel(current_level)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def disable_logging(level=logging.CRITICAL):
|
||||
"""
|
||||
Temporarily suppress log messages at or below the specified level (default: critical).
|
||||
"""
|
||||
logging.disable(level)
|
||||
yield
|
||||
logging.disable(logging.NOTSET)
|
||||
|
||||
|
||||
#
|
||||
# Custom field testing
|
||||
#
|
||||
|
@ -3,38 +3,25 @@ from typing import List
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from virtualization import models
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class VirtualizationQuery:
|
||||
@strawberry.field
|
||||
def cluster(self, id: int) -> ClusterType:
|
||||
return models.Cluster.objects.get(pk=id)
|
||||
cluster: ClusterType = strawberry_django.field()
|
||||
cluster_list: List[ClusterType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def cluster_group(self, id: int) -> ClusterGroupType:
|
||||
return models.ClusterGroup.objects.get(pk=id)
|
||||
cluster_group: ClusterGroupType = strawberry_django.field()
|
||||
cluster_group_list: List[ClusterGroupType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def cluster_type(self, id: int) -> ClusterTypeType:
|
||||
return models.ClusterType.objects.get(pk=id)
|
||||
cluster_type: ClusterTypeType = strawberry_django.field()
|
||||
cluster_type_list: List[ClusterTypeType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def virtual_machine(self, id: int) -> VirtualMachineType:
|
||||
return models.VirtualMachine.objects.get(pk=id)
|
||||
virtual_machine: VirtualMachineType = strawberry_django.field()
|
||||
virtual_machine_list: List[VirtualMachineType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def vm_interface(self, id: int) -> VMInterfaceType:
|
||||
return models.VMInterface.objects.get(pk=id)
|
||||
vm_interface: VMInterfaceType = strawberry_django.field()
|
||||
vm_interface_list: List[VMInterfaceType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def virtual_disk(self, id: int) -> VirtualDiskType:
|
||||
return models.VirtualDisk.objects.get(pk=id)
|
||||
virtual_disk: VirtualDiskType = strawberry_django.field()
|
||||
virtual_disk_list: List[VirtualDiskType] = strawberry_django.field()
|
||||
|
@ -3,58 +3,37 @@ from typing import List
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from vpn import models
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class VPNQuery:
|
||||
@strawberry.field
|
||||
def ike_policy(self, id: int) -> IKEPolicyType:
|
||||
return models.IKEPolicy.objects.get(pk=id)
|
||||
ike_policy: IKEPolicyType = strawberry_django.field()
|
||||
ike_policy_list: List[IKEPolicyType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def ike_proposal(self, id: int) -> IKEProposalType:
|
||||
return models.IKEProposal.objects.get(pk=id)
|
||||
ike_proposal: IKEProposalType = strawberry_django.field()
|
||||
ike_proposal_list: List[IKEProposalType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def ipsec_policy(self, id: int) -> IPSecPolicyType:
|
||||
return models.IPSecPolicy.objects.get(pk=id)
|
||||
ipsec_policy: IPSecPolicyType = strawberry_django.field()
|
||||
ipsec_policy_list: List[IPSecPolicyType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def ipsec_profile(self, id: int) -> IPSecProfileType:
|
||||
return models.IPSecProfile.objects.get(pk=id)
|
||||
ipsec_profile: IPSecProfileType = strawberry_django.field()
|
||||
ipsec_profile_list: List[IPSecProfileType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def ipsec_proposal(self, id: int) -> IPSecProposalType:
|
||||
return models.IPSecProposal.objects.get(pk=id)
|
||||
ipsec_proposal: IPSecProposalType = strawberry_django.field()
|
||||
ipsec_proposal_list: List[IPSecProposalType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def l2vpn(self, id: int) -> L2VPNType:
|
||||
return models.L2VPN.objects.get(pk=id)
|
||||
l2vpn: L2VPNType = strawberry_django.field()
|
||||
l2vpn_list: List[L2VPNType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def l2vpn_termination(self, id: int) -> L2VPNTerminationType:
|
||||
return models.L2VPNTermination.objects.get(pk=id)
|
||||
l2vpn_termination: L2VPNTerminationType = strawberry_django.field()
|
||||
l2vpn_termination_list: List[L2VPNTerminationType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def tunnel(self, id: int) -> TunnelType:
|
||||
return models.Tunnel.objects.get(pk=id)
|
||||
tunnel: TunnelType = strawberry_django.field()
|
||||
tunnel_list: List[TunnelType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def tunnel_group(self, id: int) -> TunnelGroupType:
|
||||
return models.TunnelGroup.objects.get(pk=id)
|
||||
tunnel_group: TunnelGroupType = strawberry_django.field()
|
||||
tunnel_group_list: List[TunnelGroupType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def tunnel_termination(self, id: int) -> TunnelTerminationType:
|
||||
return models.TunnelTermination.objects.get(pk=id)
|
||||
tunnel_termination: TunnelTerminationType = strawberry_django.field()
|
||||
tunnel_termination_list: List[TunnelTerminationType] = strawberry_django.field()
|
||||
|
@ -3,23 +3,16 @@ from typing import List
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
|
||||
from wireless import models
|
||||
from .types import *
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@strawberry.type(name="Query")
|
||||
class WirelessQuery:
|
||||
@strawberry.field
|
||||
def wireless_lan(self, id: int) -> WirelessLANType:
|
||||
return models.WirelessLAN.objects.get(pk=id)
|
||||
wireless_lan: WirelessLANType = strawberry_django.field()
|
||||
wireless_lan_list: List[WirelessLANType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def wireless_lan_group(self, id: int) -> WirelessLANGroupType:
|
||||
return models.WirelessLANGroup.objects.get(pk=id)
|
||||
wireless_lan_group: WirelessLANGroupType = strawberry_django.field()
|
||||
wireless_lan_group_list: List[WirelessLANGroupType] = strawberry_django.field()
|
||||
|
||||
@strawberry.field
|
||||
def wireless_link(self, id: int) -> WirelessLinkType:
|
||||
return models.WirelessLink.objects.get(pk=id)
|
||||
wireless_link: WirelessLinkType = strawberry_django.field()
|
||||
wireless_link_list: List[WirelessLinkType] = strawberry_django.field()
|
||||
|
@ -19,8 +19,8 @@ drf-spectacular-sidecar==2024.7.1
|
||||
feedparser==6.0.11
|
||||
gunicorn==23.0.0
|
||||
Jinja2==3.1.4
|
||||
Markdown==3.6
|
||||
mkdocs-material==9.5.31
|
||||
Markdown==3.7
|
||||
mkdocs-material==9.5.33
|
||||
mkdocstrings[python-legacy]==0.25.2
|
||||
netaddr==1.3.0
|
||||
nh3==0.2.18
|
||||
|
Loading…
Reference in New Issue
Block a user