mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Merge branch 'netbox-community:main' into feat-19492
This commit is contained in:
commit
b0f61c6288
@ -52,7 +52,7 @@ Although it is not recommended, the default validation rules can be disabled by
|
||||
|
||||
Default: `False`
|
||||
|
||||
If True, cross-origin resource sharing (CORS) requests will be accepted from all origins. If False, a whitelist will be used (see below).
|
||||
If `True`, cross-origin resource sharing (CORS) requests will be accepted from all origins. If False, a whitelist will be used (see below).
|
||||
|
||||
---
|
||||
|
||||
@ -84,7 +84,7 @@ The name of the cookie to use for the cross-site request forgery (CSRF) authenti
|
||||
|
||||
Default: `False`
|
||||
|
||||
If true, the cookie employed for cross-site request forgery (CSRF) protection will be marked as secure, meaning that it can only be sent across an HTTPS connection.
|
||||
If `True`, the cookie employed for cross-site request forgery (CSRF) protection will be marked as secure, meaning that it can only be sent across an HTTPS connection.
|
||||
|
||||
---
|
||||
|
||||
@ -164,7 +164,7 @@ EXEMPT_VIEW_PERMISSIONS = ['*']
|
||||
|
||||
Default: `False`
|
||||
|
||||
If true, the lifetime of a user's authentication session will be automatically reset upon each valid request. For example, if [`LOGIN_TIMEOUT`](#login_timeout) is configured to 14 days (the default), and a user whose session is due to expire in five days makes a NetBox request (with a valid session cookie), the session's lifetime will be reset to 14 days.
|
||||
If `True`, the lifetime of a user's authentication session will be automatically reset upon each valid request. For example, if [`LOGIN_TIMEOUT`](#login_timeout) is configured to 14 days (the default), and a user whose session is due to expire in five days makes a NetBox request (with a valid session cookie), the session's lifetime will be reset to 14 days.
|
||||
|
||||
Note that enabling this setting causes NetBox to update a user's session in the database (or file, as configured per [`SESSION_FILE_PATH`](#session_file_path)) with each request, which may introduce significant overhead in very active environments. It also permits an active user to remain authenticated to NetBox indefinitely.
|
||||
|
||||
@ -212,7 +212,7 @@ The view name or URL to which a user is redirected after logging out.
|
||||
|
||||
Default: `False`
|
||||
|
||||
If true, the `includeSubDomains` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to apply the HSTS policy to all subdomains of the current domain.
|
||||
If `True`, the `includeSubDomains` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to apply the HSTS policy to all subdomains of the current domain.
|
||||
|
||||
---
|
||||
|
||||
@ -220,7 +220,7 @@ If true, the `includeSubDomains` directive will be included in the HTTP Strict T
|
||||
|
||||
Default: `False`
|
||||
|
||||
If true, the `preload` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to preload the site in HTTPS. Browsers that use the HSTS preload list will force the site to be accessed via HTTPS even if the user types HTTP in the address bar.
|
||||
If `True`, the `preload` directive will be included in the HTTP Strict Transport Security (HSTS) header. This directive instructs the browser to preload the site in HTTPS. Browsers that use the HSTS preload list will force the site to be accessed via HTTPS even if the user types HTTP in the address bar.
|
||||
|
||||
---
|
||||
|
||||
@ -236,7 +236,7 @@ If set to a non-zero integer value, the SecurityMiddleware sets the HTTP Strict
|
||||
|
||||
Default: `False`
|
||||
|
||||
If true, all non-HTTPS requests will be automatically redirected to use HTTPS.
|
||||
If `True`, all non-HTTPS requests will be automatically redirected to use HTTPS.
|
||||
|
||||
!!! warning
|
||||
Ensure that your frontend HTTP daemon has been configured to forward the HTTP scheme correctly before enabling this option. An incorrectly configured frontend may result in a looping redirect.
|
||||
@ -255,7 +255,7 @@ The name used for the session cookie. See the [Django documentation](https://doc
|
||||
|
||||
Default: `False`
|
||||
|
||||
If true, the cookie employed for session authentication will be marked as secure, meaning that it can only be sent across an HTTPS connection.
|
||||
If `True`, the cookie employed for session authentication will be marked as secure, meaning that it can only be sent across an HTTPS connection.
|
||||
|
||||
---
|
||||
|
||||
|
@ -95,7 +95,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`](./development.md#debug) is true).
|
||||
addresses (and [`DEBUG`](./development.md#debug) is `True`).
|
||||
|
||||
---
|
||||
|
||||
@ -103,7 +103,7 @@ addresses (and [`DEBUG`](./development.md#debug) is true).
|
||||
|
||||
Default: `False`
|
||||
|
||||
Set this configuration parameter to True for NetBox deployments which do not have Internet access. This will disable miscellaneous functionality which depends on access to the Internet.
|
||||
Set this configuration parameter to `True` for NetBox deployments which do not have Internet access. This will disable miscellaneous functionality which depends on access to the Internet.
|
||||
|
||||
!!! note
|
||||
If Internet access is available via a proxy, set [`HTTP_PROXIES`](#http_proxies) instead.
|
||||
@ -114,7 +114,7 @@ Set this configuration parameter to True for NetBox deployments which do not hav
|
||||
|
||||
Default: `{}`
|
||||
|
||||
A dictionary of custom jinja2 filters with the key being the filter name and the value being a callable. For more information see the [Jinja2 documentation](https://jinja.palletsprojects.com/en/3.1.x/api/#custom-filters). For example:
|
||||
A dictionary of custom Jinja2 filters with the key being the filter name and the value being a callable. For more information see the [Jinja2 documentation](https://jinja.palletsprojects.com/en/3.1.x/api/#custom-filters). For example:
|
||||
|
||||
```python
|
||||
def uppercase(x):
|
||||
|
@ -54,6 +54,7 @@ If a new Django release is adopted or other major dependencies (Python, PostgreS
|
||||
* Update the installation guide (`docs/installation/index.md`) with the new minimum versions.
|
||||
* Update the upgrade guide (`docs/installation/upgrading.md`) for the current version accordingly.
|
||||
* Update the minimum PostgreSQL version in the programming error template (`netbox/templates/exceptions/programming_error.html`).
|
||||
* Update the minimum and supported Python versions in the project metadata file (`pyproject.toml`)
|
||||
|
||||
### Manually Perform a New Install
|
||||
|
||||
@ -165,7 +166,7 @@ Then, compile these portable (`.po`) files for use in the application:
|
||||
|
||||
### Update Version and Changelog
|
||||
|
||||
* Update the version number and date in `netbox/release.yaml`. Add or remove the designation (e.g. `beta1`) if applicable.
|
||||
* Update the version number and date in `netbox/release.yaml` and `pyproject.toml`. Add or remove the designation (e.g. `beta1`) if applicable.
|
||||
* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`.
|
||||
* Add a section for this release at the top of the changelog page for the minor version (e.g. `docs/release-notes/version-4.2.md`) listing all relevant changes made in this release.
|
||||
|
||||
|
@ -9,7 +9,7 @@ NetBox is the leading solution for modeling and documenting modern networks. By
|
||||
|
||||
## :material-server-network: Built for Networks
|
||||
|
||||
Unlike general-purpose CMDBs, NetBox has curated a data model which caters specifically to the needs of network engineers and operators. It delivers a wide assortment of object types carefully crafted to best serve the needs of infrastructure design and documentation. These cover all facets of network technology, from IP address managements to cabling to overlays and more:
|
||||
Unlike general-purpose configuration management databases (CMDBs), NetBox has curated a data model which caters specifically to the needs of network engineers and operators. It delivers a wide assortment of object types carefully crafted to best serve the needs of infrastructure design and documentation. These cover all facets of network technology, from IP address managements to cabling to overlays and more:
|
||||
|
||||
* Hierarchical regions, sites, and locations
|
||||
* Racks, devices, and device components
|
||||
|
@ -122,7 +122,7 @@ sudo cp /opt/netbox-$OLDVER/gunicorn.py /opt/netbox/
|
||||
|
||||
### Option B: Check Out a Git Release
|
||||
|
||||
This guide assumes that NetBox is installed at `/opt/netbox`. First, determine the latest release either by visiting our [releases page](https://github.com/netbox-community/netbox/releases) or by running the following command:
|
||||
This guide assumes that NetBox is installed in `/opt/netbox`. First, determine the latest release either by visiting our [releases page](https://github.com/netbox-community/netbox/releases) or by running the following command:
|
||||
|
||||
```
|
||||
git ls-remote --tags https://github.com/netbox-community/netbox.git \
|
||||
@ -134,6 +134,8 @@ git ls-remote --tags https://github.com/netbox-community/netbox.git \
|
||||
Check out the desired release by specifying its tag. For example:
|
||||
|
||||
```
|
||||
cd /opt/netbox && \
|
||||
sudo git fetch && \
|
||||
sudo git checkout v4.2.7
|
||||
```
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Views
|
||||
|
||||
## Writing Views
|
||||
## Writing Basic Views
|
||||
|
||||
If your plugin will provide its own page or pages within the NetBox web UI, you'll need to define views. A view is a piece of business logic which performs an action and/or renders a page when a request is made to a particular URL. HTML content is rendered using a [template](./templates.md). Views are typically defined in `views.py`, and URL patterns in `urls.py`.
|
||||
|
||||
@ -47,9 +47,13 @@ A URL pattern has three components:
|
||||
|
||||
This makes our view accessible at the URL `/plugins/animal-sounds/random/`. (Remember, our `AnimalSoundsConfig` class sets our plugin's base URL to `animal-sounds`.) Viewing this URL should show the base NetBox template with our custom content inside it.
|
||||
|
||||
## NetBox Model Views
|
||||
|
||||
NetBox provides several generic view classes and additional helper functions, to simplify the implementation of plugin logic. These are recommended to be used whenever possible to keep the maintenance overhead of plugins low.
|
||||
|
||||
### View Classes
|
||||
|
||||
NetBox provides several generic view classes (documented below) to facilitate common operations, such as creating, viewing, modifying, and deleting objects. Plugins can subclass these views for their own use.
|
||||
Generic view classes (documented below) facilitate common operations, such as creating, viewing, modifying, and deleting objects. Plugins can subclass these views for their own use.
|
||||
|
||||
| View Class | Description |
|
||||
|----------------------|--------------------------------------------------------|
|
||||
@ -65,18 +69,51 @@ NetBox provides several generic view classes (documented below) to facilitate co
|
||||
!!! warning
|
||||
Please note that only the classes which appear in this documentation are currently supported. Although other classes may be present within the `views.generic` module, they are not yet supported for use by plugins.
|
||||
|
||||
#### Example Usage
|
||||
### URL registration
|
||||
|
||||
The NetBox URL registration process has two parts:
|
||||
|
||||
1. View classes can be decorated with `@register_model_view()`. This registers a new URL for the model.
|
||||
2. All of a model's URLs can be included in `urls.py` using the `get_model_urls()` function. This call is usually required twice: once to import general views for the model and again to import model detail views tied to the object's primary key.
|
||||
|
||||
::: utilities.views.register_model_view
|
||||
|
||||
!!! note "Changed in NetBox v4.2"
|
||||
In NetBox v4.2, the `register_model_view()` function was extended to support the registration of list views by passing `detail=False`.
|
||||
|
||||
::: utilities.urls.get_model_urls
|
||||
|
||||
!!! note "Changed in NetBox v4.2"
|
||||
In NetBox v4.2, the `get_model_urls()` function was extended to support retrieving registered general model views (e.g. for listing objects) by passing `detail=False`.
|
||||
|
||||
### Example Usage
|
||||
|
||||
```python
|
||||
# views.py
|
||||
from netbox.views.generic import ObjectEditView
|
||||
from utilities.views import register_model_view
|
||||
from .models import Thing
|
||||
|
||||
@register_model_view(Thing, name='add', detail=False)
|
||||
@register_model_view(Thing, name='edit')
|
||||
class ThingEditView(ObjectEditView):
|
||||
queryset = Thing.objects.all()
|
||||
template_name = 'myplugin/thing.html'
|
||||
...
|
||||
```
|
||||
|
||||
```python
|
||||
# urls.py
|
||||
from django.urls import include, path
|
||||
from utilities.urls import get_model_urls
|
||||
|
||||
urlpatterns = [
|
||||
path('thing/', include(get_model_urls('myplugin', 'thing', detail=False))),
|
||||
path('thing/<int:pk>/', include(get_model_urls('myplugin', 'thing'))),
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
## Object Views
|
||||
|
||||
Below are the class definitions for NetBox's object views. These views handle CRUD actions for individual objects. The view, add/edit, and delete views each inherit from `BaseObjectView`, which is not intended to be used directly.
|
||||
@ -143,6 +180,9 @@ Below are the class definitions for NetBox's multi-object views. These views han
|
||||
|
||||
These views are provided to enable or enhance certain NetBox model features, such as change logging or journaling. These typically do not need to be subclassed: They can be used directly e.g. in a URL path.
|
||||
|
||||
!!! note
|
||||
These feature views are automatically registered for all models that implement the respective feature. There is usually no need to override them. However, if that's the case, the URL must be registered manually in `urls.py` instead of using the `register_model_view()` function or decorator.
|
||||
|
||||
::: netbox.views.generic.ObjectChangeLogView
|
||||
options:
|
||||
members:
|
||||
@ -157,7 +197,7 @@ These views are provided to enable or enhance certain NetBox model features, suc
|
||||
|
||||
### Additional Tabs
|
||||
|
||||
Plugins can "attach" a custom view to a core NetBox model by registering it with `register_model_view()`. To include a tab for this view within the NetBox UI, declare a TabView instance named `tab`, and add it to the template context dict:
|
||||
Plugins can "attach" a custom view to a NetBox model by registering it with `register_model_view()`. To include a tab for this view within the NetBox UI, declare a TabView instance named `tab`, and add it to the template context dict:
|
||||
|
||||
```python
|
||||
from dcim.models import Site
|
||||
@ -185,11 +225,6 @@ class MyView(generic.ObjectView):
|
||||
)
|
||||
```
|
||||
|
||||
!!! note "Changed in NetBox v4.2"
|
||||
The `register_model_view()` function was extended in NetBox v4.2 to support registration of list views by passing `detail=False`.
|
||||
|
||||
::: utilities.views.register_model_view
|
||||
|
||||
::: utilities.views.ViewTab
|
||||
|
||||
### Extra Template Content
|
||||
|
@ -191,12 +191,9 @@ class ProfileView(LoginRequiredMixin, View):
|
||||
def get(self, request):
|
||||
|
||||
# Compile changelog table
|
||||
changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
|
||||
user=request.user
|
||||
).prefetch_related(
|
||||
'changed_object_type'
|
||||
)[:20]
|
||||
changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(user=request.user)[:20]
|
||||
changelog_table = ObjectChangeTable(changelog)
|
||||
changelog_table.orderable = False
|
||||
changelog_table.configure(request)
|
||||
|
||||
return render(request, self.template_name, {
|
||||
|
@ -16,6 +16,7 @@ from utilities.forms import get_field_value
|
||||
from utilities.forms.fields import (
|
||||
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField,
|
||||
)
|
||||
from utilities.forms.mixins import DistanceValidationMixin
|
||||
from utilities.forms.rendering import FieldSet, InlineFields
|
||||
from utilities.forms.widgets import DatePicker, HTMXSelect, NumberWithOptions
|
||||
from utilities.templatetags.builtins.filters import bettertitle
|
||||
@ -105,7 +106,7 @@ class CircuitTypeForm(NetBoxModelForm):
|
||||
]
|
||||
|
||||
|
||||
class CircuitForm(TenancyForm, NetBoxModelForm):
|
||||
class CircuitForm(DistanceValidationMixin, TenancyForm, NetBoxModelForm):
|
||||
provider = DynamicModelChoiceField(
|
||||
label=_('Provider'),
|
||||
queryset=Provider.objects.all(),
|
||||
|
@ -41,7 +41,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CircuitTermination, lookups=True)
|
||||
@strawberry_django.filter_type(models.CircuitTermination, lookups=True)
|
||||
class CircuitTerminationFilter(
|
||||
BaseObjectTypeFilterMixin,
|
||||
CustomFieldsFilterMixin,
|
||||
@ -87,7 +87,7 @@ class CircuitTerminationFilter(
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Circuit, lookups=True)
|
||||
@strawberry_django.filter_type(models.Circuit, lookups=True)
|
||||
class CircuitFilter(
|
||||
ContactFilterMixin,
|
||||
ImageAttachmentFilterMixin,
|
||||
@ -121,17 +121,17 @@ class CircuitFilter(
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CircuitType, lookups=True)
|
||||
@strawberry_django.filter_type(models.CircuitType, lookups=True)
|
||||
class CircuitTypeFilter(BaseCircuitTypeFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CircuitGroup, lookups=True)
|
||||
@strawberry_django.filter_type(models.CircuitGroup, lookups=True)
|
||||
class CircuitGroupFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CircuitGroupAssignment, lookups=True)
|
||||
@strawberry_django.filter_type(models.CircuitGroupAssignment, lookups=True)
|
||||
class CircuitGroupAssignmentFilter(
|
||||
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
|
||||
):
|
||||
@ -148,7 +148,7 @@ class CircuitGroupAssignmentFilter(
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Provider, lookups=True)
|
||||
@strawberry_django.filter_type(models.Provider, lookups=True)
|
||||
class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@ -158,7 +158,7 @@ class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ProviderAccount, lookups=True)
|
||||
@strawberry_django.filter_type(models.ProviderAccount, lookups=True)
|
||||
class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin):
|
||||
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -168,7 +168,7 @@ class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ProviderNetwork, lookups=True)
|
||||
@strawberry_django.filter_type(models.ProviderNetwork, lookups=True)
|
||||
class ProviderNetworkFilter(PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
@ -178,12 +178,12 @@ class ProviderNetworkFilter(PrimaryModelFilterMixin):
|
||||
service_id: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualCircuitType, lookups=True)
|
||||
@strawberry_django.filter_type(models.VirtualCircuitType, lookups=True)
|
||||
class VirtualCircuitTypeFilter(BaseCircuitTypeFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualCircuit, lookups=True)
|
||||
@strawberry_django.filter_type(models.VirtualCircuit, lookups=True)
|
||||
class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
cid: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
@ -206,7 +206,7 @@ class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualCircuitTermination, lookups=True)
|
||||
@strawberry_django.filter_type(models.VirtualCircuitTermination, lookups=True)
|
||||
class VirtualCircuitTerminationFilter(
|
||||
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
|
||||
):
|
||||
|
@ -0,0 +1,16 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('circuits', '0051_virtualcircuit_group_assignment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='circuit',
|
||||
name='_abs_distance',
|
||||
field=models.DecimalField(blank=True, decimal_places=4, max_digits=13, null=True),
|
||||
),
|
||||
]
|
@ -120,7 +120,8 @@ class CircuitTerminationTable(NetBoxTable):
|
||||
)
|
||||
termination = tables.Column(
|
||||
verbose_name=_('Termination Point'),
|
||||
linkify=True
|
||||
linkify=True,
|
||||
orderable=False,
|
||||
)
|
||||
|
||||
# Termination types
|
||||
@ -132,7 +133,7 @@ class CircuitTerminationTable(NetBoxTable):
|
||||
site_group = tables.Column(
|
||||
verbose_name=_('Site Group'),
|
||||
linkify=True,
|
||||
accessor='_sitegroup'
|
||||
accessor='_site_group'
|
||||
)
|
||||
region = tables.Column(
|
||||
verbose_name=_('Region'),
|
||||
|
@ -54,9 +54,8 @@ class VirtualCircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable)
|
||||
linkify=True,
|
||||
verbose_name=_('Account')
|
||||
)
|
||||
type = tables.Column(
|
||||
type = columns.ColoredLabelColumn(
|
||||
verbose_name=_('Type'),
|
||||
linkify=True
|
||||
)
|
||||
status = columns.ChoiceFieldColumn()
|
||||
termination_count = columns.LinkedCountColumn(
|
||||
|
23
netbox/circuits/tests/test_tables.py
Normal file
23
netbox/circuits/tests/test_tables.py
Normal file
@ -0,0 +1,23 @@
|
||||
from django.test import RequestFactory, tag, TestCase
|
||||
|
||||
from circuits.models import CircuitTermination
|
||||
from circuits.tables import CircuitTerminationTable
|
||||
|
||||
|
||||
@tag('regression')
|
||||
class CircuitTerminationTableTest(TestCase):
|
||||
def test_every_orderable_field_does_not_throw_exception(self):
|
||||
terminations = CircuitTermination.objects.all()
|
||||
disallowed = {'actions', }
|
||||
|
||||
orderable_columns = [
|
||||
column.name for column in CircuitTerminationTable(terminations).columns
|
||||
if column.orderable and column.name not in disallowed
|
||||
]
|
||||
fake_request = RequestFactory().get("/")
|
||||
|
||||
for col in orderable_columns:
|
||||
for dir in ('-', ''):
|
||||
table = CircuitTerminationTable(terminations)
|
||||
table.order_by = f'{dir}{col}'
|
||||
table.as_html(fake_request)
|
@ -23,7 +23,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.DataFile, lookups=True)
|
||||
@strawberry_django.filter_type(models.DataFile, lookups=True)
|
||||
class DataFileFilter(BaseFilterMixin):
|
||||
id: ID | None = strawberry_django.filter_field()
|
||||
created: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
|
||||
@ -39,7 +39,7 @@ class DataFileFilter(BaseFilterMixin):
|
||||
hash: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.DataSource, lookups=True)
|
||||
@strawberry_django.filter_type(models.DataSource, lookups=True)
|
||||
class DataSourceFilter(PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
type: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@ -56,7 +56,7 @@ class DataSourceFilter(PrimaryModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ObjectChange, lookups=True)
|
||||
@strawberry_django.filter_type(models.ObjectChange, lookups=True)
|
||||
class ObjectChangeFilter(BaseFilterMixin):
|
||||
id: ID | None = strawberry_django.filter_field()
|
||||
time: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
|
||||
@ -82,7 +82,7 @@ class ObjectChangeFilter(BaseFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(DjangoContentType, lookups=True)
|
||||
@strawberry_django.filter_type(DjangoContentType, lookups=True)
|
||||
class ContentTypeFilter(BaseFilterMixin):
|
||||
id: ID | None = strawberry_django.filter_field()
|
||||
app_label: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
@ -9,6 +9,7 @@ from rq.registry import FailedJobRegistry, StartedJobRegistry
|
||||
|
||||
from users.models import Token, User
|
||||
from utilities.testing import APITestCase, APIViewTestCases, TestCase
|
||||
from utilities.testing.utils import disable_logging
|
||||
from ..models import *
|
||||
|
||||
|
||||
@ -189,7 +190,8 @@ class BackgroundTaskTestCase(TestCase):
|
||||
# Enqueue & run a job that will fail
|
||||
job = queue.enqueue(self.dummy_job_failing)
|
||||
worker = get_worker('default')
|
||||
worker.work(burst=True)
|
||||
with disable_logging():
|
||||
worker.work(burst=True)
|
||||
self.assertTrue(job.is_failed)
|
||||
|
||||
# Re-enqueue the failed job and check that its status has been reset
|
||||
@ -231,7 +233,8 @@ class BackgroundTaskTestCase(TestCase):
|
||||
self.assertEqual(job.get_status(), JobStatus.STARTED)
|
||||
response = self.client.post(reverse('core-api:rqtask-stop', args=[job.id]), **self.header)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started
|
||||
with disable_logging():
|
||||
worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started
|
||||
started_job_registry = StartedJobRegistry(queue.name, connection=queue.connection)
|
||||
self.assertEqual(len(started_job_registry), 0)
|
||||
|
||||
|
@ -14,7 +14,7 @@ from core.choices import ObjectChangeActionChoices
|
||||
from core.models import *
|
||||
from dcim.models import Site
|
||||
from users.models import User
|
||||
from utilities.testing import TestCase, ViewTestCases, create_tags
|
||||
from utilities.testing import TestCase, ViewTestCases, create_tags, disable_logging
|
||||
|
||||
|
||||
class DataSourceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
@ -271,7 +271,8 @@ class BackgroundTaskTestCase(TestCase):
|
||||
# Enqueue & run a job that will fail
|
||||
job = queue.enqueue(self.dummy_job_failing)
|
||||
worker = get_worker('default')
|
||||
worker.work(burst=True)
|
||||
with disable_logging():
|
||||
worker.work(burst=True)
|
||||
self.assertTrue(job.is_failed)
|
||||
|
||||
# Re-enqueue the failed job and check that its status has been reset
|
||||
@ -317,7 +318,8 @@ class BackgroundTaskTestCase(TestCase):
|
||||
self.assertEqual(len(started_job_registry), 1)
|
||||
response = self.client.get(reverse('core:background_task_stop', args=[job.id]))
|
||||
self.assertEqual(response.status_code, 302)
|
||||
worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started
|
||||
with disable_logging():
|
||||
worker.monitor_work_horse(job, queue) # Sets the job as Failed and removes from Started
|
||||
self.assertEqual(len(started_job_registry), 0)
|
||||
|
||||
canceled_job_registry = FailedJobRegistry(queue.name, connection=queue.connection)
|
||||
|
@ -2012,6 +2012,21 @@ class InterfaceFilterSet(
|
||||
'wireless': queryset.filter(type__in=WIRELESS_IFACE_TYPES),
|
||||
}.get(value, queryset.none())
|
||||
|
||||
# Override the method on CabledObjectFilterSet to also check for wireless links
|
||||
def filter_occupied(self, queryset, name, value):
|
||||
if value:
|
||||
return queryset.filter(
|
||||
Q(cable__isnull=False) |
|
||||
Q(wireless_link__isnull=False) |
|
||||
Q(mark_connected=True)
|
||||
)
|
||||
else:
|
||||
return queryset.filter(
|
||||
cable__isnull=True,
|
||||
wireless_link__isnull=True,
|
||||
mark_connected=False
|
||||
)
|
||||
|
||||
|
||||
class FrontPortFilterSet(
|
||||
ModularDeviceComponentFilterSet,
|
||||
|
@ -90,7 +90,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Cable, lookups=True)
|
||||
@strawberry_django.filter_type(models.Cable, lookups=True)
|
||||
class CableFilter(PrimaryModelFilterMixin, TenancyFilterMixin):
|
||||
type: Annotated['CableTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
status: Annotated['LinkStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
@ -107,7 +107,7 @@ class CableFilter(PrimaryModelFilterMixin, TenancyFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CableTermination, lookups=True)
|
||||
@strawberry_django.filter_type(models.CableTermination, lookups=True)
|
||||
class CableTerminationFilter(ChangeLogFilterMixin):
|
||||
cable: Annotated['CableFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
cable_id: ID | None = strawberry_django.filter_field()
|
||||
@ -120,7 +120,7 @@ class CableTerminationFilter(ChangeLogFilterMixin):
|
||||
termination_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ConsolePort, lookups=True)
|
||||
@strawberry_django.filter_type(models.ConsolePort, lookups=True)
|
||||
class ConsolePortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -130,14 +130,14 @@ class ConsolePortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilte
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ConsolePortTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.ConsolePortTemplate, lookups=True)
|
||||
class ConsolePortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ConsoleServerPort, lookups=True)
|
||||
@strawberry_django.filter_type(models.ConsoleServerPort, lookups=True)
|
||||
class ConsoleServerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -147,14 +147,14 @@ class ConsoleServerPortFilter(ModularComponentModelFilterMixin, CabledObjectMode
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ConsoleServerPortTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.ConsoleServerPortTemplate, lookups=True)
|
||||
class ConsoleServerPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Device, lookups=True)
|
||||
@strawberry_django.filter_type(models.Device, lookups=True)
|
||||
class DeviceFilter(
|
||||
ContactFilterMixin,
|
||||
TenancyFilterMixin,
|
||||
@ -271,7 +271,7 @@ class DeviceFilter(
|
||||
inventory_item_count: FilterLookup[int] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.DeviceBay, lookups=True)
|
||||
@strawberry_django.filter_type(models.DeviceBay, lookups=True)
|
||||
class DeviceBayFilter(ComponentModelFilterMixin):
|
||||
installed_device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -279,12 +279,12 @@ class DeviceBayFilter(ComponentModelFilterMixin):
|
||||
installed_device_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.DeviceBayTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.DeviceBayTemplate, lookups=True)
|
||||
class DeviceBayTemplateFilter(ComponentTemplateFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.InventoryItemTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.InventoryItemTemplate, lookups=True)
|
||||
class InventoryItemTemplateFilter(ComponentTemplateFilterMixin):
|
||||
parent: Annotated['InventoryItemTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -304,13 +304,13 @@ class InventoryItemTemplateFilter(ComponentTemplateFilterMixin):
|
||||
part_id: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.DeviceRole, lookups=True)
|
||||
@strawberry_django.filter_type(models.DeviceRole, lookups=True)
|
||||
class DeviceRoleFilter(OrganizationalModelFilterMixin, RenderConfigFilterMixin):
|
||||
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
vm_role: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.DeviceType, lookups=True)
|
||||
@strawberry_django.filter_type(models.DeviceType, lookups=True)
|
||||
class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin):
|
||||
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -382,7 +382,7 @@ class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, Weig
|
||||
inventory_item_template_count: FilterLookup[int] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.FrontPort, lookups=True)
|
||||
@strawberry_django.filter_type(models.FrontPort, lookups=True)
|
||||
class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
@ -395,7 +395,7 @@ class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterM
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.FrontPortTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.FrontPortTemplate, lookups=True)
|
||||
class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
@ -408,7 +408,7 @@ class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.MACAddress, lookups=True)
|
||||
@strawberry_django.filter_type(models.MACAddress, lookups=True)
|
||||
class MACAddressFilter(PrimaryModelFilterMixin):
|
||||
mac_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
@ -417,7 +417,7 @@ class MACAddressFilter(PrimaryModelFilterMixin):
|
||||
assigned_object_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Interface, lookups=True)
|
||||
@strawberry_django.filter_type(models.Interface, lookups=True)
|
||||
class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin, CabledObjectModelFilterMixin):
|
||||
vcdcs: Annotated['VirtualDeviceContextFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -486,7 +486,7 @@ class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.InterfaceTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.InterfaceTemplate, lookups=True)
|
||||
class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
type: Annotated['InterfaceTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -508,7 +508,7 @@ class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.InventoryItem, lookups=True)
|
||||
@strawberry_django.filter_type(models.InventoryItem, lookups=True)
|
||||
class InventoryItemFilter(ComponentModelFilterMixin):
|
||||
parent: Annotated['InventoryItemFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -535,12 +535,12 @@ class InventoryItemFilter(ComponentModelFilterMixin):
|
||||
discovered: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.InventoryItemRole, lookups=True)
|
||||
@strawberry_django.filter_type(models.InventoryItemRole, lookups=True)
|
||||
class InventoryItemRoleFilter(OrganizationalModelFilterMixin):
|
||||
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Location, lookups=True)
|
||||
@strawberry_django.filter_type(models.Location, lookups=True)
|
||||
class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, NestedGroupModelFilterMixin):
|
||||
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
site_id: ID | None = strawberry_django.filter_field()
|
||||
@ -556,12 +556,12 @@ class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilt
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Manufacturer, lookups=True)
|
||||
@strawberry_django.filter_type(models.Manufacturer, lookups=True)
|
||||
class ManufacturerFilter(ContactFilterMixin, OrganizationalModelFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Module, lookups=True)
|
||||
@strawberry_django.filter_type(models.Module, lookups=True)
|
||||
class ModuleFilter(PrimaryModelFilterMixin, ConfigContextFilterMixin):
|
||||
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
device_id: ID | None = strawberry_django.filter_field()
|
||||
@ -610,7 +610,7 @@ class ModuleFilter(PrimaryModelFilterMixin, ConfigContextFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ModuleBay, lookups=True)
|
||||
@strawberry_django.filter_type(models.ModuleBay, lookups=True)
|
||||
class ModuleBayFilter(ModularComponentModelFilterMixin):
|
||||
parent: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -619,17 +619,17 @@ class ModuleBayFilter(ModularComponentModelFilterMixin):
|
||||
position: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ModuleBayTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.ModuleBayTemplate, lookups=True)
|
||||
class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
position: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ModuleTypeProfile, lookups=True)
|
||||
@strawberry_django.filter_type(models.ModuleTypeProfile, lookups=True)
|
||||
class ModuleTypeProfileFilter(PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ModuleType, lookups=True)
|
||||
@strawberry_django.filter_type(models.ModuleType, lookups=True)
|
||||
class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin):
|
||||
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -676,7 +676,7 @@ class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, Weig
|
||||
) = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Platform, lookups=True)
|
||||
@strawberry_django.filter_type(models.Platform, lookups=True)
|
||||
class PlatformFilter(OrganizationalModelFilterMixin):
|
||||
manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -688,7 +688,7 @@ class PlatformFilter(OrganizationalModelFilterMixin):
|
||||
config_template_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.PowerFeed, lookups=True)
|
||||
@strawberry_django.filter_type(models.PowerFeed, lookups=True)
|
||||
class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
power_panel: Annotated['PowerPanelFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -723,7 +723,7 @@ class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryM
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.PowerOutlet, lookups=True)
|
||||
@strawberry_django.filter_type(models.PowerOutlet, lookups=True)
|
||||
class PowerOutletFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
type: Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -738,7 +738,7 @@ class PowerOutletFilter(ModularComponentModelFilterMixin, CabledObjectModelFilte
|
||||
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.PowerOutletTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.PowerOutletTemplate, lookups=True)
|
||||
class PowerOutletTemplateFilter(ModularComponentModelFilterMixin):
|
||||
type: Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -752,7 +752,7 @@ class PowerOutletTemplateFilter(ModularComponentModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.PowerPanel, lookups=True)
|
||||
@strawberry_django.filter_type(models.PowerPanel, lookups=True)
|
||||
class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryModelFilterMixin):
|
||||
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
site_id: ID | None = strawberry_django.filter_field()
|
||||
@ -765,7 +765,7 @@ class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryMo
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.PowerPort, lookups=True)
|
||||
@strawberry_django.filter_type(models.PowerPort, lookups=True)
|
||||
class PowerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
type: Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -778,7 +778,7 @@ class PowerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterM
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.PowerPortTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.PowerPortTemplate, lookups=True)
|
||||
class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
type: Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -791,7 +791,7 @@ class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.RackType, lookups=True)
|
||||
@strawberry_django.filter_type(models.RackType, lookups=True)
|
||||
class RackTypeFilter(RackBaseFilterMixin):
|
||||
form_factor: Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -804,7 +804,7 @@ class RackTypeFilter(RackBaseFilterMixin):
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Rack, lookups=True)
|
||||
@strawberry_django.filter_type(models.Rack, lookups=True)
|
||||
class RackFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, RackBaseFilterMixin):
|
||||
form_factor: Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -836,7 +836,7 @@ class RackFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMi
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.RackReservation, lookups=True)
|
||||
@strawberry_django.filter_type(models.RackReservation, lookups=True)
|
||||
class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
rack_id: ID | None = strawberry_django.filter_field()
|
||||
@ -848,12 +848,12 @@ class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.RackRole, lookups=True)
|
||||
@strawberry_django.filter_type(models.RackRole, lookups=True)
|
||||
class RackRoleFilter(OrganizationalModelFilterMixin):
|
||||
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.RearPort, lookups=True)
|
||||
@strawberry_django.filter_type(models.RearPort, lookups=True)
|
||||
class RearPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin):
|
||||
type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
@ -862,7 +862,7 @@ class RearPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMi
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.RearPortTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.RearPortTemplate, lookups=True)
|
||||
class RearPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
@ -871,7 +871,7 @@ class RearPortTemplateFilter(ModularComponentTemplateFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Region, lookups=True)
|
||||
@strawberry_django.filter_type(models.Region, lookups=True)
|
||||
class RegionFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
|
||||
prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -881,7 +881,7 @@ class RegionFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Site, lookups=True)
|
||||
@strawberry_django.filter_type(models.Site, lookups=True)
|
||||
class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@ -915,7 +915,7 @@ class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMi
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.SiteGroup, lookups=True)
|
||||
@strawberry_django.filter_type(models.SiteGroup, lookups=True)
|
||||
class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
|
||||
prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -925,7 +925,7 @@ class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualChassis, lookups=True)
|
||||
@strawberry_django.filter_type(models.VirtualChassis, lookups=True)
|
||||
class VirtualChassisFilter(PrimaryModelFilterMixin):
|
||||
master: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
master_id: ID | None = strawberry_django.filter_field()
|
||||
@ -937,7 +937,7 @@ class VirtualChassisFilter(PrimaryModelFilterMixin):
|
||||
member_count: FilterLookup[int] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualDeviceContext, lookups=True)
|
||||
@strawberry_django.filter_type(models.VirtualDeviceContext, lookups=True)
|
||||
class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
device_id: ID | None = strawberry_django.filter_field()
|
||||
|
@ -85,7 +85,7 @@ class CachedScopeMixin(models.Model):
|
||||
abstract = True
|
||||
|
||||
def clean(self):
|
||||
if self.scope_type and not self.scope:
|
||||
if self.scope_type and not (self.scope or self.scope_id):
|
||||
scope_type = self.scope_type.model_class()
|
||||
raise ValidationError({
|
||||
'scope': _(
|
||||
|
@ -14,7 +14,7 @@ from ipam.models import ASN, RIR, VLAN, VRF
|
||||
from netbox.api.serializers import GenericObjectSerializer
|
||||
from tenancy.models import Tenant
|
||||
from users.models import User
|
||||
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
|
||||
from utilities.testing import APITestCase, APIViewTestCases, create_test_device, disable_logging
|
||||
from virtualization.models import Cluster, ClusterType
|
||||
from wireless.choices import WirelessChannelChoices
|
||||
from wireless.models import WirelessLAN
|
||||
@ -1858,7 +1858,8 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
||||
|
||||
# Attempt to delete only the parent interface
|
||||
url = self._get_detail_url(interface1)
|
||||
self.client.delete(url, **self.header)
|
||||
with disable_logging():
|
||||
self.client.delete(url, **self.header)
|
||||
self.assertEqual(device.interfaces.count(), 4) # Parent was not deleted
|
||||
|
||||
# Attempt to bulk delete parent & child together
|
||||
|
@ -12,6 +12,7 @@ from users.models import User
|
||||
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
|
||||
from virtualization.models import Cluster, ClusterType, ClusterGroup, VMInterface, VirtualMachine
|
||||
from wireless.choices import WirelessChannelChoices, WirelessRoleChoices
|
||||
from wireless.models import WirelessLink
|
||||
|
||||
|
||||
class DeviceComponentFilterSetTests:
|
||||
@ -4496,7 +4497,9 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
# Cables
|
||||
Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[5]]).save()
|
||||
Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[6]]).save()
|
||||
# Third pair is not connected
|
||||
|
||||
# Wireless links
|
||||
WirelessLink(interface_a=interfaces[7], interface_b=interfaces[8]).save()
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Interface 1', 'Interface 2']}
|
||||
@ -4684,15 +4687,15 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
|
||||
|
||||
def test_occupied(self):
|
||||
params = {'occupied': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
params = {'occupied': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_connected(self):
|
||||
params = {'connected': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||
params = {'connected': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
|
||||
|
||||
def test_kind(self):
|
||||
params = {'kind': 'physical'}
|
||||
|
@ -40,7 +40,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ConfigContext, lookups=True)
|
||||
@strawberry_django.filter_type(models.ConfigContext, lookups=True)
|
||||
class ConfigContextFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin):
|
||||
name: FilterLookup[str] = strawberry_django.filter_field()
|
||||
weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
@ -97,7 +97,7 @@ class ConfigContextFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, Chan
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ConfigTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.ConfigTemplate, lookups=True)
|
||||
class ConfigTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@ -111,7 +111,7 @@ class ConfigTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, Cha
|
||||
as_attachment: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CustomField, lookups=True)
|
||||
@strawberry_django.filter_type(models.CustomField, lookups=True)
|
||||
class CustomFieldFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
type: Annotated['CustomFieldTypeEnum', strawberry.lazy('extras.graphql.enums')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -164,7 +164,7 @@ class CustomFieldFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
comments: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CustomFieldChoiceSet, lookups=True)
|
||||
@strawberry_django.filter_type(models.CustomFieldChoiceSet, lookups=True)
|
||||
class CustomFieldChoiceSetFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@ -177,7 +177,7 @@ class CustomFieldChoiceSetFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin
|
||||
order_alphabetically: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.CustomLink, lookups=True)
|
||||
@strawberry_django.filter_type(models.CustomLink, lookups=True)
|
||||
class CustomLinkFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
@ -193,7 +193,7 @@ class CustomLinkFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
new_window: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ExportTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.ExportTemplate, lookups=True)
|
||||
class ExportTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@ -207,7 +207,7 @@ class ExportTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, Cha
|
||||
as_attachment: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ImageAttachment, lookups=True)
|
||||
@strawberry_django.filter_type(models.ImageAttachment, lookups=True)
|
||||
class ImageAttachmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -222,7 +222,7 @@ class ImageAttachmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.JournalEntry, lookups=True)
|
||||
@strawberry_django.filter_type(models.JournalEntry, lookups=True)
|
||||
class JournalEntryFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
|
||||
assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -238,7 +238,7 @@ class JournalEntryFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, Tag
|
||||
comments: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.NotificationGroup, lookups=True)
|
||||
@strawberry_django.filter_type(models.NotificationGroup, lookups=True)
|
||||
class NotificationGroupFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@ -246,7 +246,7 @@ class NotificationGroupFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
users: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.SavedFilter, lookups=True)
|
||||
@strawberry_django.filter_type(models.SavedFilter, lookups=True)
|
||||
class SavedFilterFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@ -263,7 +263,7 @@ class SavedFilterFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.TableConfig, lookups=True)
|
||||
@strawberry_django.filter_type(models.TableConfig, lookups=True)
|
||||
class TableConfigFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@ -276,13 +276,13 @@ class TableConfigFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
shared: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Tag, lookups=True)
|
||||
@strawberry_django.filter_type(models.Tag, lookups=True)
|
||||
class TagFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin, TagBaseFilterMixin):
|
||||
color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Webhook, lookups=True)
|
||||
@strawberry_django.filter_type(models.Webhook, lookups=True)
|
||||
class WebhookFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@ -301,7 +301,7 @@ class WebhookFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilt
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.EventRule, lookups=True)
|
||||
@strawberry_django.filter_type(models.EventRule, lookups=True)
|
||||
class EventRuleFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
@ -131,7 +131,7 @@ class RenderTemplateMixin(models.Model):
|
||||
"""
|
||||
context = self.get_context(context=context, queryset=queryset)
|
||||
env_params = self.environment_params or {}
|
||||
output = render_jinja2(self.template_code, context, env_params)
|
||||
output = render_jinja2(self.template_code, context, env_params, getattr(self, 'data_file', None))
|
||||
|
||||
# Replace CRLF-style line terminators
|
||||
output = output.replace('\r\n', '\n')
|
||||
|
@ -2,7 +2,7 @@ import datetime
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import make_aware
|
||||
from django.utils.timezone import make_aware, now
|
||||
from rest_framework import status
|
||||
|
||||
from core.choices import ManagedFileRootPathChoices
|
||||
@ -991,6 +991,10 @@ class SubscriptionTest(APIViewTestCases.APIViewTestCase):
|
||||
},
|
||||
]
|
||||
|
||||
cls.bulk_update_data = {
|
||||
'user': users[3].pk,
|
||||
}
|
||||
|
||||
|
||||
class NotificationGroupTest(APIViewTestCases.APIViewTestCase):
|
||||
model = NotificationGroup
|
||||
@ -1072,6 +1076,9 @@ class NotificationGroupTest(APIViewTestCases.APIViewTestCase):
|
||||
class NotificationTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Notification
|
||||
brief_fields = ['display', 'event_type', 'id', 'object_id', 'object_type', 'read', 'url', 'user']
|
||||
bulk_update_data = {
|
||||
'read': now(),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
@ -1,9 +1,12 @@
|
||||
from django.forms import ValidationError
|
||||
from django.test import TestCase
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from core.models import ObjectType
|
||||
from django.forms import ValidationError
|
||||
from django.test import tag, TestCase
|
||||
|
||||
from core.models import DataSource, ObjectType
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup
|
||||
from extras.models import ConfigContext, Tag
|
||||
from extras.models import ConfigContext, ConfigTemplate, Tag
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.exceptions import AbortRequest
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||
@ -33,8 +36,8 @@ class TagTest(TestCase):
|
||||
]
|
||||
|
||||
site = Site.objects.create(name='Site 1')
|
||||
for tag in tags:
|
||||
site.tags.add(tag)
|
||||
for _tag in tags:
|
||||
site.tags.add(_tag)
|
||||
site.save()
|
||||
|
||||
site = Site.objects.first()
|
||||
@ -540,3 +543,66 @@ class ConfigContextTest(TestCase):
|
||||
device.local_context_data = 'foo'
|
||||
with self.assertRaises(ValidationError):
|
||||
device.clean()
|
||||
|
||||
|
||||
class ConfigTemplateTest(TestCase):
|
||||
"""
|
||||
TODO: These test cases deal with the weighting, ordering, and deep merge logic of config context data.
|
||||
"""
|
||||
MAIN_TEMPLATE = """
|
||||
{%- include 'base.j2' %}
|
||||
""".strip()
|
||||
BASE_TEMPLATE = """
|
||||
Hi
|
||||
""".strip()
|
||||
|
||||
@classmethod
|
||||
def _create_template_file(cls, templates_dir, file_name, content):
|
||||
template_file_name = file_name
|
||||
if not template_file_name.endswith('j2'):
|
||||
template_file_name += '.j2'
|
||||
temp_file_path = templates_dir / template_file_name
|
||||
|
||||
with open(temp_file_path, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
temp_dir = tempfile.TemporaryDirectory()
|
||||
templates_dir = Path(temp_dir.name) / "templates"
|
||||
templates_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
cls._create_template_file(templates_dir, 'base.j2', cls.BASE_TEMPLATE)
|
||||
cls._create_template_file(templates_dir, 'main.j2', cls.MAIN_TEMPLATE)
|
||||
|
||||
data_source = DataSource(
|
||||
name="Test DataSource",
|
||||
type="local",
|
||||
source_url=str(templates_dir),
|
||||
)
|
||||
data_source.save()
|
||||
data_source.sync()
|
||||
|
||||
base_config_template = ConfigTemplate(
|
||||
name="BaseTemplate",
|
||||
data_file=data_source.datafiles.filter(path__endswith='base.j2').first()
|
||||
)
|
||||
base_config_template.clean()
|
||||
base_config_template.save()
|
||||
cls.base_config_template = base_config_template
|
||||
|
||||
main_config_template = ConfigTemplate(
|
||||
name="MainTemplate",
|
||||
data_file=data_source.datafiles.filter(path__endswith='main.j2').first()
|
||||
)
|
||||
main_config_template.clean()
|
||||
main_config_template.save()
|
||||
cls.main_config_template = main_config_template
|
||||
|
||||
@tag('regression')
|
||||
def test_config_template_with_data_source(self):
|
||||
self.assertEqual(self.BASE_TEMPLATE, self.base_config_template.render({}))
|
||||
|
||||
@tag('regression')
|
||||
def test_config_template_with_data_source_nested_templates(self):
|
||||
self.assertEqual(self.BASE_TEMPLATE, self.main_config_template.render({}))
|
||||
|
@ -1,3 +1,4 @@
|
||||
import logging
|
||||
import tempfile
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
@ -7,6 +8,7 @@ from netaddr import IPAddress, IPNetwork
|
||||
|
||||
from dcim.models import DeviceRole
|
||||
from extras.scripts import *
|
||||
from utilities.testing import disable_logging
|
||||
|
||||
CHOICES = (
|
||||
('ff0000', 'Red'),
|
||||
@ -39,7 +41,8 @@ class ScriptTest(TestCase):
|
||||
datafile.write(bytes(YAML_DATA, 'UTF-8'))
|
||||
datafile.seek(0)
|
||||
|
||||
data = Script().load_yaml(datafile.name)
|
||||
with disable_logging(level=logging.WARNING):
|
||||
data = Script().load_yaml(datafile.name)
|
||||
self.assertEqual(data, {
|
||||
'Foo': 123,
|
||||
'Bar': 456,
|
||||
@ -51,7 +54,8 @@ class ScriptTest(TestCase):
|
||||
datafile.write(bytes(JSON_DATA, 'UTF-8'))
|
||||
datafile.seek(0)
|
||||
|
||||
data = Script().load_json(datafile.name)
|
||||
with disable_logging(level=logging.WARNING):
|
||||
data = Script().load_json(datafile.name)
|
||||
self.assertEqual(data, {
|
||||
'Foo': 123,
|
||||
'Bar': 456,
|
||||
|
@ -969,7 +969,7 @@ class ObjectRenderConfigView(generic.ObjectView):
|
||||
|
||||
# Render the config template
|
||||
rendered_config = None
|
||||
error_message = None
|
||||
error_message = ''
|
||||
if config_template := instance.get_config_template():
|
||||
try:
|
||||
rendered_config = config_template.render(context=context_data)
|
||||
|
@ -46,7 +46,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ASN, lookups=True)
|
||||
@strawberry_django.filter_type(models.ASN, lookups=True)
|
||||
class ASNFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
rir_id: ID | None = strawberry_django.filter_field()
|
||||
@ -61,7 +61,7 @@ class ASNFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
) = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ASNRange, lookups=True)
|
||||
@strawberry_django.filter_type(models.ASNRange, lookups=True)
|
||||
class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@ -75,7 +75,7 @@ class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Aggregate, lookups=True)
|
||||
@strawberry_django.filter_type(models.Aggregate, lookups=True)
|
||||
class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
prefix: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
prefix_id: ID | None = strawberry_django.filter_field()
|
||||
@ -84,7 +84,7 @@ class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
|
||||
date_added: DateFilterLookup[date] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.FHRPGroup, lookups=True)
|
||||
@strawberry_django.filter_type(models.FHRPGroup, lookups=True)
|
||||
class FHRPGroupFilter(PrimaryModelFilterMixin):
|
||||
group_id: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -102,7 +102,7 @@ class FHRPGroupFilter(PrimaryModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.FHRPGroupAssignment, lookups=True)
|
||||
@strawberry_django.filter_type(models.FHRPGroupAssignment, lookups=True)
|
||||
class FHRPGroupAssignmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin):
|
||||
interface_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -117,7 +117,7 @@ class FHRPGroupAssignmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin)
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.IPAddress, lookups=True)
|
||||
@strawberry_django.filter_type(models.IPAddress, lookups=True)
|
||||
class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
@ -142,6 +142,10 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
|
||||
nat_outside_id: ID | None = strawberry_django.filter_field()
|
||||
dns_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
@strawberry_django.filter_field()
|
||||
def assigned(self, value: bool, prefix) -> Q:
|
||||
return Q(assigned_object_id__isnull=(not value))
|
||||
|
||||
@strawberry_django.filter_field()
|
||||
def parent(self, value: list[str], prefix) -> Q:
|
||||
if not value:
|
||||
@ -155,8 +159,16 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
|
||||
return Q()
|
||||
return q
|
||||
|
||||
@strawberry_django.filter_field()
|
||||
def family(
|
||||
self,
|
||||
value: Annotated['IPAddressFamilyEnum', strawberry.lazy('ipam.graphql.enums')],
|
||||
prefix,
|
||||
) -> Q:
|
||||
return Q(**{f"{prefix}address__family": value.value})
|
||||
|
||||
@strawberry_django.filter(models.IPRange, lookups=True)
|
||||
|
||||
@strawberry_django.filter_type(models.IPRange, lookups=True)
|
||||
class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
start_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
end_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@ -185,7 +197,7 @@ class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMi
|
||||
return q
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Prefix, lookups=True)
|
||||
@strawberry_django.filter_type(models.Prefix, lookups=True)
|
||||
class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
prefix: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
@ -201,19 +213,19 @@ class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, Pr
|
||||
mark_utilized: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.RIR, lookups=True)
|
||||
@strawberry_django.filter_type(models.RIR, lookups=True)
|
||||
class RIRFilter(OrganizationalModelFilterMixin):
|
||||
is_private: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Role, lookups=True)
|
||||
@strawberry_django.filter_type(models.Role, lookups=True)
|
||||
class RoleFilter(OrganizationalModelFilterMixin):
|
||||
weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.RouteTarget, lookups=True)
|
||||
@strawberry_django.filter_type(models.RouteTarget, lookups=True)
|
||||
class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
importing_vrfs: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
@ -230,7 +242,7 @@ class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Service, lookups=True)
|
||||
@strawberry_django.filter_type(models.Service, lookups=True)
|
||||
class ServiceFilter(ContactFilterMixin, ServiceBaseFilterMixin, PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
@ -242,12 +254,12 @@ class ServiceFilter(ContactFilterMixin, ServiceBaseFilterMixin, PrimaryModelFilt
|
||||
parent_object_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ServiceTemplate, lookups=True)
|
||||
@strawberry_django.filter_type(models.ServiceTemplate, lookups=True)
|
||||
class ServiceTemplateFilter(ServiceBaseFilterMixin, PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VLAN, lookups=True)
|
||||
@strawberry_django.filter_type(models.VLAN, lookups=True)
|
||||
class VLANFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
site_id: ID | None = strawberry_django.filter_field()
|
||||
@ -277,19 +289,19 @@ class VLANFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VLANGroup, lookups=True)
|
||||
@strawberry_django.filter_type(models.VLANGroup, lookups=True)
|
||||
class VLANGroupFilter(ScopedFilterMixin, OrganizationalModelFilterMixin):
|
||||
vid_ranges: Annotated['IntegerArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VLANTranslationPolicy, lookups=True)
|
||||
@strawberry_django.filter_type(models.VLANTranslationPolicy, lookups=True)
|
||||
class VLANTranslationPolicyFilter(PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VLANTranslationRule, lookups=True)
|
||||
@strawberry_django.filter_type(models.VLANTranslationRule, lookups=True)
|
||||
class VLANTranslationRuleFilter(NetBoxModelFilterMixin):
|
||||
policy: Annotated['VLANTranslationPolicyFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -304,7 +316,7 @@ class VLANTranslationRuleFilter(NetBoxModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VRF, lookups=True)
|
||||
@strawberry_django.filter_type(models.VRF, lookups=True)
|
||||
class VRFFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
rd: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
@ -148,7 +148,7 @@ class VLANQuerySet(RestrictedQuerySet):
|
||||
|
||||
# Find all relevant VLANGroups
|
||||
q = Q()
|
||||
site = vm.site or vm.cluster._site
|
||||
site = vm.site
|
||||
if vm.cluster:
|
||||
# Add VLANGroups scoped to the assigned cluster (or its group)
|
||||
q |= Q(
|
||||
@ -160,6 +160,30 @@ class VLANQuerySet(RestrictedQuerySet):
|
||||
scope_type=ContentType.objects.get_by_natural_key('virtualization', 'clustergroup'),
|
||||
scope_id=vm.cluster.group_id
|
||||
)
|
||||
# Looking all possible cluster scopes
|
||||
if vm.cluster.scope_type == ContentType.objects.get_by_natural_key('dcim', 'location'):
|
||||
site = site or vm.cluster.scope.site
|
||||
q |= Q(
|
||||
scope_type=ContentType.objects.get_by_natural_key('dcim', 'location'),
|
||||
scope_id__in=vm.cluster.scope.get_ancestors(include_self=True)
|
||||
)
|
||||
elif vm.cluster.scope_type == ContentType.objects.get_by_natural_key('dcim', 'site'):
|
||||
site = site or vm.cluster.scope
|
||||
q |= Q(
|
||||
scope_type=ContentType.objects.get_by_natural_key('dcim', 'site'),
|
||||
scope_id=vm.cluster.scope.pk
|
||||
)
|
||||
elif vm.cluster.scope_type == ContentType.objects.get_by_natural_key('dcim', 'sitegroup'):
|
||||
q |= Q(
|
||||
scope_type=ContentType.objects.get_by_natural_key('dcim', 'sitegroup'),
|
||||
scope_id__in=vm.cluster.scope.get_ancestors(include_self=True)
|
||||
)
|
||||
elif vm.cluster.scope_type == ContentType.objects.get_by_natural_key('dcim', 'region'):
|
||||
q |= Q(
|
||||
scope_type=ContentType.objects.get_by_natural_key('dcim', 'region'),
|
||||
scope_id__in=vm.cluster.scope.get_ancestors(include_self=True)
|
||||
)
|
||||
# VM can be assigned to a site without a cluster so checking assigned site independently
|
||||
if site:
|
||||
# Add VLANGroups scoped to the assigned site (or its group or region)
|
||||
q |= Q(
|
||||
|
@ -1,5 +1,7 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.test import tag
|
||||
from django.urls import reverse
|
||||
from netaddr import IPNetwork
|
||||
from rest_framework import status
|
||||
@ -9,7 +11,7 @@ from ipam.choices import *
|
||||
from ipam.models import *
|
||||
from tenancy.models import Tenant
|
||||
from utilities.data import string_to_ranges
|
||||
from utilities.testing import APITestCase, APIViewTestCases, create_test_device, disable_warnings
|
||||
from utilities.testing import APITestCase, APIViewTestCases, create_test_device, disable_logging
|
||||
|
||||
|
||||
class AppTest(APITestCase):
|
||||
@ -382,6 +384,18 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
||||
)
|
||||
Prefix.objects.bulk_create(prefixes)
|
||||
|
||||
@tag('regression')
|
||||
def test_clean_validates_scope(self):
|
||||
prefix = Prefix.objects.first()
|
||||
site = Site.objects.create(name='Test Site', slug='test-site')
|
||||
|
||||
data = {'scope_type': 'dcim.site', 'scope_id': site.id}
|
||||
url = reverse('ipam-api:prefix-detail', kwargs={'pk': prefix.pk})
|
||||
self.add_permissions('ipam.change_prefix')
|
||||
|
||||
response = self.client.patch(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
|
||||
def test_list_available_prefixes(self):
|
||||
"""
|
||||
Test retrieval of all available prefixes within a parent prefix.
|
||||
@ -1026,7 +1040,7 @@ class VLANTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
self.add_permissions('ipam.delete_vlan')
|
||||
url = reverse('ipam-api:vlan-detail', kwargs={'pk': vlan.pk})
|
||||
with disable_warnings('netbox.api.views.ModelViewSet'):
|
||||
with disable_logging(level=logging.WARNING):
|
||||
response = self.client.delete(url, **self.header)
|
||||
|
||||
self.assertHttpStatus(response, status.HTTP_409_CONFLICT)
|
||||
|
@ -1849,6 +1849,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
Cluster(name='Cluster 1', type=cluster_type, group=cluster_groups[0], scope=sites[0]),
|
||||
Cluster(name='Cluster 2', type=cluster_type, group=cluster_groups[1], scope=sites[1]),
|
||||
Cluster(name='Cluster 3', type=cluster_type, group=cluster_groups[2], scope=sites[2]),
|
||||
Cluster(name='Cluster 4', type=cluster_type, group=cluster_groups[0], scope=locations[0]),
|
||||
)
|
||||
for cluster in clusters:
|
||||
cluster.save()
|
||||
@ -1857,6 +1858,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
VirtualMachine(name='Virtual Machine 1', cluster=clusters[0]),
|
||||
VirtualMachine(name='Virtual Machine 2', cluster=clusters[1]),
|
||||
VirtualMachine(name='Virtual Machine 3', cluster=clusters[2]),
|
||||
VirtualMachine(name='Virtual Machine 4', cluster=clusters[3]),
|
||||
)
|
||||
VirtualMachine.objects.bulk_create(virtual_machines)
|
||||
|
||||
@ -1864,6 +1866,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
VMInterface(virtual_machine=virtual_machines[0], name='VM Interface 1'),
|
||||
VMInterface(virtual_machine=virtual_machines[1], name='VM Interface 2'),
|
||||
VMInterface(virtual_machine=virtual_machines[2], name='VM Interface 3'),
|
||||
VMInterface(virtual_machine=virtual_machines[3], name='VM Interface 4'),
|
||||
)
|
||||
VMInterface.objects.bulk_create(vm_interfaces)
|
||||
|
||||
@ -1890,6 +1893,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
VLANGroup(name='Cluster 1', slug='cluster-1', scope=clusters[0]),
|
||||
VLANGroup(name='Cluster 2', slug='cluster-2', scope=clusters[1]),
|
||||
VLANGroup(name='Cluster 3', slug='cluster-3', scope=clusters[2]),
|
||||
VLANGroup(name='Cluster 4', slug='cluster-4', scope=clusters[3]),
|
||||
|
||||
# General purpose VLAN groups
|
||||
VLANGroup(name='VLAN Group 1', slug='vlan-group-1'),
|
||||
@ -1944,11 +1948,12 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
VLAN(vid=19, name='Cluster 1', group=groups[18]),
|
||||
VLAN(vid=20, name='Cluster 2', group=groups[19]),
|
||||
VLAN(vid=21, name='Cluster 3', group=groups[20]),
|
||||
VLAN(vid=22, name='Cluster 4', group=groups[21]),
|
||||
VLAN(
|
||||
vid=101,
|
||||
name='VLAN 101',
|
||||
site=sites[3],
|
||||
group=groups[21],
|
||||
group=groups[22],
|
||||
role=roles[0],
|
||||
tenant=tenants[0],
|
||||
status=VLANStatusChoices.STATUS_ACTIVE,
|
||||
@ -1957,7 +1962,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
vid=102,
|
||||
name='VLAN 102',
|
||||
site=sites[3],
|
||||
group=groups[21],
|
||||
group=groups[22],
|
||||
role=roles[0],
|
||||
tenant=tenants[0],
|
||||
status=VLANStatusChoices.STATUS_ACTIVE,
|
||||
@ -1966,7 +1971,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
vid=201,
|
||||
name='VLAN 201',
|
||||
site=sites[4],
|
||||
group=groups[22],
|
||||
group=groups[23],
|
||||
role=roles[1],
|
||||
tenant=tenants[1],
|
||||
status=VLANStatusChoices.STATUS_DEPRECATED,
|
||||
@ -1975,7 +1980,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
vid=202,
|
||||
name='VLAN 202',
|
||||
site=sites[4],
|
||||
group=groups[22],
|
||||
group=groups[23],
|
||||
role=roles[1],
|
||||
tenant=tenants[1],
|
||||
status=VLANStatusChoices.STATUS_DEPRECATED,
|
||||
@ -1984,7 +1989,7 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
vid=301,
|
||||
name='VLAN 301',
|
||||
site=sites[5],
|
||||
group=groups[23],
|
||||
group=groups[24],
|
||||
role=roles[2],
|
||||
tenant=tenants[2],
|
||||
status=VLANStatusChoices.STATUS_RESERVED,
|
||||
@ -1993,13 +1998,13 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
vid=302,
|
||||
name='VLAN 302',
|
||||
site=sites[5],
|
||||
group=groups[23],
|
||||
group=groups[24],
|
||||
role=roles[2],
|
||||
tenant=tenants[2],
|
||||
status=VLANStatusChoices.STATUS_RESERVED,
|
||||
),
|
||||
# Create one globally available VLAN on a VLAN group
|
||||
VLAN(vid=500, name='VLAN Group 1', group=groups[24]),
|
||||
VLAN(vid=500, name='VLAN Group 1', group=groups[25]),
|
||||
# Create one globally available VLAN
|
||||
VLAN(vid=1000, name='Global VLAN'),
|
||||
# Create some Q-in-Q service VLANs
|
||||
@ -2130,6 +2135,9 @@ class VLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
vm_id = VirtualMachine.objects.first().pk
|
||||
params = {'available_on_virtualmachine': vm_id}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7) # 5 scoped + 1 global group + 1 global
|
||||
vm_id = VirtualMachine.objects.get(name='Virtual Machine 4').pk
|
||||
params = {'available_on_virtualmachine': vm_id}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8) # 6 scoped + 1 global group + 1 global
|
||||
|
||||
def test_available_at_site(self):
|
||||
site_id = Site.objects.first().pk
|
||||
|
@ -1,6 +1,7 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.choices import *
|
||||
from utilities.conversion import to_grams, to_meters
|
||||
|
||||
@ -58,7 +59,7 @@ class DistanceMixin(models.Model):
|
||||
max_digits=8,
|
||||
decimal_places=2,
|
||||
blank=True,
|
||||
null=True
|
||||
null=True,
|
||||
)
|
||||
distance_unit = models.CharField(
|
||||
verbose_name=_('distance unit'),
|
||||
@ -69,7 +70,7 @@ class DistanceMixin(models.Model):
|
||||
)
|
||||
# Stores the normalized distance (in meters) for database ordering
|
||||
_abs_distance = models.DecimalField(
|
||||
max_digits=10,
|
||||
max_digits=13,
|
||||
decimal_places=4,
|
||||
blank=True,
|
||||
null=True
|
||||
|
@ -1,12 +1,10 @@
|
||||
{% extends 'account/base.html' %}
|
||||
{% load helpers %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "User Profile" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Account Details" %}</h2>
|
||||
@ -64,12 +62,7 @@
|
||||
{% if perms.core.view_objectchange %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header text-center">{% trans "Recent Activity" %}</h2>
|
||||
<div class="table-responsive">
|
||||
{% render_table changelog_table 'inc/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'users/inc/user_activity.html' with user=user table=changelog_table %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -28,6 +28,10 @@
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
|
@ -53,7 +53,6 @@
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Connection" %}</h2>
|
||||
<div class="card-body">
|
||||
{% if object.mark_connected %}
|
||||
<div class="card-body">
|
||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
|
||||
|
@ -26,6 +26,12 @@
|
||||
<th scope="row">{% trans "Location" %}</th>
|
||||
<td>{% nested_tree object.location %}</td>
|
||||
</tr>
|
||||
{% if object.virtual_chassis %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Virtual Chassis" %}</th>
|
||||
<td>{{ object.virtual_chassis|linkify }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Rack" %}</th>
|
||||
<td class="d-flex justify-content-between align-items-start">
|
||||
|
@ -63,11 +63,15 @@
|
||||
</h2>
|
||||
<pre class="card-body" id="rendered_config">{{ rendered_config }}</pre>
|
||||
</div>
|
||||
{% else %}
|
||||
{% elif error_message %}
|
||||
<div class="alert alert-warning">
|
||||
<h4 class="alert-title mb-1">{% trans "Error rendering template" %}</h4>
|
||||
{% trans error_message %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
<h4 class="alert-title mb-1">{% trans "Template output is empty" %}</h4>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
|
16
netbox/templates/users/inc/user_activity.html
Normal file
16
netbox/templates/users/inc/user_activity.html
Normal file
@ -0,0 +1,16 @@
|
||||
{% load i18n %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-header text-center">
|
||||
{% trans "Recent Activity" %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'core:objectchange_list' %}?user_id={{ user.pk }}" class="btn btn-ghost-primary btn-sm">
|
||||
<i class="mdi mdi-arrow-right-thick" aria-hidden="true"></i> {% trans "View All" %}
|
||||
</a>
|
||||
</div>
|
||||
</h2>
|
||||
<div class="table-responsive">
|
||||
{% render_table table 'inc/table.html' %}
|
||||
</div>
|
||||
</div>
|
@ -1,14 +1,12 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load i18n %}
|
||||
{% load helpers %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block title %}{% trans "User" %} {{ object.username }}{% endblock %}
|
||||
|
||||
{% block subtitle %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "User" %}</h2>
|
||||
@ -74,12 +72,7 @@
|
||||
{% if perms.core.view_objectchange %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="text-center">{% trans "Recent Activity" %}</h2>
|
||||
<div class="card-body table-responsive">
|
||||
{% render_table changelog_table 'inc/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'users/inc/user_activity.html' with user=object table=changelog_table %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -56,7 +56,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Tenant, lookups=True)
|
||||
@strawberry_django.filter_type(models.Tenant, lookups=True)
|
||||
class TenantFilter(PrimaryModelFilterMixin, ContactFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@ -135,7 +135,7 @@ class TenantFilter(PrimaryModelFilterMixin, ContactFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.TenantGroup, lookups=True)
|
||||
@strawberry_django.filter_type(models.TenantGroup, lookups=True)
|
||||
class TenantGroupFilter(OrganizationalModelFilterMixin):
|
||||
parent: Annotated['TenantGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -149,7 +149,7 @@ class TenantGroupFilter(OrganizationalModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Contact, lookups=True)
|
||||
@strawberry_django.filter_type(models.Contact, lookups=True)
|
||||
class ContactFilter(PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
title: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@ -165,19 +165,19 @@ class ContactFilter(PrimaryModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ContactRole, lookups=True)
|
||||
@strawberry_django.filter_type(models.ContactRole, lookups=True)
|
||||
class ContactRoleFilter(OrganizationalModelFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ContactGroup, lookups=True)
|
||||
@strawberry_django.filter_type(models.ContactGroup, lookups=True)
|
||||
class ContactGroupFilter(NestedGroupModelFilterMixin):
|
||||
parent: Annotated['ContactGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ContactAssignment, lookups=True)
|
||||
@strawberry_django.filter_type(models.ContactAssignment, lookups=True)
|
||||
class ContactAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin):
|
||||
object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -14,13 +14,13 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Group, lookups=True)
|
||||
@strawberry_django.filter_type(models.Group, lookups=True)
|
||||
class GroupFilter(BaseObjectTypeFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.User, lookups=True)
|
||||
@strawberry_django.filter_type(models.User, lookups=True)
|
||||
class UserFilter(BaseObjectTypeFilterMixin):
|
||||
username: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
first_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
@ -75,8 +75,9 @@ class UserView(generic.ObjectView):
|
||||
template_name = 'users/user.html'
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
changelog = ObjectChange.objects.restrict(request.user, 'view').filter(user=instance)[:20]
|
||||
changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(user=instance)[:20]
|
||||
changelog_table = ObjectChangeTable(changelog)
|
||||
changelog_table.orderable = False
|
||||
changelog_table.configure(request)
|
||||
|
||||
return {
|
||||
|
@ -1,10 +1,13 @@
|
||||
import time
|
||||
from decimal import Decimal
|
||||
|
||||
from django import forms
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
__all__ = (
|
||||
'CheckLastUpdatedMixin',
|
||||
'DistanceValidationMixin',
|
||||
)
|
||||
|
||||
|
||||
@ -44,3 +47,13 @@ class CheckLastUpdatedMixin(forms.Form):
|
||||
"This object has been modified since the form was rendered. Please consult the object's change "
|
||||
"log for details."
|
||||
))
|
||||
|
||||
|
||||
class DistanceValidationMixin(forms.Form):
|
||||
distance = forms.DecimalField(
|
||||
required=False,
|
||||
validators=[
|
||||
MinValueValidator(Decimal(0)),
|
||||
MaxValueValidator(Decimal(100000)),
|
||||
]
|
||||
)
|
||||
|
@ -49,11 +49,27 @@ class DataFileLoader(BaseLoader):
|
||||
# Utility functions
|
||||
#
|
||||
|
||||
def render_jinja2(template_code, context, environment_params=None):
|
||||
def render_jinja2(template_code, context, environment_params=None, data_file=None):
|
||||
"""
|
||||
Render a Jinja2 template with the provided context. Return the rendered content.
|
||||
"""
|
||||
environment_params = environment_params or {}
|
||||
|
||||
if 'loader' not in environment_params:
|
||||
if data_file:
|
||||
loader = DataFileLoader(data_file.source)
|
||||
loader.cache_templates({
|
||||
data_file.path: template_code
|
||||
})
|
||||
else:
|
||||
loader = BaseLoader()
|
||||
environment_params['loader'] = loader
|
||||
|
||||
environment = SandboxedEnvironment(**environment_params)
|
||||
environment.filters.update(get_config().JINJA2_FILTERS)
|
||||
return environment.from_string(source=template_code).render(**context)
|
||||
|
||||
if data_file:
|
||||
template = environment.get_template(data_file.path)
|
||||
else:
|
||||
template = environment.from_string(source=template_code)
|
||||
return template.render(**context)
|
||||
|
@ -31,6 +31,11 @@
|
||||
<div class="col mb-1">
|
||||
{{ field }}
|
||||
<div class="form-text">{% trans field.label %}</div>
|
||||
{% if field.errors %}
|
||||
<div class="form-text text-danger">
|
||||
{% for error in field.errors %}{{ error }}{% if not forloop.last %}<br />{% endif %}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
from django.test import Client, TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
from drf_spectacular.drainage import GENERATOR_STATS
|
||||
from rest_framework import status
|
||||
|
||||
from core.models import ObjectType
|
||||
@ -264,5 +265,6 @@ class APIDocsTestCase(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
url = reverse('schema')
|
||||
response = self.client.get(url)
|
||||
with GENERATOR_STATS.silence(): # Suppress schema generator warnings
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
@ -39,7 +39,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Cluster, lookups=True)
|
||||
@strawberry_django.filter_type(models.Cluster, lookups=True)
|
||||
class ClusterFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
type: Annotated['ClusterTypeFilter', strawberry.lazy('virtualization.graphql.filters')] | None = (
|
||||
@ -58,19 +58,19 @@ class ClusterFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, P
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ClusterGroup, lookups=True)
|
||||
@strawberry_django.filter_type(models.ClusterGroup, lookups=True)
|
||||
class ClusterGroupFilter(ContactFilterMixin, OrganizationalModelFilterMixin):
|
||||
vlan_groups: Annotated['VLANGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.ClusterType, lookups=True)
|
||||
@strawberry_django.filter_type(models.ClusterType, lookups=True)
|
||||
class ClusterTypeFilter(OrganizationalModelFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualMachine, lookups=True)
|
||||
@strawberry_django.filter_type(models.VirtualMachine, lookups=True)
|
||||
class VirtualMachineFilter(
|
||||
ContactFilterMixin,
|
||||
ImageAttachmentFilterMixin,
|
||||
@ -130,7 +130,7 @@ class VirtualMachineFilter(
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VMInterface, lookups=True)
|
||||
@strawberry_django.filter_type(models.VMInterface, lookups=True)
|
||||
class VMInterfaceFilter(VMComponentFilterMixin, InterfaceBaseFilterMixin):
|
||||
ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@ -155,7 +155,7 @@ class VMInterfaceFilter(VMComponentFilterMixin, InterfaceBaseFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.VirtualDisk, lookups=True)
|
||||
@strawberry_django.filter_type(models.VirtualDisk, lookups=True)
|
||||
class VirtualDiskFilter(VMComponentFilterMixin):
|
||||
size: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
|
@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
from django.test import tag
|
||||
from django.urls import reverse
|
||||
from netaddr import IPNetwork
|
||||
@ -10,7 +12,9 @@ from extras.choices import CustomFieldTypeChoices
|
||||
from extras.models import ConfigTemplate, CustomField
|
||||
from ipam.choices import VLANQinQRoleChoices
|
||||
from ipam.models import Prefix, VLAN, VRF
|
||||
from utilities.testing import APITestCase, APIViewTestCases, create_test_device, create_test_virtualmachine
|
||||
from utilities.testing import (
|
||||
APITestCase, APIViewTestCases, create_test_device, create_test_virtualmachine, disable_logging,
|
||||
)
|
||||
from virtualization.choices import *
|
||||
from virtualization.models import *
|
||||
|
||||
@ -402,7 +406,8 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
# Attempt to delete only the parent interface
|
||||
url = self._get_detail_url(interface1)
|
||||
self.client.delete(url, **self.header)
|
||||
with disable_logging(level=logging.WARNING):
|
||||
self.client.delete(url, **self.header)
|
||||
self.assertEqual(virtual_machine.interfaces.count(), 4) # Parent was not deleted
|
||||
|
||||
# Attempt to bulk delete parent & child together
|
||||
|
@ -31,12 +31,12 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.TunnelGroup, lookups=True)
|
||||
@strawberry_django.filter_type(models.TunnelGroup, lookups=True)
|
||||
class TunnelGroupFilter(OrganizationalModelFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.TunnelTermination, lookups=True)
|
||||
@strawberry_django.filter_type(models.TunnelTermination, lookups=True)
|
||||
class TunnelTerminationFilter(
|
||||
BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin
|
||||
):
|
||||
@ -56,7 +56,7 @@ class TunnelTerminationFilter(
|
||||
outside_ip_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.Tunnel, lookups=True)
|
||||
@strawberry_django.filter_type(models.Tunnel, lookups=True)
|
||||
class TunnelFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
status: Annotated['TunnelStatusEnum', strawberry.lazy('vpn.graphql.enums')] | None = (
|
||||
@ -80,7 +80,7 @@ class TunnelFilter(TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.IKEProposal, lookups=True)
|
||||
@strawberry_django.filter_type(models.IKEProposal, lookups=True)
|
||||
class IKEProposalFilter(PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
authentication_method: Annotated['AuthenticationMethodEnum', strawberry.lazy('vpn.graphql.enums')] | None = (
|
||||
@ -101,7 +101,7 @@ class IKEProposalFilter(PrimaryModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.IKEPolicy, lookups=True)
|
||||
@strawberry_django.filter_type(models.IKEPolicy, lookups=True)
|
||||
class IKEPolicyFilter(PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
version: Annotated['IKEVersionEnum', strawberry.lazy('vpn.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
@ -112,7 +112,7 @@ class IKEPolicyFilter(PrimaryModelFilterMixin):
|
||||
preshared_key: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.IPSecProposal, lookups=True)
|
||||
@strawberry_django.filter_type(models.IPSecProposal, lookups=True)
|
||||
class IPSecProposalFilter(PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
encryption_algorithm: Annotated['EncryptionAlgorithmEnum', strawberry.lazy('vpn.graphql.enums')] | None = (
|
||||
@ -132,7 +132,7 @@ class IPSecProposalFilter(PrimaryModelFilterMixin):
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.IPSecPolicy, lookups=True)
|
||||
@strawberry_django.filter_type(models.IPSecPolicy, lookups=True)
|
||||
class IPSecPolicyFilter(PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
proposals: Annotated['IPSecProposalFilter', strawberry.lazy('vpn.graphql.filters')] | None = (
|
||||
@ -141,7 +141,7 @@ class IPSecPolicyFilter(PrimaryModelFilterMixin):
|
||||
pfs_group: Annotated['DHGroupEnum', strawberry.lazy('vpn.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.IPSecProfile, lookups=True)
|
||||
@strawberry_django.filter_type(models.IPSecProfile, lookups=True)
|
||||
class IPSecProfileFilter(PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
mode: Annotated['IPSecModeEnum', strawberry.lazy('vpn.graphql.enums')] | None = strawberry_django.filter_field()
|
||||
@ -155,7 +155,7 @@ class IPSecProfileFilter(PrimaryModelFilterMixin):
|
||||
ipsec_policy_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.L2VPN, lookups=True)
|
||||
@strawberry_django.filter_type(models.L2VPN, lookups=True)
|
||||
class L2VPNFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin):
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
@ -174,7 +174,7 @@ class L2VPNFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixi
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.L2VPNTermination, lookups=True)
|
||||
@strawberry_django.filter_type(models.L2VPNTermination, lookups=True)
|
||||
class L2VPNTerminationFilter(NetBoxModelFilterMixin):
|
||||
l2vpn: Annotated['L2VPNFilter', strawberry.lazy('vpn.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
l2vpn_id: ID | None = strawberry_django.filter_field()
|
||||
|
@ -73,7 +73,7 @@ class TunnelTable(TenancyColumnsMixin, NetBoxTable):
|
||||
default_columns = ('pk', 'name', 'group', 'status', 'encapsulation', 'tenant', 'terminations_count')
|
||||
|
||||
|
||||
class TunnelTerminationTable(TenancyColumnsMixin, NetBoxTable):
|
||||
class TunnelTerminationTable(NetBoxTable):
|
||||
tunnel = tables.Column(
|
||||
verbose_name=_('Tunnel'),
|
||||
linkify=True
|
||||
@ -89,7 +89,8 @@ class TunnelTerminationTable(TenancyColumnsMixin, NetBoxTable):
|
||||
)
|
||||
termination = tables.Column(
|
||||
verbose_name=_('Tunnel interface'),
|
||||
linkify=True
|
||||
linkify=True,
|
||||
orderable=False,
|
||||
)
|
||||
ip_addresses = columns.ManyToManyColumn(
|
||||
accessor=tables.A('termination__ip_addresses'),
|
||||
|
23
netbox/vpn/tests/test_tables.py
Normal file
23
netbox/vpn/tests/test_tables.py
Normal file
@ -0,0 +1,23 @@
|
||||
from django.test import RequestFactory, tag, TestCase
|
||||
|
||||
from vpn.models import TunnelTermination
|
||||
from vpn.tables import TunnelTerminationTable
|
||||
|
||||
|
||||
@tag('regression')
|
||||
class TunnelTerminationTableTest(TestCase):
|
||||
def test_every_orderable_field_does_not_throw_exception(self):
|
||||
terminations = TunnelTermination.objects.all()
|
||||
fake_request = RequestFactory().get("/")
|
||||
disallowed = {'actions'}
|
||||
|
||||
orderable_columns = [
|
||||
column.name for column in TunnelTerminationTable(terminations).columns
|
||||
if column.orderable and column.name not in disallowed
|
||||
]
|
||||
|
||||
for col in orderable_columns:
|
||||
for dir in ('-', ''):
|
||||
table = TunnelTerminationTable(terminations)
|
||||
table.order_by = f'{dir}{col}'
|
||||
table.as_html(fake_request)
|
@ -7,6 +7,7 @@ from ipam.models import VLAN
|
||||
from netbox.forms import NetBoxModelForm
|
||||
from tenancy.forms import TenancyForm
|
||||
from utilities.forms.fields import CommentField, DynamicModelChoiceField, SlugField
|
||||
from utilities.forms.mixins import DistanceValidationMixin
|
||||
from utilities.forms.rendering import FieldSet, InlineFields
|
||||
from wireless.models import *
|
||||
|
||||
@ -73,7 +74,7 @@ class WirelessLANForm(ScopedForm, TenancyForm, NetBoxModelForm):
|
||||
}
|
||||
|
||||
|
||||
class WirelessLinkForm(TenancyForm, NetBoxModelForm):
|
||||
class WirelessLinkForm(DistanceValidationMixin, TenancyForm, NetBoxModelForm):
|
||||
site_a = DynamicModelChoiceField(
|
||||
queryset=Site.objects.all(),
|
||||
required=False,
|
||||
|
@ -23,12 +23,12 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter(models.WirelessLANGroup, lookups=True)
|
||||
@strawberry_django.filter_type(models.WirelessLANGroup, lookups=True)
|
||||
class WirelessLANGroupFilter(NestedGroupModelFilterMixin):
|
||||
pass
|
||||
|
||||
|
||||
@strawberry_django.filter(models.WirelessLAN, lookups=True)
|
||||
@strawberry_django.filter_type(models.WirelessLAN, lookups=True)
|
||||
class WirelessLANFilter(
|
||||
WirelessAuthenticationBaseFilterMixin,
|
||||
ScopedFilterMixin,
|
||||
@ -47,7 +47,7 @@ class WirelessLANFilter(
|
||||
vlan_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter(models.WirelessLink, lookups=True)
|
||||
@strawberry_django.filter_type(models.WirelessLink, lookups=True)
|
||||
class WirelessLinkFilter(
|
||||
WirelessAuthenticationBaseFilterMixin,
|
||||
DistanceFilterMixin,
|
||||
|
@ -0,0 +1,16 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wireless', '0014_wirelesslangroup_comments'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='wirelesslink',
|
||||
name='_abs_distance',
|
||||
field=models.DecimalField(blank=True, decimal_places=4, max_digits=13, null=True),
|
||||
),
|
||||
]
|
@ -1,6 +1,40 @@
|
||||
# See PEP 518 for the spec of this file
|
||||
# https://www.python.org/dev/peps/pep-0518/
|
||||
|
||||
[project]
|
||||
name = "netbox"
|
||||
version = "4.3.1"
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
{ name = "NetBox Community" }
|
||||
]
|
||||
maintainers = [
|
||||
{ name = "NetBox Community" }
|
||||
]
|
||||
description = "The premier source of truth powering network automation."
|
||||
readme = "README.md"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE.txt"]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Framework :: Django",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: System Administrators",
|
||||
"Natural Language :: English",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
]
|
||||
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://netboxlabs.com/products/netbox/"
|
||||
Documentation = "https://netboxlabs.com/docs/netbox/"
|
||||
Source = "https://github.com/netbox-community/netbox"
|
||||
Issues = "https://github.com/netbox-community/netbox/issues"
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
target_version = ['py310', 'py311', 'py312']
|
||||
|
Loading…
Reference in New Issue
Block a user