mirror of
https://github.com/netbox-community/netbox.git
synced 2026-03-05 14:00:04 +01:00
Compare commits
9 Commits
21357-regi
...
21566-user
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dd246acb7 | ||
|
|
5c4b1550e2 | ||
|
|
b0100a062c | ||
|
|
4c9dd25c20 | ||
|
|
a0642aed94 | ||
|
|
68a6e2569e | ||
|
|
810bb56776 | ||
|
|
5ae503428a | ||
|
|
55c2e59241 |
@@ -15,7 +15,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.5.4
|
||||
placeholder: v4.5.3
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
@@ -27,7 +27,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.5.4
|
||||
placeholder: v4.5.3
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/03-performance.yaml
vendored
2
.github/ISSUE_TEMPLATE/03-performance.yaml
vendored
@@ -8,7 +8,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox Version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v4.5.4
|
||||
placeholder: v4.5.3
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
||||
44
.github/workflows/claude-code-review.yml
vendored
44
.github/workflows/claude-code-review.yml
vendored
@@ -1,44 +0,0 @@
|
||||
name: Claude Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, ready_for_review, reopened]
|
||||
# Optional: Only run on specific file changes
|
||||
# paths:
|
||||
# - "src/**/*.ts"
|
||||
# - "src/**/*.tsx"
|
||||
# - "src/**/*.js"
|
||||
# - "src/**/*.jsx"
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
# Optional: Filter by PR author
|
||||
# if: |
|
||||
# github.event.pull_request.user.login == 'external-contributor' ||
|
||||
# github.event.pull_request.user.login == 'new-developer' ||
|
||||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code Review
|
||||
id: claude-review
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
|
||||
plugins: 'code-review@claude-code-plugins'
|
||||
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
|
||||
50
.github/workflows/claude.yml
vendored
50
.github/workflows/claude.yml
vendored
@@ -1,50 +0,0 @@
|
||||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
id-token: write
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
||||
# prompt: 'Update the pull request description to include a summary of changes.'
|
||||
|
||||
# Optional: Add claude_args to customize behavior and configuration
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
||||
|
||||
84
CLAUDE.md
84
CLAUDE.md
@@ -1,84 +0,0 @@
|
||||
# NetBox
|
||||
|
||||
Network source-of-truth and infrastructure resource modeling (IRM) tool combining DCIM and IPAM. Built on Django + PostgreSQL + Redis.
|
||||
|
||||
## Tech Stack
|
||||
- Python 3.12+ / Django / Django REST Framework
|
||||
- PostgreSQL (required), Redis (required for caching/queuing)
|
||||
- GraphQL via Strawberry, background jobs via RQ
|
||||
- Docs: MkDocs (in `docs/`)
|
||||
|
||||
## Repository Layout
|
||||
- `netbox/` — Django project root; run all `manage.py` commands from here
|
||||
- `netbox/netbox/` — Core settings, URLs, WSGI entrypoint
|
||||
- `netbox/<app>/` — Django apps: `circuits`, `core`, `dcim`, `ipam`, `extras`, `tenancy`, `virtualization`, `wireless`, `users`, `vpn`
|
||||
- `docs/` — MkDocs documentation source
|
||||
- `contrib/` — Example configs (systemd, nginx, etc.) and other resources
|
||||
|
||||
## Development Setup
|
||||
```bash
|
||||
python -m venv ~/.venv/netbox
|
||||
source ~/.venv/netbox/bin/activate
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Copy and configure
|
||||
cp netbox/netbox/configuration.example.py netbox/netbox/configuration.py
|
||||
# Edit configuration.py: set DATABASE, REDIS, SECRET_KEY, ALLOWED_HOSTS
|
||||
|
||||
cd netbox/
|
||||
python manage.py migrate
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
## Key Commands
|
||||
All commands run from the `netbox/` subdirectory with venv active.
|
||||
|
||||
```bash
|
||||
# Development server
|
||||
python manage.py runserver
|
||||
|
||||
# Run full test suite
|
||||
export NETBOX_CONFIGURATION=netbox.configuration_testing
|
||||
python manage.py test
|
||||
|
||||
# Faster test runs (no DB rebuild, parallel)
|
||||
python manage.py test --keepdb --parallel 4
|
||||
|
||||
# Migrations
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
|
||||
# Shell
|
||||
python manage.py nbshell # NetBox-enhanced shell
|
||||
```
|
||||
|
||||
## Architecture Conventions
|
||||
- **Apps**: Each Django app owns its models, views, API serializers, filtersets, forms, and tests.
|
||||
- **REST API**: DRF serializers live in `<app>/api/serializers.py`; viewsets in `<app>/api/views.py`; URLs auto-registered in `<app>/api/urls.py`.
|
||||
- **GraphQL**: Strawberry types in `<app>/graphql/types.py`.
|
||||
- **Filtersets**: `<app>/filtersets.py` — used for both UI filtering and API `?filter=` params.
|
||||
- **Tables**: `django-tables2` used for all object list views (`<app>/tables.py`).
|
||||
- **Templates**: Django templates in `netbox/templates/<app>/`.
|
||||
- **Tests**: Mirror the app structure in `<app>/tests/`. Use `netbox.configuration_testing` for test config.
|
||||
|
||||
## Coding Standards
|
||||
- Follow existing Django conventions; don't reinvent patterns already present in the codebase.
|
||||
- New models must include `created`, `last_updated` fields (inherit from `NetBoxModel` where appropriate).
|
||||
- Every model exposed in the UI needs: model, serializer, filterset, form, table, views, URL route, and tests.
|
||||
- API serializers must include a `url` field (absolute URL of the object).
|
||||
- Use `FeatureQuery` for generic relations (config contexts, custom fields, tags, etc.).
|
||||
- Avoid adding new dependencies without strong justification.
|
||||
|
||||
## Branch & PR Conventions
|
||||
- Branch naming: `<issue-number>-short-description` (e.g., `1234-device-typerror`)
|
||||
- Use the `main` branch for patch releases; `feature` tracks work for the upcoming minor/major release.
|
||||
- Every PR must reference an approved GitHub issue.
|
||||
- PRs must include tests for new functionality.
|
||||
|
||||
## Gotchas
|
||||
- `configuration.py` is gitignored — never commit it.
|
||||
- `manage.py` lives in `netbox/`, NOT the repo root. Running from the wrong directory is a common mistake.
|
||||
- `NETBOX_CONFIGURATION` env var controls which settings module loads; set to `netbox.configuration_testing` for tests.
|
||||
- The `extras` app is a catch-all for cross-cutting features (custom fields, tags, webhooks, scripts).
|
||||
- Plugins API: only documented public APIs are stable. Internal NetBox code is subject to change without notice.
|
||||
- See `docs/development/` for the full contributing guide and code style details.
|
||||
@@ -98,10 +98,6 @@ jsonschema
|
||||
# https://python-markdown.github.io/changelog/
|
||||
Markdown
|
||||
|
||||
# MkDocs
|
||||
# https://github.com/mkdocs/mkdocs/releases
|
||||
mkdocs<2.0
|
||||
|
||||
# MkDocs Material theme (for documentation build)
|
||||
# https://squidfunk.github.io/mkdocs-material/changelog/
|
||||
mkdocs-material
|
||||
@@ -161,7 +157,8 @@ strawberry-graphql
|
||||
|
||||
# Strawberry GraphQL Django extension
|
||||
# https://github.com/strawberry-graphql/strawberry-django/releases
|
||||
strawberry-graphql-django
|
||||
# Blocked by #21450
|
||||
strawberry-graphql-django==0.75.0
|
||||
|
||||
# SVG image rendering (used for rack elevations)
|
||||
# https://github.com/mozman/svgwrite/blob/master/NEWS.rst
|
||||
|
||||
@@ -349,7 +349,6 @@
|
||||
"5gbase-t",
|
||||
"10gbase-br-d",
|
||||
"10gbase-br-u",
|
||||
"10gbase-cu",
|
||||
"10gbase-cx4",
|
||||
"10gbase-er",
|
||||
"10gbase-lr",
|
||||
@@ -368,7 +367,6 @@
|
||||
"40gbase-fr4",
|
||||
"40gbase-lr4",
|
||||
"40gbase-sr4",
|
||||
"40gbase-sr4-bd",
|
||||
"50gbase-cr",
|
||||
"50gbase-er",
|
||||
"50gbase-fr",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -20,9 +20,7 @@ There are four core actions that can be permitted for each type of object within
|
||||
* **Change** - Modify an existing object
|
||||
* **Delete** - Delete an existing object
|
||||
|
||||
In addition to these, permissions can also grant custom actions that may be required by a specific model or plugin. For example, the `sync` action for data sources allows a user to synchronize data from a remote source, and the `render_config` action for devices and virtual machines allows rendering configuration templates.
|
||||
|
||||
Some models have registered custom actions that appear as checkboxes when creating or editing a permission. These are grouped by model under "Custom actions" in the permission form. Additional custom actions (such as those not yet registered or for backwards compatibility) can be entered manually in the "Additional actions" field.
|
||||
In addition to these, permissions can also grant custom actions that may be required by a specific model or plugin. For example, the `run` permission for scripts allows a user to execute custom scripts. These can be specified when granting a permission in the "additional actions" field.
|
||||
|
||||
!!! note
|
||||
Internally, all actions granted by a permission (both built-in and custom) are stored as strings in an array field named `actions`.
|
||||
|
||||
@@ -31,11 +31,6 @@ The following data is available as context for Jinja2 templates:
|
||||
* `data` - A detailed representation of the object in its current state. This is typically equivalent to the model's representation in NetBox's REST API.
|
||||
* `snapshots` - Minimal "snapshots" of the object state both before and after the change was made; provided as a dictionary with keys named `prechange` and `postchange`. These are not as extensive as the fully serialized representation, but contain enough information to convey what has changed.
|
||||
|
||||
!!! warning "Deprecation of legacy fields"
|
||||
The "request_id" and "username" fields in the webhook payload above are deprecated and should no longer be used. Support for them will be removed in NetBox v4.7.0.
|
||||
|
||||
Use `request.user.username` and `request.request_id` from the `request` object included in the callback context instead.
|
||||
|
||||
### Default Request Body
|
||||
|
||||
If no body template is specified, the request body will be populated with a JSON object containing the context data. For example, a newly created site might appear as follows:
|
||||
|
||||
@@ -88,8 +88,3 @@ The following context variables are available in to the text and link templates.
|
||||
| `request_id` | The unique request ID |
|
||||
| `data` | A complete serialized representation of the object |
|
||||
| `snapshots` | Pre- and post-change snapshots of the object |
|
||||
|
||||
!!! warning "Deprecation of legacy fields"
|
||||
The "request_id" and "username" fields in the webhook payload above are deprecated and should no longer be used. Support for them will be removed in NetBox v4.7.0.
|
||||
|
||||
Use `request.user.username` and `request.request_id` from the `request` object included in the callback context instead.
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# Custom Model Actions
|
||||
|
||||
Plugins can register custom permission actions for their models. These actions appear as checkboxes in the ObjectPermission form, making it easy for administrators to grant or restrict access to plugin-specific functionality without manually entering action names.
|
||||
|
||||
For example, a plugin might define a "sync" action for a model that syncs data from an external source, or a "bypass" action that allows users to bypass certain restrictions.
|
||||
|
||||
## Registering Model Actions
|
||||
|
||||
To register custom actions for a model, call `register_model_actions()` in your plugin's `ready()` method:
|
||||
|
||||
```python
|
||||
# __init__.py
|
||||
from netbox.plugins import PluginConfig
|
||||
|
||||
class MyPluginConfig(PluginConfig):
|
||||
name = 'my_plugin'
|
||||
# ...
|
||||
|
||||
def ready(self):
|
||||
super().ready()
|
||||
from utilities.permissions import ModelAction, register_model_actions
|
||||
from .models import MyModel
|
||||
|
||||
register_model_actions(MyModel, [
|
||||
ModelAction('sync', help_text='Synchronize data from external source'),
|
||||
ModelAction('export', help_text='Export data to external system'),
|
||||
])
|
||||
|
||||
config = MyPluginConfig
|
||||
```
|
||||
|
||||
Once registered, these actions will appear grouped under your model's name when creating or editing an ObjectPermission that includes your model as an object type.
|
||||
|
||||
::: utilities.permissions.ModelAction
|
||||
|
||||
::: utilities.permissions.register_model_actions
|
||||
@@ -43,11 +43,6 @@ The resulting webhook payload will look like the following:
|
||||
}
|
||||
```
|
||||
|
||||
!!! warning "Deprecation of legacy fields"
|
||||
The "request_id" and "username" fields in the webhook payload above are deprecated and should no longer be used. Support for them will be removed in NetBox v4.7.0.
|
||||
|
||||
Use `request.user.username` and `request.request_id` from the `request` object included in the callback context instead.
|
||||
|
||||
!!! note "Consider namespacing webhook data"
|
||||
The data returned from all webhook callbacks will be compiled into a single `context` dictionary. Any existing keys within this dictionary will be overwritten by subsequent callbacks which include those keys. To avoid collisions with webhook data provided by other plugins, consider namespacing your plugin's data within a nested dictionary as such:
|
||||
|
||||
|
||||
@@ -1,34 +1,5 @@
|
||||
# NetBox v4.5
|
||||
|
||||
## v4.5.4 (2026-03-03)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#21369](https://github.com/netbox-community/netbox/issues/21369) - Support lazy-loading of image attachments
|
||||
* [#21385](https://github.com/netbox-community/netbox/issues/21385) - Add contact assignment support for virtual circuits
|
||||
* [#21394](https://github.com/netbox-community/netbox/issues/21394) - Add 10GBASE-CU and 40GBASE-SR4 BiDi interface types
|
||||
* [#21477](https://github.com/netbox-community/netbox/issues/21477) - Extend GraphQL API filters for cables
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* [#21456](https://github.com/netbox-community/netbox/issues/21456) - Improve performance of config context resolution via GraphQL API
|
||||
* [#21459](https://github.com/netbox-community/netbox/issues/21459) - Avoid prefetching data for hidden table columns
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#20490](https://github.com/netbox-community/netbox/issues/20490) - Restrict visibility of scripts in list view to users with view permission
|
||||
* [#20911](https://github.com/netbox-community/netbox/issues/20911) - Sort module bay options alphabetically when installing a module
|
||||
* [#21347](https://github.com/netbox-community/netbox/issues/21347) - The allocation of IPv6 addresses from a non-pool prefix should start at one, not zero
|
||||
* [#21429](https://github.com/netbox-community/netbox/issues/21429) - Termination type should persist when employing "create & add another" workflow for cables
|
||||
* [#21478](https://github.com/netbox-community/netbox/issues/21478) - Fix GraphQL union type resolution for connected console ports
|
||||
* [#21481](https://github.com/netbox-community/netbox/issues/21481) - Fix display of facility ID on rack view
|
||||
* [#21518](https://github.com/netbox-community/netbox/issues/21518) - Fix decimal custom field displaying as unset when value is zero
|
||||
* [#21524](https://github.com/netbox-community/netbox/issues/21524) - Avoid `IndexError` exception when encountering stale cable paths
|
||||
* [#21527](https://github.com/netbox-community/netbox/issues/21527) - Fix display of primary IP address with associated NAT IP on device view
|
||||
* [#21550](https://github.com/netbox-community/netbox/issues/21550) - Ensure pre-change snapshots are recorded for related objects
|
||||
|
||||
---
|
||||
|
||||
## v4.5.3 (2026-02-17)
|
||||
|
||||
### Enhancements
|
||||
|
||||
@@ -151,7 +151,6 @@ nav:
|
||||
- Filters & Filter Sets: 'plugins/development/filtersets.md'
|
||||
- Search: 'plugins/development/search.md'
|
||||
- Event Types: 'plugins/development/event-types.md'
|
||||
- Permissions: 'plugins/development/permissions.md'
|
||||
- Data Backends: 'plugins/development/data-backends.md'
|
||||
- Webhooks: 'plugins/development/webhooks.md'
|
||||
- User Interface: 'plugins/development/user-interface.md'
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Annotated
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry.scalars import ID
|
||||
from strawberry_django import BaseFilterLookup, DateFilterLookup, StrFilterLookup
|
||||
from strawberry_django import BaseFilterLookup, DateFilterLookup, FilterLookup
|
||||
|
||||
from circuits import models
|
||||
from circuits.graphql.filter_mixins import CircuitTypeFilterMixin
|
||||
@@ -62,9 +62,9 @@ class CircuitTerminationFilter(
|
||||
upstream_speed: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
xconnect_id: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
pp_info: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
xconnect_id: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
pp_info: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
# Cached relations
|
||||
_provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
@@ -92,7 +92,7 @@ class CircuitFilter(
|
||||
TenancyFilterMixin,
|
||||
PrimaryModelFilter
|
||||
):
|
||||
cid: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
cid: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -145,8 +145,8 @@ class CircuitGroupAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, Cha
|
||||
|
||||
@strawberry_django.filter_type(models.Provider, lookups=True)
|
||||
class ProviderFilter(ContactFilterMixin, PrimaryModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
asns: Annotated['ASNFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
circuits: Annotated['CircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -159,18 +159,18 @@ class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilter):
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
provider_id: ID | None = strawberry_django.filter_field()
|
||||
account: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
account: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ProviderNetwork, lookups=True)
|
||||
class ProviderNetworkFilter(PrimaryModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
provider_id: ID | None = strawberry_django.filter_field()
|
||||
service_id: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
service_id: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VirtualCircuitType, lookups=True)
|
||||
@@ -180,7 +180,7 @@ class VirtualCircuitTypeFilter(CircuitTypeFilterMixin, OrganizationalModelFilter
|
||||
|
||||
@strawberry_django.filter_type(models.VirtualCircuit, lookups=True)
|
||||
class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
cid: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
cid: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -218,4 +218,4 @@ class VirtualCircuitTerminationFilter(CustomFieldsFilterMixin, TagsFilterMixin,
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
interface_id: ID | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -25,19 +25,12 @@ class CoreConfig(AppConfig):
|
||||
from core.checks import check_duplicate_indexes # noqa: F401
|
||||
from netbox import context_managers # noqa: F401
|
||||
from netbox.models.features import register_models
|
||||
from utilities.permissions import ModelAction, register_model_actions
|
||||
|
||||
from . import data_backends, events, search # noqa: F401
|
||||
from .models import DataSource
|
||||
|
||||
# Register models
|
||||
register_models(*self.get_models())
|
||||
|
||||
# Register custom permission actions
|
||||
register_model_actions(DataSource, [
|
||||
ModelAction('sync', help_text=_('Synchronize data from remote source')),
|
||||
])
|
||||
|
||||
# Register core events
|
||||
EventType(OBJECT_CREATED, _('Object created')).register()
|
||||
EventType(OBJECT_UPDATED, _('Object updated')).register()
|
||||
|
||||
@@ -5,7 +5,7 @@ import strawberry
|
||||
import strawberry_django
|
||||
from django.contrib.contenttypes.models import ContentType as DjangoContentType
|
||||
from strawberry.scalars import ID
|
||||
from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLookup, StrFilterLookup
|
||||
from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLookup
|
||||
|
||||
from core import models
|
||||
from netbox.graphql.filters import BaseModelFilter, PrimaryModelFilter
|
||||
@@ -32,23 +32,23 @@ class DataFileFilter(BaseModelFilter):
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
source_id: ID | None = strawberry_django.filter_field()
|
||||
path: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
path: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
size: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
hash: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
hash: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.DataSource, lookups=True)
|
||||
class DataSourceFilter(PrimaryModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
type: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
source_url: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
type: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
source_url: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
status: (
|
||||
BaseFilterLookup[Annotated['DataSourceStatusEnum', strawberry.lazy('core.graphql.enums')]] | None
|
||||
) = strawberry_django.filter_field()
|
||||
enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
ignore_rules: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
ignore_rules: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
parameters: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -62,8 +62,8 @@ class DataSourceFilter(PrimaryModelFilter):
|
||||
class ObjectChangeFilter(BaseModelFilter):
|
||||
time: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
|
||||
user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
user_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
request_id: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
user_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
request_id: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
action: (
|
||||
BaseFilterLookup[Annotated['ObjectChangeActionEnum', strawberry.lazy('core.graphql.enums')]] | None
|
||||
) = strawberry_django.filter_field()
|
||||
@@ -76,7 +76,7 @@ class ObjectChangeFilter(BaseModelFilter):
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
related_object_id: ID | None = strawberry_django.filter_field()
|
||||
object_repr: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
object_repr: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
prechange_data: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -87,5 +87,5 @@ class ObjectChangeFilter(BaseModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(DjangoContentType, lookups=True)
|
||||
class ContentTypeFilter(BaseModelFilter):
|
||||
app_label: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
model: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
app_label: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
model: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -84,9 +84,6 @@ class CablePathSerializer(serializers.ModelSerializer):
|
||||
def get_path(self, obj):
|
||||
ret = []
|
||||
for nodes in obj.path_objects:
|
||||
if not nodes:
|
||||
# The path contains an invalid object
|
||||
return []
|
||||
serializer = get_serializer_for_model(nodes[0])
|
||||
context = {'request': self.context['request']}
|
||||
ret.append(serializer(nodes, nested=True, many=True, context=context).data)
|
||||
|
||||
@@ -8,11 +8,8 @@ class DCIMConfig(AppConfig):
|
||||
verbose_name = "DCIM"
|
||||
|
||||
def ready(self):
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from netbox.models.features import register_models
|
||||
from utilities.counters import connect_counters
|
||||
from utilities.permissions import ModelAction, register_model_actions
|
||||
|
||||
from . import search, signals # noqa: F401
|
||||
from .models import CableTermination, Device, DeviceType, ModuleType, RackType, VirtualChassis
|
||||
@@ -20,11 +17,6 @@ class DCIMConfig(AppConfig):
|
||||
# Register models
|
||||
register_models(*self.get_models())
|
||||
|
||||
# Register custom permission actions
|
||||
register_model_actions(Device, [
|
||||
ModelAction('render_config', help_text=_('Render device configuration')),
|
||||
])
|
||||
|
||||
# Register denormalized fields
|
||||
denormalized.register(CableTermination, '_device', {
|
||||
'_rack': 'rack',
|
||||
|
||||
@@ -1386,7 +1386,6 @@ class MACAddressImportForm(PrimaryModelImportForm):
|
||||
|
||||
# Assign the MAC address as primary for its interface, if designated as such
|
||||
if interface and self.cleaned_data['is_primary'] and self.instance.pk:
|
||||
interface.snapshot()
|
||||
interface.primary_mac_address = self.instance
|
||||
interface.save()
|
||||
|
||||
|
||||
@@ -15,10 +15,6 @@ def get_cable_form(a_type, b_type):
|
||||
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
|
||||
# NOTE: Cable.clone() mirrors the parent selector mapping below:
|
||||
# termination_{end}_device / termination_{end}_powerpanel / termination_{end}_circuit
|
||||
# This supports both the "Clone" and "Create & Add Another" workflows.
|
||||
# If you change the mapping here, update Cable.clone() accordingly.
|
||||
for cable_end, term_cls in (('a', a_type), ('b', b_type)):
|
||||
|
||||
# Device component
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Annotated
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry import ID
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup, StrFilterLookup
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup
|
||||
|
||||
from core.graphql.filters import ContentTypeFilter
|
||||
|
||||
@@ -66,9 +66,9 @@ class ComponentModelFilterMixin:
|
||||
)
|
||||
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
device_id: ID | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
label: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
label: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -96,9 +96,9 @@ class ComponentTemplateFilterMixin:
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
device_type_id: ID | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
label: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
label: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -4,7 +4,7 @@ import strawberry
|
||||
import strawberry_django
|
||||
from django.db.models import Q
|
||||
from strawberry.scalars import ID
|
||||
from strawberry_django import BaseFilterLookup, ComparisonFilterLookup, FilterLookup, StrFilterLookup
|
||||
from strawberry_django import BaseFilterLookup, ComparisonFilterLookup, FilterLookup
|
||||
|
||||
from dcim import models
|
||||
from dcim.constants import *
|
||||
@@ -114,7 +114,7 @@ class CableFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
status: BaseFilterLookup[Annotated['LinkStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
label: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
label: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -141,20 +141,6 @@ class CableTerminationFilter(ChangeLoggedModelFilter):
|
||||
)
|
||||
termination_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
# Cached relations
|
||||
_device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field(
|
||||
name='device'
|
||||
)
|
||||
_rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field(
|
||||
name='rack'
|
||||
)
|
||||
_location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field(name='location')
|
||||
)
|
||||
_site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field(
|
||||
name='site'
|
||||
)
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ConsolePort, lookups=True)
|
||||
class ConsolePortFilter(ModularComponentFilterMixin, CabledObjectModelFilterMixin, NetBoxModelFilter):
|
||||
@@ -210,9 +196,9 @@ class DeviceFilter(
|
||||
platform: Annotated['PlatformFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
serial: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
asset_tag: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
serial: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
asset_tag: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
site_id: ID | None = strawberry_django.filter_field()
|
||||
location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
@@ -339,7 +325,7 @@ class InventoryItemTemplateFilter(ComponentTemplateFilterMixin, ChangeLoggedMode
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
manufacturer_id: ID | None = strawberry_django.filter_field()
|
||||
part_id: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
part_id: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.DeviceRole, lookups=True)
|
||||
@@ -356,13 +342,13 @@ class DeviceTypeFilter(ImageAttachmentFilterMixin, WeightFilterMixin, PrimaryMod
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
manufacturer_id: ID | None = strawberry_django.filter_field()
|
||||
model: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
model: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
default_platform: Annotated['PlatformFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
default_platform_id: ID | None = strawberry_django.filter_field()
|
||||
part_number: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
part_number: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
instances: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -479,7 +465,7 @@ class PortTemplateMappingFilter(BaseModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.MACAddress, lookups=True)
|
||||
class MACAddressFilter(PrimaryModelFilter):
|
||||
mac_address: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
mac_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -525,7 +511,7 @@ class InterfaceFilter(
|
||||
duplex: BaseFilterLookup[Annotated['InterfaceDuplexEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
wwn: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
wwn: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
parent: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -645,9 +631,9 @@ class InventoryItemFilter(ComponentModelFilterMixin, NetBoxModelFilter):
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
manufacturer_id: ID | None = strawberry_django.filter_field()
|
||||
part_id: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
serial: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
asset_tag: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
part_id: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
serial: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
asset_tag: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
discovered: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@@ -665,7 +651,7 @@ class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilt
|
||||
status: BaseFilterLookup[Annotated['LocationStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
facility: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
facility: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -694,8 +680,8 @@ class ModuleFilter(ConfigContextFilterMixin, PrimaryModelFilter):
|
||||
status: BaseFilterLookup[Annotated['ModuleStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
serial: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
asset_tag: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
serial: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
asset_tag: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
console_ports: Annotated['ConsolePortFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -734,17 +720,17 @@ class ModuleBayFilter(ModularComponentFilterMixin, NetBoxModelFilter):
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
parent_id: ID | None = strawberry_django.filter_field()
|
||||
position: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
position: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ModuleBayTemplate, lookups=True)
|
||||
class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin, ChangeLoggedModelFilter):
|
||||
position: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
position: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ModuleTypeProfile, lookups=True)
|
||||
class ModuleTypeProfileFilter(PrimaryModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ModuleType, lookups=True)
|
||||
@@ -757,8 +743,8 @@ class ModuleTypeFilter(ImageAttachmentFilterMixin, WeightFilterMixin, PrimaryMod
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
profile_id: ID | None = strawberry_django.filter_field()
|
||||
model: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
part_number: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
model: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
part_number: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
instances: Annotated['ModuleFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -818,7 +804,7 @@ class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryM
|
||||
power_panel_id: ID | None = strawberry_django.filter_field()
|
||||
rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
rack_id: ID | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
status: BaseFilterLookup[Annotated['PowerFeedStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -889,7 +875,7 @@ class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryMo
|
||||
location_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.PowerPort, lookups=True)
|
||||
@@ -927,8 +913,8 @@ class RackTypeFilter(ImageAttachmentFilterMixin, RackFilterMixin, WeightFilterMi
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
manufacturer_id: ID | None = strawberry_django.filter_field()
|
||||
model: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
model: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
racks: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
rack_count: ComparisonFilterLookup[int] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -949,8 +935,8 @@ class RackFilter(
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
rack_type_id: ID | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
facility_id: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
facility_id: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
site_id: ID | None = strawberry_django.filter_field()
|
||||
location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
@@ -964,8 +950,8 @@ class RackFilter(
|
||||
)
|
||||
role: Annotated['RackRoleFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
role_id: ID | None = strawberry_django.filter_field()
|
||||
serial: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
asset_tag: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
serial: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
asset_tag: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
airflow: BaseFilterLookup[Annotated['RackAirflowEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -983,7 +969,7 @@ class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
)
|
||||
user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
user_id: ID | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
status: BaseFilterLookup[Annotated['RackReservationStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -1034,8 +1020,8 @@ class RegionFilter(ContactFilterMixin, NestedGroupModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.Site, lookups=True)
|
||||
class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
status: BaseFilterLookup[Annotated['SiteStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -1049,11 +1035,11 @@ class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMi
|
||||
group_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
facility: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
facility: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
asns: Annotated['ASNFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
time_zone: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
physical_address: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
shipping_address: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
time_zone: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
physical_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
shipping_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
latitude: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -1082,8 +1068,8 @@ class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilter):
|
||||
class VirtualChassisFilter(PrimaryModelFilter):
|
||||
master: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
master_id: ID | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
domain: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
domain: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
members: (
|
||||
Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
@@ -1094,7 +1080,7 @@ class VirtualChassisFilter(PrimaryModelFilter):
|
||||
class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
device_id: ID | None = strawberry_django.filter_field()
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
status: (
|
||||
BaseFilterLookup[Annotated['VirtualDeviceContextStatusEnum', strawberry.lazy('dcim.graphql.enums')]] | None
|
||||
) = (
|
||||
@@ -1111,7 +1097,7 @@ class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
primary_ip6_id: ID | None = strawberry_django.filter_field()
|
||||
comments: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
comments: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
interfaces: (
|
||||
Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None
|
||||
) = strawberry_django.filter_field()
|
||||
|
||||
@@ -305,50 +305,6 @@ class Cable(PrimaryModel):
|
||||
except UnsupportedCablePath as e:
|
||||
raise AbortRequest(e)
|
||||
|
||||
def clone(self):
|
||||
"""
|
||||
Return attributes suitable for cloning this cable.
|
||||
|
||||
In addition to the fields defined in `clone_fields`, include the termination
|
||||
type and parent selector fields used by dcim.forms.connections.get_cable_form().
|
||||
"""
|
||||
attrs = super().clone()
|
||||
|
||||
# Mirror dcim.forms.connections.get_cable_form() parent-field logic
|
||||
for cable_end, terminations in (('a', self.a_terminations), ('b', self.b_terminations)):
|
||||
if not terminations:
|
||||
continue
|
||||
|
||||
term_cls = type(terminations[0])
|
||||
term_label = term_cls._meta.label_lower
|
||||
|
||||
# Matches CableForm choices: "<app_label>.<model>"
|
||||
attrs[f'{cable_end}_terminations_type'] = term_label
|
||||
|
||||
# Device component
|
||||
if hasattr(term_cls, 'device'):
|
||||
device_ids = sorted({t.device_id for t in terminations if t.device_id})
|
||||
if device_ids:
|
||||
attrs[f'termination_{cable_end}_device'] = device_ids
|
||||
|
||||
# PowerFeed
|
||||
elif term_label == 'dcim.powerfeed':
|
||||
powerpanel_ids = sorted({t.power_panel_id for t in terminations if t.power_panel_id})
|
||||
if powerpanel_ids:
|
||||
attrs[f'termination_{cable_end}_powerpanel'] = powerpanel_ids
|
||||
|
||||
# CircuitTermination
|
||||
elif term_label == 'circuits.circuittermination':
|
||||
circuit_ids = sorted({t.circuit_id for t in terminations if t.circuit_id})
|
||||
if circuit_ids:
|
||||
attrs[f'termination_{cable_end}_circuit'] = circuit_ids
|
||||
|
||||
# Never clone the actual terminations, as they are already occupied
|
||||
attrs.pop('a_terminations', None)
|
||||
attrs.pop('b_terminations', None)
|
||||
|
||||
return attrs
|
||||
|
||||
def serialize_object(self, exclude=None):
|
||||
data = serialize_object(self, exclude=exclude or [])
|
||||
|
||||
|
||||
@@ -2614,126 +2614,6 @@ class CableTest(APIViewTestCases.APIViewTestCase):
|
||||
},
|
||||
]
|
||||
|
||||
def test_graphql_cable_termination_cached_filters(self):
|
||||
"""
|
||||
Validate filtering cables by cached CableTermination relations via GraphQL:
|
||||
|
||||
cable_list(filters: { terminations: { <relation>: {...}, DISTINCT: true } })
|
||||
|
||||
Also asserts deduplication when both ends match (cable between two interfaces
|
||||
on the same device/rack/location/site).
|
||||
"""
|
||||
self.add_permissions(
|
||||
'dcim.view_cable',
|
||||
'dcim.view_device',
|
||||
'dcim.view_interface',
|
||||
'dcim.view_rack',
|
||||
'dcim.view_location',
|
||||
'dcim.view_site',
|
||||
)
|
||||
|
||||
# Reuse existing fixtures from setUpTestData()
|
||||
devicetype = DeviceType.objects.get(slug='device-type-1')
|
||||
role = DeviceRole.objects.get(slug='device-role-1')
|
||||
|
||||
# Create an isolated topology for this test
|
||||
site_a = Site.objects.create(name='GQL Site A', slug='gql-site-a')
|
||||
site_b = Site.objects.create(name='GQL Site B', slug='gql-site-b')
|
||||
|
||||
location_a = Location.objects.create(
|
||||
site=site_a,
|
||||
name='GQL Location A',
|
||||
slug='gql-location-a',
|
||||
status=LocationStatusChoices.STATUS_ACTIVE,
|
||||
)
|
||||
location_b = Location.objects.create(
|
||||
site=site_b,
|
||||
name='GQL Location B',
|
||||
slug='gql-location-b',
|
||||
status=LocationStatusChoices.STATUS_ACTIVE,
|
||||
)
|
||||
|
||||
rack_a = Rack.objects.create(site=site_a, location=location_a, name='GQL Rack A', u_height=42)
|
||||
rack_b = Rack.objects.create(site=site_b, location=location_b, name='GQL Rack B', u_height=42)
|
||||
|
||||
device_a = Device.objects.create(
|
||||
device_type=devicetype,
|
||||
role=role,
|
||||
name='GQL Device A',
|
||||
site=site_a,
|
||||
location=location_a,
|
||||
rack=rack_a,
|
||||
)
|
||||
device_b = Device.objects.create(
|
||||
device_type=devicetype,
|
||||
role=role,
|
||||
name='GQL Device B',
|
||||
site=site_b,
|
||||
location=location_b,
|
||||
rack=rack_b,
|
||||
)
|
||||
|
||||
a0 = Interface.objects.create(device=device_a, type=InterfaceTypeChoices.TYPE_1GE_FIXED, name='eth0')
|
||||
a1 = Interface.objects.create(device=device_a, type=InterfaceTypeChoices.TYPE_1GE_FIXED, name='eth1')
|
||||
a2 = Interface.objects.create(device=device_a, type=InterfaceTypeChoices.TYPE_1GE_FIXED, name='eth2')
|
||||
b0 = Interface.objects.create(device=device_b, type=InterfaceTypeChoices.TYPE_1GE_FIXED, name='eth0')
|
||||
|
||||
# Both ends on Device A (duplication risk without DISTINCT)
|
||||
cable_same_device = Cable(a_terminations=[a0], b_terminations=[a1], label='GQL Cable Same Device')
|
||||
cable_same_device.save()
|
||||
|
||||
# Cross to Device B
|
||||
cable_cross = Cable(a_terminations=[a2], b_terminations=[b0], label='GQL Cable Cross')
|
||||
cable_cross.save()
|
||||
|
||||
expected_a = {str(cable_same_device.pk), str(cable_cross.pk)}
|
||||
expected_b = {str(cable_cross.pk)}
|
||||
|
||||
url = reverse('graphql')
|
||||
|
||||
test_cases = (
|
||||
# Device (ID + name)
|
||||
(f'device: {{ id: {{ exact: "{device_a.pk}" }} }}', expected_a),
|
||||
(f'device: {{ name: {{ exact: "{device_a.name}" }} }}', expected_a),
|
||||
(f'device: {{ id: {{ exact: "{device_b.pk}" }} }}', expected_b),
|
||||
(f'device: {{ name: {{ exact: "{device_b.name}" }} }}', expected_b),
|
||||
# Rack (ID + name)
|
||||
(f'rack: {{ id: {{ exact: "{rack_a.pk}" }} }}', expected_a),
|
||||
(f'rack: {{ name: {{ exact: "{rack_a.name}" }} }}', expected_a),
|
||||
(f'rack: {{ id: {{ exact: "{rack_b.pk}" }} }}', expected_b),
|
||||
(f'rack: {{ name: {{ exact: "{rack_b.name}" }} }}', expected_b),
|
||||
# Location (ID + name)
|
||||
(f'location: {{ id: {{ exact: "{location_a.pk}" }} }}', expected_a),
|
||||
(f'location: {{ name: {{ exact: "{location_a.name}" }} }}', expected_a),
|
||||
(f'location: {{ id: {{ exact: "{location_b.pk}" }} }}', expected_b),
|
||||
(f'location: {{ name: {{ exact: "{location_b.name}" }} }}', expected_b),
|
||||
# Site (ID + slug)
|
||||
(f'site: {{ id: {{ exact: "{site_a.pk}" }} }}', expected_a),
|
||||
(f'site: {{ slug: {{ exact: "{site_a.slug}" }} }}', expected_a),
|
||||
(f'site: {{ id: {{ exact: "{site_b.pk}" }} }}', expected_b),
|
||||
(f'site: {{ slug: {{ exact: "{site_b.slug}" }} }}', expected_b),
|
||||
)
|
||||
|
||||
for inner_filter, expected in test_cases:
|
||||
with self.subTest(filter=inner_filter):
|
||||
query = f"""{{
|
||||
cable_list(filters: {{ terminations: {{ {inner_filter} DISTINCT: true }} }})
|
||||
{{ id }}
|
||||
}}"""
|
||||
|
||||
response = self.client.post(url, data={'query': query}, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
data = response.json()
|
||||
self.assertNotIn('errors', data)
|
||||
|
||||
rows = data['data']['cable_list']
|
||||
ids = [row['id'] for row in rows]
|
||||
|
||||
# Ensure DISTINCT is actually effective (no duplicate cables when both ends match)
|
||||
self.assertEqual(len(ids), len(set(ids)), f'Duplicate cables returned for: {inner_filter}')
|
||||
|
||||
self.assertSetEqual(set(ids), expected)
|
||||
|
||||
|
||||
class CableTerminationTest(
|
||||
APIViewTestCases.GetObjectViewTestCase,
|
||||
|
||||
@@ -2733,7 +2733,6 @@ class DeviceBulkImportView(generic.BulkImportView):
|
||||
# For child devices, save the reverse relation to the parent device bay
|
||||
if parent_bay:
|
||||
device_bay = parent_bay
|
||||
device_bay.snapshot()
|
||||
device_bay.installed_device = obj
|
||||
device_bay.save()
|
||||
|
||||
@@ -3913,6 +3912,19 @@ class CableEditView(generic.ObjectEditView):
|
||||
|
||||
return super().alter_object(obj, request, url_args, url_kwargs)
|
||||
|
||||
def get_extra_addanother_params(self, request):
|
||||
|
||||
params = {
|
||||
'a_terminations_type': request.GET.get('a_terminations_type'),
|
||||
'b_terminations_type': request.GET.get('b_terminations_type')
|
||||
}
|
||||
|
||||
for key in request.POST:
|
||||
if 'device' in key or 'power_panel' in key or 'circuit' in key:
|
||||
params.update({key: request.POST.get(key)})
|
||||
|
||||
return params
|
||||
|
||||
|
||||
@register_model_view(Cable, 'delete')
|
||||
class CableDeleteView(generic.ObjectDeleteView):
|
||||
@@ -4087,7 +4099,6 @@ class VirtualChassisEditView(ObjectPermissionRequiredMixin, GetReturnURLMixin, V
|
||||
members = formset.save(commit=False)
|
||||
devices = Device.objects.filter(pk__in=[m.pk for m in members])
|
||||
for device in devices:
|
||||
device.snapshot()
|
||||
device.vc_position = None
|
||||
device.save()
|
||||
for member in members:
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Annotated
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry.scalars import ID
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup, StrFilterLookup
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup
|
||||
|
||||
from extras import models
|
||||
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin
|
||||
@@ -50,11 +50,11 @@ __all__ = (
|
||||
|
||||
@strawberry_django.filter_type(models.ConfigContext, lookups=True)
|
||||
class ConfigContextFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
is_active: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
regions: Annotated['RegionFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
@@ -107,22 +107,22 @@ class ConfigContextFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.ConfigContextProfile, lookups=True)
|
||||
class ConfigContextProfileFilter(SyncedDataFilterMixin, PrimaryModelFilter):
|
||||
name: StrFilterLookup[str] = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] = strawberry_django.filter_field()
|
||||
tags: Annotated['TagFilter', strawberry.lazy('extras.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.ConfigTemplate, lookups=True)
|
||||
class ConfigTemplateFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
template_code: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
template_code: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
environment_params: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
mime_type: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
file_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
file_extension: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
mime_type: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
file_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
file_extension: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
as_attachment: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@@ -137,10 +137,10 @@ class CustomFieldFilter(ChangeLoggedModelFilter):
|
||||
related_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
label: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
group_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
label: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
group_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
required: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
unique: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
search_weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
@@ -166,7 +166,7 @@ class CustomFieldFilter(ChangeLoggedModelFilter):
|
||||
validation_maximum: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
validation_regex: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
validation_regex: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
choice_set: Annotated['CustomFieldChoiceSetFilter', strawberry.lazy('extras.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -182,13 +182,13 @@ class CustomFieldFilter(ChangeLoggedModelFilter):
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
is_cloneable: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
comments: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
comments: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.CustomFieldChoiceSet, lookups=True)
|
||||
class CustomFieldChoiceSetFilter(ChangeLoggedModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
base_choices: (
|
||||
BaseFilterLookup[Annotated['CustomFieldChoiceSetBaseEnum', strawberry.lazy('extras.graphql.enums')]] | None
|
||||
) = (
|
||||
@@ -202,14 +202,14 @@ class CustomFieldChoiceSetFilter(ChangeLoggedModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.CustomLink, lookups=True)
|
||||
class CustomLinkFilter(ChangeLoggedModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
link_text: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
link_url: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
link_text: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
link_url: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
group_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
group_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
button_class: (
|
||||
BaseFilterLookup[Annotated['CustomLinkButtonClassEnum', strawberry.lazy('extras.graphql.enums')]] | None
|
||||
) = (
|
||||
@@ -220,15 +220,15 @@ class CustomLinkFilter(ChangeLoggedModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.ExportTemplate, lookups=True)
|
||||
class ExportTemplateFilter(SyncedDataFilterMixin, ChangeLoggedModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
template_code: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
template_code: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
environment_params: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
mime_type: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
file_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
file_extension: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
mime_type: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
file_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
file_extension: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
as_attachment: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@@ -244,7 +244,7 @@ class ImageAttachmentFilter(ChangeLoggedModelFilter):
|
||||
image_width: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.JournalEntry, lookups=True)
|
||||
@@ -260,22 +260,22 @@ class JournalEntryFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedM
|
||||
kind: BaseFilterLookup[Annotated['JournalEntryKindEnum', strawberry.lazy('extras.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
comments: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
comments: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.NotificationGroup, lookups=True)
|
||||
class NotificationGroupFilter(ChangeLoggedModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
groups: Annotated['GroupFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
users: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.SavedFilter, lookups=True)
|
||||
class SavedFilterFilter(ChangeLoggedModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
user_id: ID | None = strawberry_django.filter_field()
|
||||
weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
@@ -290,8 +290,8 @@ class SavedFilterFilter(ChangeLoggedModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.TableConfig, lookups=True)
|
||||
class TableConfigFilter(ChangeLoggedModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
user_id: ID | None = strawberry_django.filter_field()
|
||||
weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
@@ -303,30 +303,30 @@ class TableConfigFilter(ChangeLoggedModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.Tag, lookups=True)
|
||||
class TagFilter(ChangeLoggedModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
color: BaseFilterLookup[Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.Webhook, lookups=True)
|
||||
class WebhookFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
payload_url: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
payload_url: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
http_method: (
|
||||
BaseFilterLookup[Annotated['WebhookHttpMethodEnum', strawberry.lazy('extras.graphql.enums')]] | None
|
||||
) = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
http_content_type: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
additional_headers: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
body_template: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
secret: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
http_content_type: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
additional_headers: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
body_template: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
secret: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
ssl_verification: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
ca_file_path: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
ca_file_path: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
events: Annotated['EventRuleFilter', strawberry.lazy('extras.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -334,8 +334,8 @@ class WebhookFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelF
|
||||
|
||||
@strawberry_django.filter_type(models.EventRule, lookups=True)
|
||||
class EventRuleFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
event_types: Annotated['StringArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -346,10 +346,10 @@ class EventRuleFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLoggedMode
|
||||
action_type: BaseFilterLookup[Annotated['EventRuleActionEnum', strawberry.lazy('extras.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
action_object_type: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
action_object_type: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
action_object_type_id: ID | None = strawberry_django.filter_field()
|
||||
action_object_id: ID | None = strawberry_django.filter_field()
|
||||
action_data: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
comments: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
comments: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import warnings
|
||||
from datetime import timedelta
|
||||
from importlib import import_module
|
||||
|
||||
@@ -18,12 +17,11 @@ class Command(BaseCommand):
|
||||
help = "Perform nightly housekeeping tasks [DEPRECATED]"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
warnings.warn(
|
||||
"\n\nDEPRECATION WARNING\n"
|
||||
self.stdout.write(
|
||||
"Running this command is no longer necessary: All housekeeping tasks\n"
|
||||
"are addressed automatically via NetBox's built-in job scheduler. It\n"
|
||||
"will be removed in a future release.\n",
|
||||
category=FutureWarning,
|
||||
"will be removed in a future release.",
|
||||
self.style.WARNING
|
||||
)
|
||||
|
||||
config = Config()
|
||||
|
||||
@@ -424,7 +424,6 @@ class IPAddressImportForm(PrimaryModelImportForm):
|
||||
# Set as primary for device/VM
|
||||
if self.cleaned_data.get('is_primary') is not None:
|
||||
parent = self.cleaned_data.get('device') or self.cleaned_data.get('virtual_machine')
|
||||
parent.snapshot()
|
||||
if self.instance.address.version == 4:
|
||||
parent.primary_ip4 = ipaddress if self.cleaned_data.get('is_primary') else None
|
||||
elif self.instance.address.version == 6:
|
||||
@@ -434,7 +433,6 @@ class IPAddressImportForm(PrimaryModelImportForm):
|
||||
# Set as OOB for device
|
||||
if self.cleaned_data.get('is_oob') is not None:
|
||||
parent = self.cleaned_data.get('device')
|
||||
parent.snapshot()
|
||||
parent.oob_ip = ipaddress if self.cleaned_data.get('is_oob') else None
|
||||
parent.save()
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import strawberry_django
|
||||
from django.db.models import Q
|
||||
from netaddr.core import AddrFormatError
|
||||
from strawberry.scalars import ID
|
||||
from strawberry_django import BaseFilterLookup, DateFilterLookup, FilterLookup, StrFilterLookup
|
||||
from strawberry_django import BaseFilterLookup, DateFilterLookup, FilterLookup
|
||||
|
||||
from dcim.graphql.filter_mixins import ScopedFilterMixin
|
||||
from dcim.models import Device
|
||||
@@ -70,8 +70,8 @@ class ASNFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.ASNRange, lookups=True)
|
||||
class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
rir_id: ID | None = strawberry_django.filter_field()
|
||||
start: Annotated['BigIntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
@@ -84,7 +84,7 @@ class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.Aggregate, lookups=True)
|
||||
class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
prefix: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
prefix: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
rir_id: ID | None = strawberry_django.filter_field()
|
||||
date_added: DateFilterLookup[date] | None = strawberry_django.filter_field()
|
||||
@@ -120,14 +120,14 @@ class FHRPGroupFilter(PrimaryModelFilter):
|
||||
group_id: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
protocol: BaseFilterLookup[Annotated['FHRPGroupProtocolEnum', strawberry.lazy('ipam.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
auth_type: BaseFilterLookup[Annotated['FHRPGroupAuthTypeEnum', strawberry.lazy('ipam.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
auth_key: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
auth_key: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -138,7 +138,7 @@ class FHRPGroupAssignmentFilter(ChangeLoggedModelFilter):
|
||||
interface_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
interface_id: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
interface_id: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
group: Annotated['FHRPGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -174,7 +174,7 @@ class FHRPGroupAssignmentFilter(ChangeLoggedModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.IPAddress, lookups=True)
|
||||
class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
address: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
vrf_id: ID | None = strawberry_django.filter_field()
|
||||
status: BaseFilterLookup[Annotated['IPAddressStatusEnum', strawberry.lazy('ipam.graphql.enums')]] | None = (
|
||||
@@ -195,7 +195,7 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
nat_outside_id: ID | None = strawberry_django.filter_field()
|
||||
dns_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
dns_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
@strawberry_django.filter_field()
|
||||
def assigned(self, value: bool, prefix) -> Q:
|
||||
@@ -225,8 +225,8 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
|
||||
|
||||
@strawberry_django.filter_type(models.IPRange, lookups=True)
|
||||
class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
start_address: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
end_address: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
start_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
end_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
size: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -279,7 +279,7 @@ class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.Prefix, lookups=True)
|
||||
class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
prefix: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
prefix: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
vrf_id: ID | None = strawberry_django.filter_field()
|
||||
vlan: Annotated['VLANFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
@@ -328,7 +328,7 @@ class RoleFilter(OrganizationalModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.RouteTarget, lookups=True)
|
||||
class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
importing_vrfs: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -345,7 +345,7 @@ class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.Service, lookups=True)
|
||||
class ServiceFilter(ContactFilterMixin, ServiceFilterMixin, PrimaryModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -357,7 +357,7 @@ class ServiceFilter(ContactFilterMixin, ServiceFilterMixin, PrimaryModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.ServiceTemplate, lookups=True)
|
||||
class ServiceTemplateFilter(ServiceFilterMixin, PrimaryModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VLAN, lookups=True)
|
||||
@@ -371,7 +371,7 @@ class VLANFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
vid: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
status: BaseFilterLookup[Annotated['VLANStatusEnum', strawberry.lazy('ipam.graphql.enums')]] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -401,7 +401,7 @@ class VLANGroupFilter(ScopedFilterMixin, OrganizationalModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.VLANTranslationPolicy, lookups=True)
|
||||
class VLANTranslationPolicyFilter(PrimaryModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.VLANTranslationRule, lookups=True)
|
||||
@@ -410,7 +410,7 @@ class VLANTranslationRuleFilter(NetBoxModelFilter):
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
policy_id: ID | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
local_vid: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -421,8 +421,8 @@ class VLANTranslationRuleFilter(NetBoxModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.VRF, lookups=True)
|
||||
class VRFFilter(TenancyFilterMixin, PrimaryModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
rd: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
rd: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
enforce_unique: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
import_targets: Annotated['RouteTargetFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
|
||||
@@ -432,11 +432,9 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
|
||||
])
|
||||
available_ips = prefix - child_ips - child_ranges
|
||||
|
||||
# Pool, IPv4 /31-/32 or IPv6 /127-/128 sets are fully usable
|
||||
if (
|
||||
self.is_pool
|
||||
or (self.family == 4 and self.prefix.prefixlen >= 31)
|
||||
or (self.family == 6 and self.prefix.prefixlen >= 127)
|
||||
# IPv6 /127's, pool, or IPv4 /31-/32 sets are fully usable
|
||||
if (self.family == 6 and self.prefix.prefixlen >= 127) or self.is_pool or (
|
||||
self.family == 4 and self.prefix.prefixlen >= 31
|
||||
):
|
||||
return available_ips
|
||||
|
||||
|
||||
@@ -39,132 +39,3 @@ class AnnotatedIPAddressTableTest(TestCase):
|
||||
|
||||
iprange_checkbox_count = html.count(f'name="pk" value="{self.ip_range.pk}"')
|
||||
self.assertEqual(iprange_checkbox_count, 0)
|
||||
|
||||
def test_annotate_ip_space_ipv4_non_pool_excludes_network_and_broadcast(self):
|
||||
prefix = Prefix.objects.create(
|
||||
prefix=IPNetwork('192.0.2.0/29'), # 8 addresses total
|
||||
status='active',
|
||||
is_pool=False,
|
||||
)
|
||||
|
||||
data = annotate_ip_space(prefix)
|
||||
|
||||
self.assertEqual(len(data), 1)
|
||||
available = data[0]
|
||||
|
||||
# /29 non-pool: exclude .0 (network) and .7 (broadcast)
|
||||
self.assertEqual(available.first_ip, '192.0.2.1/29')
|
||||
self.assertEqual(available.size, 6)
|
||||
|
||||
def test_annotate_ip_space_ipv4_pool_includes_network_and_broadcast(self):
|
||||
prefix = Prefix.objects.create(
|
||||
prefix=IPNetwork('192.0.2.8/29'), # 8 addresses total
|
||||
status='active',
|
||||
is_pool=True,
|
||||
)
|
||||
|
||||
data = annotate_ip_space(prefix)
|
||||
|
||||
self.assertEqual(len(data), 1)
|
||||
available = data[0]
|
||||
|
||||
# Pool: all addresses are usable, including network/broadcast
|
||||
self.assertEqual(available.first_ip, '192.0.2.8/29')
|
||||
self.assertEqual(available.size, 8)
|
||||
|
||||
def test_annotate_ip_space_ipv4_31_includes_all_ips(self):
|
||||
prefix = Prefix.objects.create(
|
||||
prefix=IPNetwork('192.0.2.16/31'), # 2 addresses total
|
||||
status='active',
|
||||
is_pool=False,
|
||||
)
|
||||
|
||||
data = annotate_ip_space(prefix)
|
||||
|
||||
self.assertEqual(len(data), 1)
|
||||
available = data[0]
|
||||
|
||||
# /31: fully usable
|
||||
self.assertEqual(available.first_ip, '192.0.2.16/31')
|
||||
self.assertEqual(available.size, 2)
|
||||
|
||||
def test_annotate_ip_space_ipv4_32_includes_single_ip(self):
|
||||
prefix = Prefix.objects.create(
|
||||
prefix=IPNetwork('192.0.2.100/32'), # 1 address total
|
||||
status='active',
|
||||
is_pool=False,
|
||||
)
|
||||
|
||||
data = annotate_ip_space(prefix)
|
||||
|
||||
self.assertEqual(len(data), 1)
|
||||
available = data[0]
|
||||
|
||||
# /32: single usable address
|
||||
self.assertEqual(available.first_ip, '192.0.2.100/32')
|
||||
self.assertEqual(available.size, 1)
|
||||
|
||||
def test_annotate_ip_space_ipv6_non_pool_excludes_anycast_first_ip(self):
|
||||
prefix = Prefix.objects.create(
|
||||
prefix=IPNetwork('2001:db8::/126'), # 4 addresses total
|
||||
status='active',
|
||||
is_pool=False,
|
||||
)
|
||||
|
||||
data = annotate_ip_space(prefix)
|
||||
|
||||
# No child records -> expect one AvailableIPSpace entry
|
||||
self.assertEqual(len(data), 1)
|
||||
available = data[0]
|
||||
|
||||
# For IPv6 non-pool prefixes (except /127-/128), the first address is reserved (subnet-router anycast)
|
||||
self.assertEqual(available.first_ip, '2001:db8::1/126')
|
||||
self.assertEqual(available.size, 3) # 4 total - 1 reserved anycast
|
||||
|
||||
def test_annotate_ip_space_ipv6_127_includes_all_ips(self):
|
||||
prefix = Prefix.objects.create(
|
||||
prefix=IPNetwork('2001:db8::/127'), # 2 addresses total
|
||||
status='active',
|
||||
is_pool=False,
|
||||
)
|
||||
|
||||
data = annotate_ip_space(prefix)
|
||||
|
||||
self.assertEqual(len(data), 1)
|
||||
available = data[0]
|
||||
|
||||
# /127 is fully usable (no anycast exclusion)
|
||||
self.assertEqual(available.first_ip, '2001:db8::/127')
|
||||
self.assertEqual(available.size, 2)
|
||||
|
||||
def test_annotate_ip_space_ipv6_128_includes_single_ip(self):
|
||||
prefix = Prefix.objects.create(
|
||||
prefix=IPNetwork('2001:db8::1/128'), # 1 address total
|
||||
status='active',
|
||||
is_pool=False,
|
||||
)
|
||||
|
||||
data = annotate_ip_space(prefix)
|
||||
|
||||
self.assertEqual(len(data), 1)
|
||||
available = data[0]
|
||||
|
||||
# /128 is fully usable (single host address)
|
||||
self.assertEqual(available.first_ip, '2001:db8::1/128')
|
||||
self.assertEqual(available.size, 1)
|
||||
|
||||
def test_annotate_ip_space_ipv6_pool_includes_anycast_first_ip(self):
|
||||
prefix = Prefix.objects.create(
|
||||
prefix=IPNetwork('2001:db8:1::/126'), # 4 addresses total
|
||||
status='active',
|
||||
is_pool=True,
|
||||
)
|
||||
|
||||
data = annotate_ip_space(prefix)
|
||||
|
||||
self.assertEqual(len(data), 1)
|
||||
available = data[0]
|
||||
|
||||
# Pools are fully usable
|
||||
self.assertEqual(available.first_ip, '2001:db8:1::/126')
|
||||
self.assertEqual(available.size, 4)
|
||||
|
||||
@@ -78,21 +78,12 @@ def annotate_ip_space(prefix):
|
||||
records = sorted(records, key=lambda x: x[0])
|
||||
|
||||
# Determine the first & last valid IP addresses in the prefix
|
||||
if (
|
||||
prefix.is_pool
|
||||
or (prefix.family == 4 and prefix.mask_length >= 31)
|
||||
or (prefix.family == 6 and prefix.mask_length >= 127)
|
||||
):
|
||||
# Pool, IPv4 /31-/32 or IPv6 /127-/128 sets are fully usable
|
||||
first_ip_in_prefix = netaddr.IPAddress(prefix.prefix.first)
|
||||
last_ip_in_prefix = netaddr.IPAddress(prefix.prefix.last)
|
||||
elif prefix.family == 4:
|
||||
if prefix.family == 4 and prefix.mask_length < 31 and not prefix.is_pool:
|
||||
# Ignore the network and broadcast addresses for non-pool IPv4 prefixes larger than /31
|
||||
first_ip_in_prefix = netaddr.IPAddress(prefix.prefix.first + 1)
|
||||
last_ip_in_prefix = netaddr.IPAddress(prefix.prefix.last - 1)
|
||||
else:
|
||||
# For IPv6 prefixes, omit the Subnet-Router anycast address (RFC 4291)
|
||||
first_ip_in_prefix = netaddr.IPAddress(prefix.prefix.first + 1)
|
||||
first_ip_in_prefix = netaddr.IPAddress(prefix.prefix.first)
|
||||
last_ip_in_prefix = netaddr.IPAddress(prefix.prefix.last)
|
||||
|
||||
if not records:
|
||||
|
||||
@@ -15,7 +15,6 @@ from strawberry_django import (
|
||||
DatetimeFilterLookup,
|
||||
FilterLookup,
|
||||
RangeLookup,
|
||||
StrFilterLookup,
|
||||
TimeFilterLookup,
|
||||
process_filters,
|
||||
)
|
||||
@@ -41,7 +40,7 @@ SKIP_MSG = 'Filter will be skipped on `null` value'
|
||||
|
||||
@strawberry.input(one_of=True, description='Lookup for JSON field. Only one of the lookup fields can be set.')
|
||||
class JSONLookup:
|
||||
string_lookup: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
string_lookup: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
int_range_lookup: RangeLookup[int] | None = strawberry_django.filter_field()
|
||||
int_comparison_lookup: ComparisonFilterLookup[int] | None = strawberry_django.filter_field()
|
||||
float_range_lookup: RangeLookup[float] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Annotated, TypeVar
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLookup, StrFilterLookup
|
||||
from strawberry_django import BaseFilterLookup, DatetimeFilterLookup, FilterLookup
|
||||
|
||||
__all__ = (
|
||||
'DistanceFilterMixin',
|
||||
@@ -48,7 +48,7 @@ class SyncedDataFilterMixin:
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
data_file_id: FilterLookup[int] | None = strawberry_django.filter_field()
|
||||
data_path: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
data_path: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
auto_sync_enabled: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
data_synced: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import strawberry_django
|
||||
from strawberry import ID
|
||||
from strawberry_django import ComparisonFilterLookup, StrFilterLookup
|
||||
from strawberry_django import ComparisonFilterLookup, FilterLookup
|
||||
|
||||
from core.graphql.filter_mixins import ChangeLoggingMixin
|
||||
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, JournalEntriesFilterMixin, TagsFilterMixin
|
||||
@@ -42,21 +42,21 @@ class NetBoxModelFilter(
|
||||
|
||||
@dataclass
|
||||
class NestedGroupModelFilter(NetBoxModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
parent_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
class OrganizationalModelFilter(NetBoxModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
comments: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
comments: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@dataclass
|
||||
class PrimaryModelFilter(NetBoxModelFilter):
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
comments: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
comments: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -28,7 +28,6 @@ registry = Registry({
|
||||
'denormalized_fields': collections.defaultdict(list),
|
||||
'event_types': dict(),
|
||||
'filtersets': dict(),
|
||||
'model_actions': collections.defaultdict(list),
|
||||
'model_features': dict(),
|
||||
'models': collections.defaultdict(set),
|
||||
'plugins': dict(),
|
||||
|
||||
2
netbox/project-static/dist/netbox.css
vendored
2
netbox/project-static/dist/netbox.css
vendored
File diff suppressed because one or more lines are too long
8
netbox/project-static/dist/netbox.js
vendored
8
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
8
netbox/project-static/dist/netbox.js.map
vendored
8
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -32,19 +32,19 @@
|
||||
"htmx.org": "2.0.8",
|
||||
"query-string": "9.3.1",
|
||||
"sass": "1.97.3",
|
||||
"tom-select": "2.5.2",
|
||||
"tom-select": "2.4.3",
|
||||
"typeface-inter": "3.18.1",
|
||||
"typeface-roboto-mono": "1.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^2.0.2",
|
||||
"@eslint/eslintrc": "^3.3.4",
|
||||
"@eslint/eslintrc": "^3.3.3",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@types/bootstrap": "5.2.10",
|
||||
"@types/cookie": "^1.0.0",
|
||||
"@types/node": "^24.10.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||
"@typescript-eslint/parser": "^8.56.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
||||
"@typescript-eslint/parser": "^8.56.0",
|
||||
"esbuild": "^0.27.3",
|
||||
"esbuild-sass-plugin": "^3.6.0",
|
||||
"eslint": "^9.39.2",
|
||||
@@ -52,7 +52,7 @@
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"globals": "^17.4.0",
|
||||
"globals": "^17.3.0",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import { initClearField } from './clearField';
|
||||
import { initFormElements } from './elements';
|
||||
import { initFilterModifiers } from './filterModifiers';
|
||||
import { initRegisteredActions } from './registeredActions';
|
||||
import { initSpeedSelector } from './speedSelector';
|
||||
|
||||
export function initForms(): void {
|
||||
for (const func of [
|
||||
initFormElements,
|
||||
initSpeedSelector,
|
||||
initFilterModifiers,
|
||||
initClearField,
|
||||
initRegisteredActions,
|
||||
]) {
|
||||
for (const func of [initFormElements, initSpeedSelector, initFilterModifiers, initClearField]) {
|
||||
func();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import { getElements } from '../util';
|
||||
|
||||
/**
|
||||
* Show/hide registered action checkboxes based on selected object_types.
|
||||
*/
|
||||
export function initRegisteredActions(): void {
|
||||
const actionsContainer = document.getElementById('id_registered_actions_container');
|
||||
const selectedList = document.getElementById('id_object_types_1') as HTMLSelectElement;
|
||||
|
||||
if (!actionsContainer || !selectedList) {
|
||||
return;
|
||||
}
|
||||
|
||||
function updateVisibility(): void {
|
||||
const selectedModels = new Set<string>();
|
||||
|
||||
// Get model keys from selected options
|
||||
for (const option of Array.from(selectedList.options)) {
|
||||
const modelKey = option.dataset.modelKey;
|
||||
if (modelKey) {
|
||||
selectedModels.add(modelKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide action groups
|
||||
const groups = actionsContainer!.querySelectorAll('.model-actions');
|
||||
let anyVisible = false;
|
||||
|
||||
groups.forEach(group => {
|
||||
const modelKey = group.getAttribute('data-model');
|
||||
const visible = modelKey !== null && selectedModels.has(modelKey);
|
||||
(group as HTMLElement).style.display = visible ? 'block' : 'none';
|
||||
if (visible) {
|
||||
anyVisible = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Show/hide "no actions" message
|
||||
const noActionsMsg = document.getElementById('no-custom-actions-message');
|
||||
if (noActionsMsg) {
|
||||
noActionsMsg.style.display = anyVisible ? 'none' : 'block';
|
||||
}
|
||||
|
||||
// Hide the entire field row when no actions are visible
|
||||
const fieldRow = actionsContainer!.closest('.field-row, .mb-3');
|
||||
if (fieldRow) {
|
||||
(fieldRow as HTMLElement).style.display = anyVisible ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Initial update
|
||||
updateVisibility();
|
||||
|
||||
// Listen to move button clicks
|
||||
for (const btn of getElements<HTMLButtonElement>('.move-option')) {
|
||||
btn.addEventListener('click', () => {
|
||||
// Wait for DOM update
|
||||
setTimeout(updateVisibility, 50);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { RecursivePartial, TomOption, TomSettings, TomInput } from 'tom-select/dist/cjs/types';
|
||||
import { RecursivePartial, TomOption, TomSettings } from 'tom-select/dist/types/types';
|
||||
import { TomInput } from 'tom-select/dist/cjs/types/core';
|
||||
import { addClasses } from 'tom-select/src/vanilla.ts';
|
||||
import queryString from 'query-string';
|
||||
import TomSelect from 'tom-select';
|
||||
|
||||
@@ -210,7 +210,7 @@
|
||||
dependencies:
|
||||
"@types/json-schema" "^7.0.15"
|
||||
|
||||
"@eslint/eslintrc@^3.3.1":
|
||||
"@eslint/eslintrc@^3.3.1", "@eslint/eslintrc@^3.3.3":
|
||||
version "3.3.3"
|
||||
resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz"
|
||||
integrity sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==
|
||||
@@ -225,21 +225,6 @@
|
||||
minimatch "^3.1.2"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@eslint/eslintrc@^3.3.4":
|
||||
version "3.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.4.tgz#e402b1920f7c1f5a15342caa432b1348cacbb641"
|
||||
integrity sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==
|
||||
dependencies:
|
||||
ajv "^6.14.0"
|
||||
debug "^4.3.2"
|
||||
espree "^10.0.1"
|
||||
globals "^14.0.0"
|
||||
ignore "^5.2.0"
|
||||
import-fresh "^3.2.1"
|
||||
js-yaml "^4.1.1"
|
||||
minimatch "^3.1.3"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@eslint/js@9.39.2", "@eslint/js@^9.39.2":
|
||||
version "9.39.2"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.2.tgz#2d4b8ec4c3ea13c1b3748e0c97ecd766bdd80599"
|
||||
@@ -950,100 +935,100 @@
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^8.56.1":
|
||||
version "8.56.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz#b1ce606d87221daec571e293009675992f0aae76"
|
||||
integrity sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==
|
||||
"@typescript-eslint/eslint-plugin@^8.56.0":
|
||||
version "8.56.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz#5aec3db807a6b8437ea5d5ebf7bd16b4119aba8d"
|
||||
integrity sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.12.2"
|
||||
"@typescript-eslint/scope-manager" "8.56.1"
|
||||
"@typescript-eslint/type-utils" "8.56.1"
|
||||
"@typescript-eslint/utils" "8.56.1"
|
||||
"@typescript-eslint/visitor-keys" "8.56.1"
|
||||
"@typescript-eslint/scope-manager" "8.56.0"
|
||||
"@typescript-eslint/type-utils" "8.56.0"
|
||||
"@typescript-eslint/utils" "8.56.0"
|
||||
"@typescript-eslint/visitor-keys" "8.56.0"
|
||||
ignore "^7.0.5"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.4.0"
|
||||
|
||||
"@typescript-eslint/parser@^8.56.1":
|
||||
version "8.56.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.56.1.tgz#21d13b3d456ffb08614c1d68bb9a4f8d9237cdc7"
|
||||
integrity sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==
|
||||
"@typescript-eslint/parser@^8.56.0":
|
||||
version "8.56.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.56.0.tgz#8ecff1678b8b1a742d29c446ccf5eeea7f971d72"
|
||||
integrity sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.56.1"
|
||||
"@typescript-eslint/types" "8.56.1"
|
||||
"@typescript-eslint/typescript-estree" "8.56.1"
|
||||
"@typescript-eslint/visitor-keys" "8.56.1"
|
||||
"@typescript-eslint/scope-manager" "8.56.0"
|
||||
"@typescript-eslint/types" "8.56.0"
|
||||
"@typescript-eslint/typescript-estree" "8.56.0"
|
||||
"@typescript-eslint/visitor-keys" "8.56.0"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/project-service@8.56.1":
|
||||
version "8.56.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.56.1.tgz#65c8d645f028b927bfc4928593b54e2ecd809244"
|
||||
integrity sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==
|
||||
"@typescript-eslint/project-service@8.56.0":
|
||||
version "8.56.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.56.0.tgz#bb8562fecd8f7922e676fc6a1189c20dd7991d73"
|
||||
integrity sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils" "^8.56.1"
|
||||
"@typescript-eslint/types" "^8.56.1"
|
||||
"@typescript-eslint/tsconfig-utils" "^8.56.0"
|
||||
"@typescript-eslint/types" "^8.56.0"
|
||||
debug "^4.4.3"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.56.1":
|
||||
version "8.56.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz#254df93b5789a871351335dd23e20bc164060f24"
|
||||
integrity sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==
|
||||
"@typescript-eslint/scope-manager@8.56.0":
|
||||
version "8.56.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz#604030a4c6433df3728effdd441d47f45a86edb4"
|
||||
integrity sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.56.1"
|
||||
"@typescript-eslint/visitor-keys" "8.56.1"
|
||||
"@typescript-eslint/types" "8.56.0"
|
||||
"@typescript-eslint/visitor-keys" "8.56.0"
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@8.56.1", "@typescript-eslint/tsconfig-utils@^8.56.1":
|
||||
version "8.56.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz#1afa830b0fada5865ddcabdc993b790114a879b7"
|
||||
integrity sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==
|
||||
"@typescript-eslint/tsconfig-utils@8.56.0", "@typescript-eslint/tsconfig-utils@^8.56.0":
|
||||
version "8.56.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz#2538ce83cbc376e685487960cbb24b65fe2abc4e"
|
||||
integrity sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==
|
||||
|
||||
"@typescript-eslint/type-utils@8.56.1":
|
||||
version "8.56.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz#7a6c4fabf225d674644931e004302cbbdd2f2e24"
|
||||
integrity sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==
|
||||
"@typescript-eslint/type-utils@8.56.0":
|
||||
version "8.56.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz#72b4edc1fc73988998f1632b3ec99c2a66eaac6e"
|
||||
integrity sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.56.1"
|
||||
"@typescript-eslint/typescript-estree" "8.56.1"
|
||||
"@typescript-eslint/utils" "8.56.1"
|
||||
"@typescript-eslint/types" "8.56.0"
|
||||
"@typescript-eslint/typescript-estree" "8.56.0"
|
||||
"@typescript-eslint/utils" "8.56.0"
|
||||
debug "^4.4.3"
|
||||
ts-api-utils "^2.4.0"
|
||||
|
||||
"@typescript-eslint/types@8.56.1", "@typescript-eslint/types@^8.56.1":
|
||||
version "8.56.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.56.1.tgz#975e5942bf54895291337c91b9191f6eb0632ab9"
|
||||
integrity sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==
|
||||
"@typescript-eslint/types@8.56.0", "@typescript-eslint/types@^8.56.0":
|
||||
version "8.56.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.56.0.tgz#a2444011b9a98ca13d70411d2cbfed5443b3526a"
|
||||
integrity sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.56.1":
|
||||
version "8.56.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz#3b9e57d8129a860c50864c42188f761bdef3eab0"
|
||||
integrity sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==
|
||||
"@typescript-eslint/typescript-estree@8.56.0":
|
||||
version "8.56.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz#fadbc74c14c5bac947db04980ff58bb178701c2e"
|
||||
integrity sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service" "8.56.1"
|
||||
"@typescript-eslint/tsconfig-utils" "8.56.1"
|
||||
"@typescript-eslint/types" "8.56.1"
|
||||
"@typescript-eslint/visitor-keys" "8.56.1"
|
||||
"@typescript-eslint/project-service" "8.56.0"
|
||||
"@typescript-eslint/tsconfig-utils" "8.56.0"
|
||||
"@typescript-eslint/types" "8.56.0"
|
||||
"@typescript-eslint/visitor-keys" "8.56.0"
|
||||
debug "^4.4.3"
|
||||
minimatch "^10.2.2"
|
||||
minimatch "^9.0.5"
|
||||
semver "^7.7.3"
|
||||
tinyglobby "^0.2.15"
|
||||
ts-api-utils "^2.4.0"
|
||||
|
||||
"@typescript-eslint/utils@8.56.1":
|
||||
version "8.56.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.56.1.tgz#5a86acaf9f1b4c4a85a42effb217f73059f6deb7"
|
||||
integrity sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==
|
||||
"@typescript-eslint/utils@8.56.0":
|
||||
version "8.56.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.56.0.tgz#063ce6f702ec603de1b83ee795ed5e877d6f7841"
|
||||
integrity sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.9.1"
|
||||
"@typescript-eslint/scope-manager" "8.56.1"
|
||||
"@typescript-eslint/types" "8.56.1"
|
||||
"@typescript-eslint/typescript-estree" "8.56.1"
|
||||
"@typescript-eslint/scope-manager" "8.56.0"
|
||||
"@typescript-eslint/types" "8.56.0"
|
||||
"@typescript-eslint/typescript-estree" "8.56.0"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.56.1":
|
||||
version "8.56.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz#50e03475c33a42d123dc99e63acf1841c0231f87"
|
||||
integrity sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==
|
||||
"@typescript-eslint/visitor-keys@8.56.0":
|
||||
version "8.56.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz#7d6592ab001827d3ce052155edf7ecad19688d7d"
|
||||
integrity sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.56.1"
|
||||
"@typescript-eslint/types" "8.56.0"
|
||||
eslint-visitor-keys "^5.0.0"
|
||||
|
||||
"@unrs/resolver-binding-android-arm-eabi@1.11.1":
|
||||
@@ -1163,16 +1148,6 @@ ajv@^6.12.4:
|
||||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
ajv@^6.14.0:
|
||||
version "6.14.0"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.14.0.tgz#fd067713e228210636ebb08c60bd3765d6dbe73a"
|
||||
integrity sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==
|
||||
dependencies:
|
||||
fast-deep-equal "^3.1.1"
|
||||
fast-json-stable-stringify "^2.0.0"
|
||||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
ansi-styles@^4.1.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz"
|
||||
@@ -1299,11 +1274,6 @@ balanced-match@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
balanced-match@^4.0.2:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a"
|
||||
integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==
|
||||
|
||||
bootstrap@5.3.7:
|
||||
version "5.3.7"
|
||||
resolved "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz"
|
||||
@@ -1322,12 +1292,12 @@ brace-expansion@^1.1.7:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
brace-expansion@^5.0.2:
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.4.tgz#614daaecd0a688f660bbbc909a8748c3d80d4336"
|
||||
integrity sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==
|
||||
brace-expansion@^2.0.1:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz"
|
||||
integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==
|
||||
dependencies:
|
||||
balanced-match "^4.0.2"
|
||||
balanced-match "^1.0.0"
|
||||
|
||||
braces@^3.0.3:
|
||||
version "3.0.3"
|
||||
@@ -2219,10 +2189,10 @@ globals@^14.0.0:
|
||||
resolved "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz"
|
||||
integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
|
||||
|
||||
globals@^17.4.0:
|
||||
version "17.4.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-17.4.0.tgz#33d7d297ed1536b388a0e2f4bcd0ff19c8ff91b5"
|
||||
integrity sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==
|
||||
globals@^17.3.0:
|
||||
version "17.3.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-17.3.0.tgz#8b96544c2fa91afada02747cc9731c002a96f3b9"
|
||||
integrity sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==
|
||||
|
||||
globalthis@^1.0.3, globalthis@^1.0.4:
|
||||
version "1.0.4"
|
||||
@@ -2814,13 +2784,6 @@ micromatch@^4.0.5:
|
||||
braces "^3.0.3"
|
||||
picomatch "^2.3.1"
|
||||
|
||||
minimatch@^10.2.2:
|
||||
version "10.2.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.4.tgz#465b3accbd0218b8281f5301e27cedc697f96fde"
|
||||
integrity sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==
|
||||
dependencies:
|
||||
brace-expansion "^5.0.2"
|
||||
|
||||
minimatch@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
|
||||
@@ -2828,12 +2791,12 @@ minimatch@^3.1.2:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimatch@^3.1.3:
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e"
|
||||
integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==
|
||||
minimatch@^9.0.5:
|
||||
version "9.0.5"
|
||||
resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz"
|
||||
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimist@^1.2.0, minimist@^1.2.6:
|
||||
version "1.2.8"
|
||||
@@ -3492,10 +3455,10 @@ toggle-selection@^1.0.6:
|
||||
resolved "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz"
|
||||
integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==
|
||||
|
||||
tom-select@2.5.2:
|
||||
version "2.5.2"
|
||||
resolved "https://registry.yarnpkg.com/tom-select/-/tom-select-2.5.2.tgz#77dd4bc780b1ea72905337b24f04ce19dc6d2ca1"
|
||||
integrity sha512-VAlGj5MBWVLMJje2NwA3XSmxa7CUFpp1tdzFZ8wymCkcLeP0NwF4ARmSuUK4BWbmSN1fETlSazWkMIxEpP4GdQ==
|
||||
tom-select@2.4.3:
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/tom-select/-/tom-select-2.4.3.tgz#1daa4131cd317de691f39eb5bf41148265986c1f"
|
||||
integrity sha512-MFFrMxP1bpnAMPbdvPCZk0KwYxLqhYZso39torcdoefeV/NThNyDu8dV96/INJ5XQVTL3O55+GqQ78Pkj5oCfw==
|
||||
dependencies:
|
||||
"@orchidjs/sifter" "^1.1.0"
|
||||
"@orchidjs/unicode-variants" "^1.1.2"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version: "4.5.4"
|
||||
version: "4.5.3"
|
||||
edition: "Community"
|
||||
published: "2026-03-03"
|
||||
published: "2026-02-17"
|
||||
|
||||
1
netbox/templates/users/attrs/date_joined.html
Normal file
1
netbox/templates/users/attrs/date_joined.html
Normal file
@@ -0,0 +1 @@
|
||||
{% load helpers %}{{ value|isodate }}
|
||||
1
netbox/templates/users/attrs/full_name.html
Normal file
1
netbox/templates/users/attrs/full_name.html
Normal file
@@ -0,0 +1 @@
|
||||
{% load helpers %}{{ object.get_full_name|placeholder }}
|
||||
1
netbox/templates/users/attrs/last_login.html
Normal file
1
netbox/templates/users/attrs/last_login.html
Normal file
@@ -0,0 +1 @@
|
||||
{% load helpers %}{{ value|isodatetime:"minutes"|placeholder }}
|
||||
@@ -1,60 +1,3 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load i18n %}
|
||||
{% load helpers %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block title %}{% trans "Group" %} {{ object.name }}{% endblock %}
|
||||
|
||||
{% block subtitle %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Group" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Users" %}</h2>
|
||||
<div class="list-group list-group-flush">
|
||||
{% for user in object.users.all %}
|
||||
<a href="{% url 'users:user' pk=user.pk %}" class="list-group-item list-group-item-action">{{ user }}</a>
|
||||
{% empty %}
|
||||
<div class="list-group-item text-muted">{% trans "None" %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Assigned Permissions" %}</h2>
|
||||
<div class="list-group list-group-flush">
|
||||
{% for perm in object.object_permissions.all %}
|
||||
<a href="{% url 'users:objectpermission' pk=perm.pk %}" class="list-group-item list-group-item-action">{{ perm }}</a>
|
||||
{% empty %}
|
||||
<div class="list-group-item text-muted">{% trans "None" %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Owner Membership" %}</h2>
|
||||
<div class="list-group list-group-flush">
|
||||
{% for owner in object.owners.all %}
|
||||
<a href="{% url 'users:owner' pk=owner.pk %}" class="list-group-item list-group-item-action">{{ owner }}</a>
|
||||
{% empty %}
|
||||
<div class="list-group-item text-muted">{% trans "None" %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,101 +1,3 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load i18n %}
|
||||
{% load helpers %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block title %}{% trans "Permission" %} {{ object.name }}{% endblock %}
|
||||
|
||||
{% block subtitle %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Permission" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Enabled" %}</th>
|
||||
<td>{% checkmark object.enabled %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Actions" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "View" %}</th>
|
||||
<td>{% checkmark object.can_view %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Add" %}</th>
|
||||
<td>{% checkmark object.can_add %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Change" %}</th>
|
||||
<td>{% checkmark object.can_change %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Delete" %}</th>
|
||||
<td>{% checkmark object.can_delete %}</td>
|
||||
</tr>
|
||||
{% for action in object.actions %}
|
||||
{% if action not in reserved_actions %}
|
||||
<tr>
|
||||
<th scope="row">{{ action }}</th>
|
||||
<td>{% checkmark True %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Constraints" %}</h2>
|
||||
<div class="card-body">
|
||||
{% if object.constraints %}
|
||||
<pre>{{ object.constraints|json }}</pre>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Object Types" %}</h2>
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for user in object.object_types.all %}
|
||||
<li class="list-group-item">{{ user }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Assigned Users" %}</h2>
|
||||
<div class="list-group list-group-flush">
|
||||
{% for user in object.users.all %}
|
||||
<a href="{% url 'users:user' pk=user.pk %}" class="list-group-item list-group-item-action">{{ user }}</a>
|
||||
{% empty %}
|
||||
<div class="list-group-item text-muted">{% trans "None" %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Assigned Groups" %}</h2>
|
||||
<div class="list-group list-group-flush">
|
||||
{% for group in object.groups.all %}
|
||||
<a href="{% url 'users:group' pk=group.pk %}" class="list-group-item list-group-item-action">{{ group }}</a>
|
||||
{% empty %}
|
||||
<div class="list-group-item text-muted">{% trans "None" %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -11,50 +11,3 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block subtitle %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Owner" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Group" %}</th>
|
||||
<td>{{ object.group|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Groups" %}</h2>
|
||||
<div class="list-group list-group-flush">
|
||||
{% for group in object.user_groups.all %}
|
||||
<a href="{% url 'users:group' pk=group.pk %}" class="list-group-item list-group-item-action">{{ group }}</a>
|
||||
{% empty %}
|
||||
<div class="list-group-item text-muted">{% trans "None" %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Users" %}</h2>
|
||||
<div class="list-group list-group-flush">
|
||||
{% for user in object.users.all %}
|
||||
<a href="{% url 'users:user' pk=user.pk %}" class="list-group-item list-group-item-action">{{ user }}</a>
|
||||
{% empty %}
|
||||
<div class="list-group-item text-muted">{% trans "None" %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' with filter_name='owner_id' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,46 +1,3 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load i18n %}
|
||||
{% load helpers %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block subtitle %}{% endblock %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{% if perms.users.add_owner %}
|
||||
<a href="{% url 'users:owner_add' %}?group={{ object.pk }}" class="btn btn-primary">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Owner" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Group" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Members" %}</h2>
|
||||
<div class="list-group list-group-flush">
|
||||
{% for owner in object.members.all %}
|
||||
<a href="{% url 'users:owner' pk=owner.pk %}" class="list-group-item list-group-item-action">{{ owner }}</a>
|
||||
{% empty %}
|
||||
<div class="list-group-item text-muted">{% trans "None" %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
11
netbox/templates/users/panels/object_types.html
Normal file
11
netbox/templates/users/panels/object_types.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{% load i18n %}
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Object Types" %}</h2>
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for object_type in object.object_types.all %}
|
||||
<li class="list-group-item">{{ object_type }}</li>
|
||||
{% empty %}
|
||||
<li class="list-group-item text-muted">{% trans "None" %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -1,4 +1,3 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Token" %} {{ object }}{% endblock %}
|
||||
{% block subtitle %}{% endblock %}
|
||||
|
||||
@@ -1,85 +1,3 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "User" %} {{ object.username }}{% endblock %}
|
||||
|
||||
{% block subtitle %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "User" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Username" %}</th>
|
||||
<td>{{ object.username }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Full Name" %}</th>
|
||||
<td>{{ object.get_full_name|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Email" %}</th>
|
||||
<td>{{ object.email|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Account Created" %}</th>
|
||||
<td>{{ object.date_joined|isodate }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Last Login" %}</th>
|
||||
<td>{{ object.last_login|isodatetime:"minutes"|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Active" %}</th>
|
||||
<td>{% checkmark object.is_active %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Superuser" %}</th>
|
||||
<td>{% checkmark object.is_superuser %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Assigned Groups" %}</h2>
|
||||
<div class="list-group list-group-flush">
|
||||
{% for group in object.groups.all %}
|
||||
<a href="{% url 'users:group' pk=group.pk %}" class="list-group-item list-group-item-action">{{ group }}</a>
|
||||
{% empty %}
|
||||
<div class="list-group-item text-muted">{% trans "None" %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Assigned Permissions" %}</h2>
|
||||
<div class="list-group list-group-flush">
|
||||
{% for perm in object.object_permissions.all %}
|
||||
<a href="{% url 'users:objectpermission' pk=perm.pk %}" class="list-group-item list-group-item-action">{{ perm }}</a>
|
||||
{% empty %}
|
||||
<div class="list-group-item text-muted">{% trans "None" %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Owner Membership" %}</h2>
|
||||
<div class="list-group list-group-flush">
|
||||
{% for owner in object.owners.all %}
|
||||
<a href="{% url 'users:owner' pk=owner.pk %}" class="list-group-item list-group-item-action">{{ owner }}</a>
|
||||
{% empty %}
|
||||
<div class="list-group-item text-muted">{% trans "None" %}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if perms.core.view_objectchange %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'users/inc/user_activity.html' with user=object table=changelog_table %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Annotated
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry.scalars import ID
|
||||
from strawberry_django import BaseFilterLookup, StrFilterLookup
|
||||
from strawberry_django import BaseFilterLookup, FilterLookup
|
||||
|
||||
from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin
|
||||
from netbox.graphql.filters import (
|
||||
@@ -60,8 +60,8 @@ __all__ = (
|
||||
|
||||
@strawberry_django.filter_type(models.Tenant, lookups=True)
|
||||
class TenantFilter(ContactFilterMixin, PrimaryModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
slug: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
group: Annotated['TenantGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -153,12 +153,12 @@ class TenantGroupFilter(OrganizationalModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.Contact, lookups=True)
|
||||
class ContactFilter(PrimaryModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
title: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
phone: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
email: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
address: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
link: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
title: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
phone: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
email: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
link: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
groups: Annotated['ContactGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-03 05:20+0000\n"
|
||||
"POT-Creation-Date: 2026-02-28 05:11+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -13824,17 +13824,17 @@ msgstr ""
|
||||
msgid "Not Connected"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/dcim/device/attrs/ipaddress.html:5
|
||||
#: netbox/templates/dcim/device/attrs/ipaddress.html:4
|
||||
#: netbox/templates/virtualization/virtualmachine/attrs/ipaddress.html:4
|
||||
msgid "NAT for"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/dcim/device/attrs/ipaddress.html:7
|
||||
#: netbox/templates/dcim/device/attrs/ipaddress.html:6
|
||||
#: netbox/templates/virtualization/virtualmachine/attrs/ipaddress.html:6
|
||||
msgid "NAT"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/templates/dcim/device/attrs/ipaddress.html:10
|
||||
#: netbox/templates/dcim/device/attrs/ipaddress.html:8
|
||||
#: netbox/templates/ui/actions/copy_content.html:2
|
||||
#: netbox/templates/ui/attrs/numeric.html:9
|
||||
#: netbox/templates/ui/attrs/text.html:4
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -10,10 +10,6 @@ OBJECTPERMISSION_OBJECT_TYPES = (
|
||||
|
||||
CONSTRAINT_TOKEN_USER = '$user'
|
||||
|
||||
# Built-in actions that receive special handling (dedicated checkboxes, model properties)
|
||||
# and should not be registered as custom model actions.
|
||||
RESERVED_ACTIONS = ('view', 'add', 'change', 'delete')
|
||||
|
||||
# API tokens
|
||||
TOKEN_PREFIX = 'nbt_' # Used for v2 tokens only
|
||||
TOKEN_KEY_LENGTH = 12
|
||||
|
||||
@@ -14,7 +14,6 @@ from ipam.formfields import IPNetworkFormField
|
||||
from ipam.validators import prefix_validator
|
||||
from netbox.config import get_config
|
||||
from netbox.preferences import PREFERENCES
|
||||
from netbox.registry import registry
|
||||
from users.choices import TokenVersionChoices
|
||||
from users.constants import *
|
||||
from users.models import *
|
||||
@@ -26,7 +25,7 @@ from utilities.forms.fields import (
|
||||
JSONField,
|
||||
)
|
||||
from utilities.forms.rendering import FieldSet
|
||||
from utilities.forms.widgets import DateTimePicker, ObjectTypeSplitMultiSelectWidget, RegisteredActionsWidget
|
||||
from utilities.forms.widgets import DateTimePicker, SplitMultiSelectWidget
|
||||
from utilities.permissions import qs_filter_from_constraints
|
||||
from utilities.string import title
|
||||
|
||||
@@ -326,7 +325,7 @@ class ObjectPermissionForm(forms.ModelForm):
|
||||
object_types = ContentTypeMultipleChoiceField(
|
||||
label=_('Object types'),
|
||||
queryset=ObjectType.objects.all(),
|
||||
widget=ObjectTypeSplitMultiSelectWidget(
|
||||
widget=SplitMultiSelectWidget(
|
||||
choices=get_object_types_choices
|
||||
),
|
||||
help_text=_('Select the types of objects to which the permission will apply.')
|
||||
@@ -343,11 +342,6 @@ class ObjectPermissionForm(forms.ModelForm):
|
||||
can_delete = forms.BooleanField(
|
||||
required=False
|
||||
)
|
||||
registered_actions = forms.MultipleChoiceField(
|
||||
required=False,
|
||||
widget=RegisteredActionsWidget(),
|
||||
label=_('Custom actions'),
|
||||
)
|
||||
actions = SimpleArrayField(
|
||||
label=_('Additional actions'),
|
||||
base_field=forms.CharField(),
|
||||
@@ -376,11 +370,8 @@ class ObjectPermissionForm(forms.ModelForm):
|
||||
|
||||
fieldsets = (
|
||||
FieldSet('name', 'description', 'enabled'),
|
||||
FieldSet('can_view', 'can_add', 'can_change', 'can_delete', 'actions', name=_('Actions')),
|
||||
FieldSet('object_types', name=_('Objects')),
|
||||
FieldSet(
|
||||
'can_view', 'can_add', 'can_change', 'can_delete', 'registered_actions', 'actions',
|
||||
name=_('Actions')
|
||||
),
|
||||
FieldSet('groups', 'users', name=_('Assignment')),
|
||||
FieldSet('constraints', name=_('Constraints')),
|
||||
)
|
||||
@@ -394,22 +385,6 @@ class ObjectPermissionForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Build PK to model key mapping for object_types widget
|
||||
pk_to_model_key = {
|
||||
ot.pk: f'{ot.app_label}.{ot.model}'
|
||||
for ot in ObjectType.objects.filter(OBJECTPERMISSION_OBJECT_TYPES)
|
||||
}
|
||||
self.fields['object_types'].widget.set_model_key_map(pk_to_model_key)
|
||||
|
||||
# Configure registered_actions widget and field choices
|
||||
model_actions = dict(registry['model_actions'])
|
||||
self.fields['registered_actions'].widget.model_actions = model_actions
|
||||
choices = []
|
||||
for model_key, actions in model_actions.items():
|
||||
for action in actions:
|
||||
choices.append((f'{model_key}.{action.name}', action.name))
|
||||
self.fields['registered_actions'].choices = choices
|
||||
|
||||
# Make the actions field optional since the form uses it only for non-CRUD actions
|
||||
self.fields['actions'].required = False
|
||||
|
||||
@@ -419,31 +394,11 @@ class ObjectPermissionForm(forms.ModelForm):
|
||||
self.fields['groups'].initial = self.instance.groups.values_list('id', flat=True)
|
||||
self.fields['users'].initial = self.instance.users.values_list('id', flat=True)
|
||||
|
||||
# Work with a copy to avoid mutating the instance
|
||||
remaining_actions = list(self.instance.actions)
|
||||
|
||||
# Check the appropriate CRUD checkboxes
|
||||
for action in RESERVED_ACTIONS:
|
||||
if action in remaining_actions:
|
||||
# Check the appropriate checkboxes when editing an existing ObjectPermission
|
||||
for action in ['view', 'add', 'change', 'delete']:
|
||||
if action in self.instance.actions:
|
||||
self.fields[f'can_{action}'].initial = True
|
||||
remaining_actions.remove(action)
|
||||
|
||||
# Pre-select registered actions
|
||||
selected_registered = []
|
||||
consumed_actions = set()
|
||||
for ct in self.instance.object_types.all():
|
||||
model_key = f'{ct.app_label}.{ct.model}'
|
||||
if model_key in model_actions:
|
||||
for ma in model_actions[model_key]:
|
||||
if ma.name in remaining_actions:
|
||||
selected_registered.append(f'{model_key}.{ma.name}')
|
||||
consumed_actions.add(ma.name)
|
||||
self.fields['registered_actions'].initial = selected_registered
|
||||
|
||||
# Remaining actions go to the additional actions field
|
||||
self.initial['actions'] = [
|
||||
a for a in remaining_actions if a not in consumed_actions
|
||||
]
|
||||
self.instance.actions.remove(action)
|
||||
|
||||
# Populate initial data for a new ObjectPermission
|
||||
elif self.initial:
|
||||
@@ -453,7 +408,7 @@ class ObjectPermissionForm(forms.ModelForm):
|
||||
if isinstance(self.initial['actions'], str):
|
||||
self.initial['actions'] = [self.initial['actions']]
|
||||
if cloned_actions := self.initial['actions']:
|
||||
for action in RESERVED_ACTIONS:
|
||||
for action in ['view', 'add', 'change', 'delete']:
|
||||
if action in cloned_actions:
|
||||
self.fields[f'can_{action}'].initial = True
|
||||
self.initial['actions'].remove(action)
|
||||
@@ -465,38 +420,15 @@ class ObjectPermissionForm(forms.ModelForm):
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
object_types = self.cleaned_data.get('object_types', [])
|
||||
registered_actions = self.cleaned_data.get('registered_actions', [])
|
||||
object_types = self.cleaned_data.get('object_types')
|
||||
constraints = self.cleaned_data.get('constraints')
|
||||
|
||||
# Build set of selected model keys for validation
|
||||
selected_models = {f'{ct.app_label}.{ct.model}' for ct in object_types}
|
||||
|
||||
# Validate registered actions match selected object_types and collect action names
|
||||
final_actions = []
|
||||
for action_key in registered_actions:
|
||||
model_key, action_name = action_key.rsplit('.', 1)
|
||||
if model_key not in selected_models:
|
||||
raise forms.ValidationError({
|
||||
'registered_actions': _(
|
||||
'Action "{action}" is for {model} which is not selected.'
|
||||
).format(action=action_name, model=model_key)
|
||||
})
|
||||
if action_name not in final_actions:
|
||||
final_actions.append(action_name)
|
||||
|
||||
# Append any of the selected CRUD checkboxes to the actions list
|
||||
for action in RESERVED_ACTIONS:
|
||||
if self.cleaned_data.get(f'can_{action}') and action not in final_actions:
|
||||
final_actions.append(action)
|
||||
|
||||
# Add additional/manual actions
|
||||
if additional_actions := self.cleaned_data.get('actions'):
|
||||
for action in additional_actions:
|
||||
if action not in final_actions:
|
||||
final_actions.append(action)
|
||||
|
||||
self.cleaned_data['actions'] = final_actions
|
||||
if not self.cleaned_data.get('actions'):
|
||||
self.cleaned_data['actions'] = list()
|
||||
for action in ['view', 'add', 'change', 'delete']:
|
||||
if self.cleaned_data[f'can_{action}'] and action not in self.cleaned_data['actions']:
|
||||
self.cleaned_data['actions'].append(action)
|
||||
|
||||
# At least one action must be specified
|
||||
if not self.cleaned_data['actions']:
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Annotated
|
||||
|
||||
import strawberry
|
||||
import strawberry_django
|
||||
from strawberry_django import DatetimeFilterLookup, FilterLookup, StrFilterLookup
|
||||
from strawberry_django import DatetimeFilterLookup, FilterLookup
|
||||
|
||||
from netbox.graphql.filters import BaseModelFilter
|
||||
from users import models
|
||||
@@ -18,16 +18,16 @@ __all__ = (
|
||||
|
||||
@strawberry_django.filter_type(models.Group, lookups=True)
|
||||
class GroupFilter(BaseModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
|
||||
@strawberry_django.filter_type(models.User, lookups=True)
|
||||
class UserFilter(BaseModelFilter):
|
||||
username: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
first_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
last_name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
email: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
username: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
first_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
last_name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
email: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
is_superuser: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
is_active: FilterLookup[bool] | None = strawberry_django.filter_field()
|
||||
date_joined: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field()
|
||||
@@ -37,8 +37,8 @@ class UserFilter(BaseModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.Owner, lookups=True)
|
||||
class OwnerFilter(BaseModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
group: Annotated['OwnerGroupFilter', strawberry.lazy('users.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
@@ -50,5 +50,5 @@ class OwnerFilter(BaseModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.OwnerGroup, lookups=True)
|
||||
class OwnerGroupFilter(BaseModelFilter):
|
||||
name: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: StrFilterLookup[str] | None = strawberry_django.filter_field()
|
||||
name: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
description: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -23,3 +23,46 @@ class TokenExamplePanel(panels.Panel):
|
||||
actions = [
|
||||
actions.CopyContent('token-example')
|
||||
]
|
||||
|
||||
|
||||
class UserPanel(panels.ObjectAttributesPanel):
|
||||
username = attrs.TextAttr('username')
|
||||
full_name = attrs.TemplatedAttr(
|
||||
'get_full_name',
|
||||
label=_('Full name'),
|
||||
template_name='users/attrs/full_name.html',
|
||||
)
|
||||
email = attrs.TextAttr('email')
|
||||
date_joined = attrs.TemplatedAttr(
|
||||
'date_joined',
|
||||
label=_('Account created'),
|
||||
template_name='users/attrs/date_joined.html',
|
||||
)
|
||||
last_login = attrs.TemplatedAttr(
|
||||
'last_login',
|
||||
label=_('Last login'),
|
||||
template_name='users/attrs/last_login.html',
|
||||
)
|
||||
is_active = attrs.BooleanAttr('is_active', label=_('Active'))
|
||||
is_superuser = attrs.BooleanAttr('is_superuser', label=_('Superuser'))
|
||||
|
||||
|
||||
class ObjectPermissionPanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
description = attrs.TextAttr('description')
|
||||
enabled = attrs.BooleanAttr('enabled')
|
||||
|
||||
|
||||
class ObjectPermissionActionsPanel(panels.ObjectAttributesPanel):
|
||||
title = _('Actions')
|
||||
|
||||
can_view = attrs.BooleanAttr('can_view', label=_('View'))
|
||||
can_add = attrs.BooleanAttr('can_add', label=_('Add'))
|
||||
can_change = attrs.BooleanAttr('can_change', label=_('Change'))
|
||||
can_delete = attrs.BooleanAttr('can_delete', label=_('Delete'))
|
||||
|
||||
|
||||
class OwnerPanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
group = attrs.RelatedObjectAttr('group', linkify=True)
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
from django.db.models import Count
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import ObjectChange
|
||||
from core.tables import ObjectChangeTable
|
||||
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename
|
||||
from netbox.ui import layout
|
||||
from netbox.ui import actions, layout
|
||||
from netbox.ui.panels import (
|
||||
ContextTablePanel,
|
||||
JSONPanel,
|
||||
ObjectsTablePanel,
|
||||
OrganizationalObjectPanel,
|
||||
RelatedObjectsPanel,
|
||||
TemplatePanel,
|
||||
)
|
||||
from netbox.views import generic
|
||||
from users.ui import panels
|
||||
from utilities.query import count_related
|
||||
from utilities.views import GetRelatedModelsMixin, register_model_view
|
||||
|
||||
from . import filtersets, forms, tables
|
||||
from .constants import RESERVED_ACTIONS
|
||||
from .models import Group, ObjectPermission, Owner, OwnerGroup, Token, User
|
||||
|
||||
#
|
||||
@@ -87,7 +95,40 @@ class UserListView(generic.ObjectListView):
|
||||
@register_model_view(User)
|
||||
class UserView(generic.ObjectView):
|
||||
queryset = User.objects.all()
|
||||
template_name = 'users/user.html'
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.UserPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
ObjectsTablePanel(
|
||||
'users.Group', title=_('Assigned Groups'), filters={'user_id': lambda ctx: ctx['object'].pk}
|
||||
),
|
||||
ObjectsTablePanel(
|
||||
'users.ObjectPermission',
|
||||
title=_('Assigned Permissions'),
|
||||
filters={'user_id': lambda ctx: ctx['object'].pk},
|
||||
),
|
||||
ObjectsTablePanel(
|
||||
'users.Owner', title=_('Owner Membership'), filters={'user_id': lambda ctx: ctx['object'].pk}
|
||||
),
|
||||
],
|
||||
bottom_panels=[
|
||||
ContextTablePanel(
|
||||
'changelog_table',
|
||||
title=_('Recent Activity'),
|
||||
actions=[
|
||||
actions.LinkAction(
|
||||
view_name='core:objectchange_list',
|
||||
url_params={'user_id': lambda ctx: ctx['object'].pk},
|
||||
label=_('View All'),
|
||||
button_class='ghost-primary',
|
||||
button_icon='arrow-right-thick',
|
||||
permissions=['core.view_objectchange'],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(user=instance)[:20]
|
||||
@@ -155,7 +196,22 @@ class GroupListView(generic.ObjectListView):
|
||||
@register_model_view(Group)
|
||||
class GroupView(generic.ObjectView):
|
||||
queryset = Group.objects.all()
|
||||
template_name = 'users/group.html'
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
OrganizationalObjectPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
ObjectsTablePanel('users.User', filters={'group_id': lambda ctx: ctx['object'].pk}),
|
||||
ObjectsTablePanel(
|
||||
'users.ObjectPermission',
|
||||
title=_('Assigned Permissions'),
|
||||
filters={'group_id': lambda ctx: ctx['object'].pk},
|
||||
),
|
||||
ObjectsTablePanel(
|
||||
'users.Owner', title=_('Owner Membership'), filters={'user_group_id': lambda ctx: ctx['object'].pk}
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(Group, 'add', detail=False)
|
||||
@@ -213,12 +269,22 @@ class ObjectPermissionListView(generic.ObjectListView):
|
||||
@register_model_view(ObjectPermission)
|
||||
class ObjectPermissionView(generic.ObjectView):
|
||||
queryset = ObjectPermission.objects.all()
|
||||
template_name = 'users/objectpermission.html'
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'reserved_actions': RESERVED_ACTIONS,
|
||||
}
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.ObjectPermissionPanel(),
|
||||
panels.ObjectPermissionActionsPanel(),
|
||||
JSONPanel('constraints', title=_('Constraints')),
|
||||
],
|
||||
right_panels=[
|
||||
TemplatePanel('users/panels/object_types.html'),
|
||||
ObjectsTablePanel(
|
||||
'users.User', title=_('Assigned Users'), filters={'permission_id': lambda ctx: ctx['object'].pk}
|
||||
),
|
||||
ObjectsTablePanel(
|
||||
'users.Group', title=_('Assigned Groups'), filters={'permission_id': lambda ctx: ctx['object'].pk}
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(ObjectPermission, 'add', detail=False)
|
||||
@@ -261,7 +327,7 @@ class ObjectPermissionBulkDeleteView(generic.BulkDeleteView):
|
||||
@register_model_view(OwnerGroup, 'list', path='', detail=False)
|
||||
class OwnerGroupListView(generic.ObjectListView):
|
||||
queryset = OwnerGroup.objects.annotate(
|
||||
owner_count=count_related(Owner, 'group')
|
||||
owner_count=count_related(Owner, 'group')
|
||||
)
|
||||
filterset = filtersets.OwnerGroupFilterSet
|
||||
filterset_form = forms.OwnerGroupFilterForm
|
||||
@@ -269,14 +335,26 @@ class OwnerGroupListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(OwnerGroup)
|
||||
class OwnerGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
class OwnerGroupView(generic.ObjectView):
|
||||
queryset = OwnerGroup.objects.all()
|
||||
template_name = 'users/ownergroup.html'
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
OrganizationalObjectPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
ObjectsTablePanel(
|
||||
'users.Owner',
|
||||
filters={'group_id': lambda ctx: ctx['object'].pk},
|
||||
title=_('Members'),
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'users.Owner',
|
||||
url_params={'group': lambda ctx: ctx['object'].pk},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(OwnerGroup, 'add', detail=False)
|
||||
@@ -332,7 +410,19 @@ class OwnerListView(generic.ObjectListView):
|
||||
@register_model_view(Owner)
|
||||
class OwnerView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = Owner.objects.all()
|
||||
# Custom template is required to override the breadcrumbs block and include
|
||||
# the owner's group in the breadcrumb trail when present.
|
||||
template_name = 'users/owner.html'
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.OwnerPanel(),
|
||||
ObjectsTablePanel('users.Group', filters={'owner_id': lambda ctx: ctx['object'].pk}),
|
||||
ObjectsTablePanel('users.User', filters={'owner_id': lambda ctx: ctx['object'].pk}),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from .actions import *
|
||||
from .apiselect import *
|
||||
from .datetime import *
|
||||
from .misc import *
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
from django import forms
|
||||
from django.apps import apps
|
||||
|
||||
__all__ = (
|
||||
'RegisteredActionsWidget',
|
||||
)
|
||||
|
||||
|
||||
class RegisteredActionsWidget(forms.CheckboxSelectMultiple):
|
||||
"""
|
||||
Widget rendering checkboxes for registered model actions.
|
||||
Groups actions by model with data attributes for JS show/hide.
|
||||
"""
|
||||
template_name = 'widgets/registered_actions.html'
|
||||
|
||||
def __init__(self, *args, model_actions=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.model_actions = model_actions or {}
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super().get_context(name, value, attrs)
|
||||
|
||||
model_actions_with_labels = {}
|
||||
for model_key, actions in self.model_actions.items():
|
||||
app_label, model_name = model_key.split('.')
|
||||
try:
|
||||
model = apps.get_model(app_label, model_name)
|
||||
app_config = apps.get_app_config(app_label)
|
||||
label = f"{app_config.verbose_name} | {model._meta.verbose_name.title()}"
|
||||
except LookupError:
|
||||
label = model_key
|
||||
model_actions_with_labels[model_key] = {
|
||||
'label': label,
|
||||
'actions': actions,
|
||||
}
|
||||
|
||||
context['widget']['model_actions'] = model_actions_with_labels
|
||||
context['widget']['value'] = value or []
|
||||
return context
|
||||
@@ -9,7 +9,6 @@ __all__ = (
|
||||
'ClearableSelect',
|
||||
'ColorSelect',
|
||||
'HTMXSelect',
|
||||
'ObjectTypeSplitMultiSelectWidget',
|
||||
'SelectWithPK',
|
||||
'SplitMultiSelectWidget',
|
||||
)
|
||||
@@ -151,16 +150,14 @@ class SplitMultiSelectWidget(forms.MultiWidget):
|
||||
be enabled only if the order of the selected choices is significant.
|
||||
"""
|
||||
template_name = 'widgets/splitmultiselect.html'
|
||||
available_widget_class = AvailableOptions
|
||||
selected_widget_class = SelectedOptions
|
||||
|
||||
def __init__(self, choices, attrs=None, ordering=False):
|
||||
widgets = [
|
||||
self.available_widget_class(
|
||||
AvailableOptions(
|
||||
attrs={'size': 8},
|
||||
choices=choices
|
||||
),
|
||||
self.selected_widget_class(
|
||||
SelectedOptions(
|
||||
attrs={'size': 8, 'class': 'select-all'},
|
||||
choices=choices
|
||||
),
|
||||
@@ -183,48 +180,3 @@ class SplitMultiSelectWidget(forms.MultiWidget):
|
||||
def value_from_datadict(self, data, files, name):
|
||||
# Return only the choices from the SelectedOptions widget
|
||||
return super().value_from_datadict(data, files, name)[1]
|
||||
|
||||
|
||||
#
|
||||
# ObjectType-specific widgets for ObjectPermissionForm
|
||||
#
|
||||
|
||||
class ObjectTypeSelectMultiple(SelectMultipleBase):
|
||||
"""
|
||||
SelectMultiple that adds data-model-key attribute to options for JS targeting.
|
||||
"""
|
||||
pk_to_model_key = None
|
||||
|
||||
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
|
||||
option = super().create_option(name, value, label, selected, index, subindex, attrs)
|
||||
if self.pk_to_model_key:
|
||||
model_key = self.pk_to_model_key.get(value) or self.pk_to_model_key.get(str(value))
|
||||
if model_key:
|
||||
option['attrs']['data-model-key'] = model_key
|
||||
return option
|
||||
|
||||
|
||||
class ObjectTypeAvailableOptions(ObjectTypeSelectMultiple):
|
||||
include_selected = False
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super().get_context(name, value, attrs)
|
||||
context['widget']['attrs']['required'] = False
|
||||
return context
|
||||
|
||||
|
||||
class ObjectTypeSelectedOptions(ObjectTypeSelectMultiple):
|
||||
include_selected = True
|
||||
|
||||
|
||||
class ObjectTypeSplitMultiSelectWidget(SplitMultiSelectWidget):
|
||||
"""
|
||||
SplitMultiSelectWidget that adds data-model-key attributes to options.
|
||||
Used by ObjectPermissionForm to enable JS show/hide of custom actions.
|
||||
"""
|
||||
available_widget_class = ObjectTypeAvailableOptions
|
||||
selected_widget_class = ObjectTypeSelectedOptions
|
||||
|
||||
def set_model_key_map(self, pk_to_model_key):
|
||||
for widget in self.widgets:
|
||||
widget.pk_to_model_key = pk_to_model_key
|
||||
|
||||
@@ -1,64 +1,19 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.db.models import Model, Q
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.registry import registry
|
||||
from users.constants import CONSTRAINT_TOKEN_USER, RESERVED_ACTIONS
|
||||
from users.constants import CONSTRAINT_TOKEN_USER
|
||||
|
||||
__all__ = (
|
||||
'ModelAction',
|
||||
'get_permission_for_model',
|
||||
'permission_is_exempt',
|
||||
'qs_filter_from_constraints',
|
||||
'register_model_actions',
|
||||
'resolve_permission',
|
||||
'resolve_permission_type',
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelAction:
|
||||
"""
|
||||
Represents a custom permission action for a model.
|
||||
|
||||
Attributes:
|
||||
name: The action identifier (e.g. 'sync', 'render_config')
|
||||
help_text: Optional description displayed in the ObjectPermission form
|
||||
"""
|
||||
name: str
|
||||
help_text: str = ''
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, ModelAction):
|
||||
return self.name == other.name
|
||||
return self.name == other
|
||||
|
||||
|
||||
def register_model_actions(model: type[Model], actions: list[ModelAction | str]):
|
||||
"""
|
||||
Register custom permission actions for a model. These actions will appear as
|
||||
checkboxes in the ObjectPermission form when the model is selected.
|
||||
|
||||
Args:
|
||||
model: The model class to register actions for
|
||||
actions: A list of ModelAction instances or action name strings
|
||||
"""
|
||||
label = f'{model._meta.app_label}.{model._meta.model_name}'
|
||||
for action in actions:
|
||||
if isinstance(action, str):
|
||||
action = ModelAction(name=action)
|
||||
if action.name in RESERVED_ACTIONS:
|
||||
raise ValueError(f"'{action.name}' is a reserved action and cannot be registered.")
|
||||
if action not in registry['model_actions'][label]:
|
||||
registry['model_actions'][label].append(action)
|
||||
|
||||
|
||||
def get_permission_for_model(model, action):
|
||||
"""
|
||||
Resolve the named permission for a given model (or instance) and action (e.g. view or add).
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
{% load i18n %}
|
||||
<div class="registered-actions-container" id="id_registered_actions_container">
|
||||
{% for model_key, model_data in widget.model_actions.items %}
|
||||
<div class="model-actions" data-model="{{ model_key }}" style="display: none;">
|
||||
<h5 class="mb-2 mt-3">{{ model_data.label }}</h5>
|
||||
{% for action in model_data.actions %}
|
||||
<div class="form-check">
|
||||
<input type="checkbox"
|
||||
class="form-check-input"
|
||||
name="{{ widget.name }}"
|
||||
value="{{ model_key }}.{{ action.name }}"
|
||||
id="id_{{ widget.name }}_{{ forloop.parentloop.counter }}_{{ forloop.counter }}"
|
||||
{% if model_key|add:"."|add:action.name in widget.value %}checked{% endif %}>
|
||||
<label class="form-check-label" for="id_{{ widget.name }}_{{ forloop.parentloop.counter }}_{{ forloop.counter }}">
|
||||
{{ action.name }}
|
||||
{% if action.help_text %}
|
||||
<small class="text-muted ms-1">{{ action.help_text }}</small>
|
||||
{% endif %}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="text-muted" id="no-custom-actions-message">
|
||||
{% trans "No custom actions registered." %}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user