mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-16 04:28:17 -06:00
Merge branch 'feature' into 15574-graphql-filter
This commit is contained in:
commit
36e30e4491
@ -19,9 +19,8 @@ django-filter
|
|||||||
django-htmx
|
django-htmx
|
||||||
|
|
||||||
# Modified Preorder Tree Traversal (recursive nesting of objects)
|
# Modified Preorder Tree Traversal (recursive nesting of objects)
|
||||||
# Pinned to 0.14.0; 0.15.0 requires Python 3.9+
|
|
||||||
# https://github.com/django-mptt/django-mptt/blob/main/CHANGELOG.rst
|
# https://github.com/django-mptt/django-mptt/blob/main/CHANGELOG.rst
|
||||||
django-mptt==0.14.0
|
django-mptt
|
||||||
|
|
||||||
# Context managers for PostgreSQL advisory locks
|
# Context managers for PostgreSQL advisory locks
|
||||||
# https://github.com/Xof/django-pglocks/blob/master/CHANGES.txt
|
# https://github.com/Xof/django-pglocks/blob/master/CHANGES.txt
|
||||||
|
@ -12,8 +12,12 @@ Group=netbox
|
|||||||
PIDFile=/var/tmp/netbox.pid
|
PIDFile=/var/tmp/netbox.pid
|
||||||
WorkingDirectory=/opt/netbox
|
WorkingDirectory=/opt/netbox
|
||||||
|
|
||||||
|
# Remove the following line if using uWSGI instead of Gunicorn
|
||||||
ExecStart=/opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox --config /opt/netbox/gunicorn.py netbox.wsgi
|
ExecStart=/opt/netbox/venv/bin/gunicorn --pid /var/tmp/netbox.pid --pythonpath /opt/netbox/netbox --config /opt/netbox/gunicorn.py netbox.wsgi
|
||||||
|
|
||||||
|
# Uncomment the following line if using uWSGI instead of Gunicorn
|
||||||
|
#ExecStart=/opt/netbox/venv/bin/uwsgi --ini /opt/netbox/uwsgi.ini
|
||||||
|
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=30
|
RestartSec=30
|
||||||
PrivateTmp=true
|
PrivateTmp=true
|
||||||
|
@ -14,10 +14,20 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
# Remove these lines if using uWSGI instead of Gunicorn
|
||||||
proxy_pass http://127.0.0.1:8001;
|
proxy_pass http://127.0.0.1:8001;
|
||||||
proxy_set_header X-Forwarded-Host $http_host;
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# Uncomment these lines if using uWSGI instead of Gunicorn
|
||||||
|
# include uwsgi_params;
|
||||||
|
# uwsgi_pass 127.0.0.1:8001;
|
||||||
|
# uwsgi_param Host $host;
|
||||||
|
# uwsgi_param X-Real-IP $remote_addr;
|
||||||
|
# uwsgi_param X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
# uwsgi_param X-Forwarded-Proto $http_x_forwarded_proto;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
18
contrib/uwsgi.ini
Normal file
18
contrib/uwsgi.ini
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[uwsgi]
|
||||||
|
; bind to the specified UNIX/TCP socket and port (usually localhost)
|
||||||
|
socket = 127.0.0.1:8001
|
||||||
|
|
||||||
|
; fail to start if any parameter in the configuration file isn’t explicitly understood by uWSGI.
|
||||||
|
strict = true
|
||||||
|
|
||||||
|
; re-spawn and pre-fork workers
|
||||||
|
master = true
|
||||||
|
|
||||||
|
; clear environment on exit
|
||||||
|
vacuum = true
|
||||||
|
|
||||||
|
; exit if no app can be loaded
|
||||||
|
need-app = true
|
||||||
|
|
||||||
|
; do not use multiple interpreters
|
||||||
|
single-interpreter = true
|
@ -1,10 +1,13 @@
|
|||||||
# Gunicorn
|
# Gunicorn
|
||||||
|
|
||||||
Like most Django applications, NetBox runs as a [WSGI application](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) behind an HTTP server. This documentation shows how to install and configure [gunicorn](http://gunicorn.org/) (which is automatically installed with NetBox) for this role, however other WSGI servers are available and should work similarly well. [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) is a popular alternative.
|
!!! tip
|
||||||
|
This page provides instructions for setting up the [gunicorn](http://gunicorn.org/) WSGI server. If you plan to use [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) instead, go [here](./4b-uwsgi.md).
|
||||||
|
|
||||||
|
NetBox runs as a [WSGI application](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) behind an HTTP server. This documentation shows how to install and configure [gunicorn](http://gunicorn.org/) (which is automatically installed with NetBox) for this role, however other WSGI servers are available and should work similarly well.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
NetBox ships with a default configuration file for gunicorn. To use it, copy `/opt/netbox/contrib/gunicorn.py` to `/opt/netbox/gunicorn.py`. (We make a copy of this file rather than pointing to it directly to ensure that any local changes to it do not get overwritten by a future upgrade.)
|
NetBox ships with a default configuration file for gunicorn. To use it, copy `/opt/netbox/contrib/gunicorn.py` to `/opt/netbox/gunicorn.py`. (We make a copy of this file rather than pointing to it directly to ensure that any local changes to it do not get overwritten during a future NetBox upgrade.)
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
sudo cp /opt/netbox/contrib/gunicorn.py /opt/netbox/gunicorn.py
|
sudo cp /opt/netbox/contrib/gunicorn.py /opt/netbox/gunicorn.py
|
104
docs/installation/4b-uwsgi.md
Normal file
104
docs/installation/4b-uwsgi.md
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
# uWSGI
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
This page provides instructions for setting up the [uWSGI](https://uwsgi-docs.readthedocs.io/) WSGI server. If you plan to use [gunicorn](http://gunicorn.org/) instead, go [here](./4a-gunicorn.md).
|
||||||
|
|
||||||
|
NetBox runs as a [WSGI application](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) behind an HTTP server. This documentation shows how to install and configure [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/) for this role, however other WSGI servers are available and should work similarly well.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Activate the Python virtual environment and install the `pyuwsgi` package using pip:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
source /opt/netbox/venv/bin/activate
|
||||||
|
pip3 install pyuwsgi
|
||||||
|
```
|
||||||
|
|
||||||
|
Once installed, add the package to `local_requirements.txt` to ensure it is re-installed during future rebuilds of the virtual environment:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
sudo sh -c "echo 'pyuwgsi' >> /opt/netbox/local_requirements.txt"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
NetBox ships with a default configuration file for uWSGI. To use it, copy `/opt/netbox/contrib/uwsgi.ini` to `/opt/netbox/uwsgi.ini`. (We make a copy of this file rather than pointing to it directly to ensure that any local changes to it do not get overwritten during a future NetBox upgrade.)
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
sudo cp /opt/netbox/contrib/uwsgi.ini /opt/netbox/uwsgi.ini
|
||||||
|
```
|
||||||
|
|
||||||
|
While the provided configuration should suffice for most initial installations, you may wish to edit this file to change the bound IP address and/or port number, or to make performance-related adjustments. See [the uWSGI documentation](https://uwsgi-docs-additions.readthedocs.io/en/latest/Options.html) for the available configuration parameters and take a minute to review the [Things to know](https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html) page. Django also provides [additional documentation](https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/uwsgi/) on configuring uWSGI with a Django app.
|
||||||
|
|
||||||
|
## systemd Setup
|
||||||
|
|
||||||
|
We'll use systemd to control both uWSGI and NetBox's background worker process. First, copy `contrib/netbox.service` and `contrib/netbox-rq.service` to the `/etc/systemd/system/` directory.
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
sudo cp -v /opt/netbox/contrib/*.service /etc/systemd/system/
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
```
|
||||||
|
|
||||||
|
The reference configuration assumes that gunicorn is in use, so we need to update it. Edit the `netbox.service` file to remove the line beginning with `ExecStart=/opt/netbox/venv/bin/gunicorn` and uncomment the line below it.
|
||||||
|
|
||||||
|
!!! warning "Check user & group assignment"
|
||||||
|
The stock service configuration files packaged with NetBox assume that the service will run with the `netbox` user and group names. If these differ on your installation, be sure to update the service files accordingly.
|
||||||
|
|
||||||
|
Once the configuration file has been saved, reload the service:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
sudo systemctl enable --now netbox netbox-rq
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use the command `systemctl status netbox` to verify that the WSGI service is running:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
systemctl status netbox.service
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see output similar to the following:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
● netbox.service - NetBox WSGI Service
|
||||||
|
Loaded: loaded (/etc/systemd/system/netbox.service; enabled; vendor preset: enabled)
|
||||||
|
Active: active (running) since Mon 2021-08-30 04:02:36 UTC; 14h ago
|
||||||
|
Docs: https://docs.netbox.dev/
|
||||||
|
Main PID: 1140492 (uwsgi)
|
||||||
|
Tasks: 19 (limit: 4683)
|
||||||
|
Memory: 666.2M
|
||||||
|
CGroup: /system.slice/netbox.service
|
||||||
|
├─1061 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/uwsgi --ini /opt/netbox/uwsgi.ini
|
||||||
|
├─1976 /opt/netbox/venv/bin/python3 /opt/netbox/venv/bin/uwsgi --ini /opt/netbox/uwsgi.ini
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
If the NetBox service fails to start, issue the command `journalctl -eu netbox` to check for log messages that may indicate the problem.
|
||||||
|
|
||||||
|
Once you've verified that the WSGI workers are up and running, move on to HTTP server setup.
|
||||||
|
|
||||||
|
## HTTP Server Installation
|
||||||
|
|
||||||
|
For server installation, you will want to follow the NetBox [HTTP Server Setup](5-http-server.md) guide, however after copying the configuration file, you will need to edit the file and change the `location` section to uncomment the uWSGI parameters:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
location / {
|
||||||
|
# proxy_pass http://127.0.0.1:8001;
|
||||||
|
# proxy_set_header X-Forwarded-Host $http_host;
|
||||||
|
# proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
# comment the lines above and uncomment the lines below if using uWSGI
|
||||||
|
include uwsgi_params;
|
||||||
|
uwsgi_pass 127.0.0.1:8001;
|
||||||
|
uwsgi_param Host $host;
|
||||||
|
uwsgi_param X-Real-IP $remote_addr;
|
||||||
|
uwsgi_param X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
uwsgi_param X-Forwarded-Proto $http_x_forwarded_proto;
|
||||||
|
}
|
||||||
|
```
|
@ -35,6 +35,9 @@ Once nginx is installed, copy the nginx configuration file provided by NetBox to
|
|||||||
sudo cp /opt/netbox/contrib/nginx.conf /etc/nginx/sites-available/netbox
|
sudo cp /opt/netbox/contrib/nginx.conf /etc/nginx/sites-available/netbox
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! tip "gunicorn vs. uWSGI"
|
||||||
|
The reference nginx configuration file assumes that gunicorn is in use. If using uWSGI instead, you'll need to remove the gunicorn-specific configuration (lines beginning with `proxy_pass` and `proxy_set_header`) and uncomment the uWSGI section below them before proceeding.
|
||||||
|
|
||||||
Then, delete `/etc/nginx/sites-enabled/default` and create a symlink in the `sites-enabled` directory to the configuration file you just created.
|
Then, delete `/etc/nginx/sites-enabled/default` and create a symlink in the `sites-enabled` directory to the configuration file you just created.
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
|
@ -94,7 +94,8 @@ nav:
|
|||||||
- 1. PostgreSQL: 'installation/1-postgresql.md'
|
- 1. PostgreSQL: 'installation/1-postgresql.md'
|
||||||
- 2. Redis: 'installation/2-redis.md'
|
- 2. Redis: 'installation/2-redis.md'
|
||||||
- 3. NetBox: 'installation/3-netbox.md'
|
- 3. NetBox: 'installation/3-netbox.md'
|
||||||
- 4. Gunicorn: 'installation/4-gunicorn.md'
|
- 4a. Gunicorn: 'installation/4a-gunicorn.md'
|
||||||
|
- 4b. uWSGI: 'installation/4b-uwsgi.md'
|
||||||
- 5. HTTP Server: 'installation/5-http-server.md'
|
- 5. HTTP Server: 'installation/5-http-server.md'
|
||||||
- 6. LDAP (Optional): 'installation/6-ldap.md'
|
- 6. LDAP (Optional): 'installation/6-ldap.md'
|
||||||
- Upgrading NetBox: 'installation/upgrading.md'
|
- Upgrading NetBox: 'installation/upgrading.md'
|
||||||
|
@ -156,8 +156,6 @@ class NetBoxAutoSchema(AutoSchema):
|
|||||||
remove_fields.append(child_name)
|
remove_fields.append(child_name)
|
||||||
if isinstance(child, (ChoiceField, WritableNestedSerializer)):
|
if isinstance(child, (ChoiceField, WritableNestedSerializer)):
|
||||||
properties[child_name] = None
|
properties[child_name] = None
|
||||||
elif isinstance(child, ManyRelatedField) and isinstance(child.child_relation, SerializedPKRelatedField):
|
|
||||||
properties[child_name] = None
|
|
||||||
|
|
||||||
if not properties:
|
if not properties:
|
||||||
return None
|
return None
|
||||||
|
@ -25,6 +25,7 @@ from netbox.views import generic
|
|||||||
from netbox.views.generic.base import BaseObjectView
|
from netbox.views.generic.base import BaseObjectView
|
||||||
from netbox.views.generic.mixins import TableMixin
|
from netbox.views.generic.mixins import TableMixin
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
|
from utilities.htmx import htmx_partial
|
||||||
from utilities.query import count_related
|
from utilities.query import count_related
|
||||||
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
|
from utilities.views import ContentTypePermissionRequiredMixin, register_model_view
|
||||||
from . import filtersets, forms, tables
|
from . import filtersets, forms, tables
|
||||||
@ -320,7 +321,7 @@ class BackgroundTaskListView(TableMixin, BaseRQView):
|
|||||||
table = self.get_table(data, request, False)
|
table = self.get_table(data, request, False)
|
||||||
|
|
||||||
# If this is an HTMX request, return only the rendered table HTML
|
# If this is an HTMX request, return only the rendered table HTML
|
||||||
if request.htmx:
|
if htmx_partial(request):
|
||||||
return render(request, 'htmx/table.html', {
|
return render(request, 'htmx/table.html', {
|
||||||
'table': table,
|
'table': table,
|
||||||
})
|
})
|
||||||
@ -489,8 +490,8 @@ class WorkerListView(TableMixin, BaseRQView):
|
|||||||
table = self.get_table(data, request, False)
|
table = self.get_table(data, request, False)
|
||||||
|
|
||||||
# If this is an HTMX request, return only the rendered table HTML
|
# If this is an HTMX request, return only the rendered table HTML
|
||||||
if request.htmx:
|
if htmx_partial(request):
|
||||||
if request.htmx.target != 'object_list':
|
if not request.htmx.target:
|
||||||
table.embedded = True
|
table.embedded = True
|
||||||
# Hide selection checkboxes
|
# Hide selection checkboxes
|
||||||
if 'pk' in table.base_columns:
|
if 'pk' in table.base_columns:
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import decimal
|
||||||
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
@ -22,7 +24,7 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
|
|||||||
max_digits=4,
|
max_digits=4,
|
||||||
decimal_places=1,
|
decimal_places=1,
|
||||||
label=_('Position (U)'),
|
label=_('Position (U)'),
|
||||||
min_value=0,
|
min_value=decimal.Decimal(0),
|
||||||
default=1.0
|
default=1.0
|
||||||
)
|
)
|
||||||
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False, allow_null=True)
|
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False, allow_null=True)
|
||||||
|
@ -20,6 +20,7 @@ from netbox.views import generic
|
|||||||
from netbox.views.generic.mixins import TableMixin
|
from netbox.views.generic.mixins import TableMixin
|
||||||
from utilities.data import shallow_compare_dict
|
from utilities.data import shallow_compare_dict
|
||||||
from utilities.forms import ConfirmationForm, get_field_value
|
from utilities.forms import ConfirmationForm, get_field_value
|
||||||
|
from utilities.htmx import htmx_partial
|
||||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||||
from utilities.query import count_related
|
from utilities.query import count_related
|
||||||
from utilities.querydict import normalize_querydict
|
from utilities.querydict import normalize_querydict
|
||||||
@ -1224,7 +1225,7 @@ class ScriptResultView(TableMixin, generic.ObjectView):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# If this is an HTMX request, return only the result HTML
|
# If this is an HTMX request, return only the result HTML
|
||||||
if request.htmx:
|
if htmx_partial(request):
|
||||||
response = render(request, 'extras/htmx/script_result.html', context)
|
response = render(request, 'extras/htmx/script_result.html', context)
|
||||||
if job.completed or not job.started:
|
if job.completed or not job.started:
|
||||||
response.status_code = 286
|
response.status_code = 286
|
||||||
|
@ -27,9 +27,13 @@ class BaseModelSerializer(serializers.ModelSerializer):
|
|||||||
self.nested = nested
|
self.nested = nested
|
||||||
self._requested_fields = fields
|
self._requested_fields = fields
|
||||||
|
|
||||||
|
# Disable validators for nested objects (which already exist)
|
||||||
|
if self.nested:
|
||||||
|
self.validators = []
|
||||||
|
|
||||||
# If this serializer is nested but no fields have been specified,
|
# If this serializer is nested but no fields have been specified,
|
||||||
# default to using Meta.brief_fields (if set)
|
# default to using Meta.brief_fields (if set)
|
||||||
if nested and not fields:
|
if self.nested and not fields:
|
||||||
self._requested_fields = getattr(self.Meta, 'brief_fields', None)
|
self._requested_fields = getattr(self.Meta, 'brief_fields', None)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
@ -8,9 +8,11 @@ def settings_and_registry(request):
|
|||||||
"""
|
"""
|
||||||
Expose Django settings and NetBox registry stores in the template context. Example: {{ settings.DEBUG }}
|
Expose Django settings and NetBox registry stores in the template context. Example: {{ settings.DEBUG }}
|
||||||
"""
|
"""
|
||||||
|
user_preferences = request.user.config if request.user.is_authenticated else {}
|
||||||
return {
|
return {
|
||||||
'settings': django_settings,
|
'settings': django_settings,
|
||||||
'config': get_config(),
|
'config': get_config(),
|
||||||
'registry': registry,
|
'registry': registry,
|
||||||
'preferences': request.user.config if request.user.is_authenticated else {},
|
'preferences': user_preferences,
|
||||||
|
'htmx_navigation': user_preferences.get('ui.htmx_navigation', False) == 'true'
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,14 @@ PREFERENCES = {
|
|||||||
),
|
),
|
||||||
default='light',
|
default='light',
|
||||||
),
|
),
|
||||||
|
'ui.htmx_navigation': UserPreference(
|
||||||
|
label=_('HTMX Navigation'),
|
||||||
|
choices=(
|
||||||
|
('', _('Disabled')),
|
||||||
|
('true', _('Enabled')),
|
||||||
|
),
|
||||||
|
default=False
|
||||||
|
),
|
||||||
'locale.language': UserPreference(
|
'locale.language': UserPreference(
|
||||||
label=_('Language'),
|
label=_('Language'),
|
||||||
choices=(
|
choices=(
|
||||||
|
@ -23,6 +23,7 @@ from utilities.error_handlers import handle_protectederror
|
|||||||
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
|
from utilities.exceptions import AbortRequest, AbortTransaction, PermissionsViolation
|
||||||
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
|
from utilities.forms import BulkRenameForm, ConfirmationForm, restrict_form_fields
|
||||||
from utilities.forms.bulk_import import BulkImportForm
|
from utilities.forms.bulk_import import BulkImportForm
|
||||||
|
from utilities.htmx import htmx_partial
|
||||||
from utilities.permissions import get_permission_for_model
|
from utilities.permissions import get_permission_for_model
|
||||||
from utilities.views import GetReturnURLMixin, get_viewname
|
from utilities.views import GetReturnURLMixin, get_viewname
|
||||||
from .base import BaseMultiObjectView
|
from .base import BaseMultiObjectView
|
||||||
@ -161,8 +162,8 @@ class ObjectListView(BaseMultiObjectView, ActionsMixin, TableMixin):
|
|||||||
table = self.get_table(self.queryset, request, has_bulk_actions)
|
table = self.get_table(self.queryset, request, has_bulk_actions)
|
||||||
|
|
||||||
# If this is an HTMX request, return only the rendered table HTML
|
# If this is an HTMX request, return only the rendered table HTML
|
||||||
if request.htmx:
|
if htmx_partial(request):
|
||||||
if request.htmx.target != 'object_list':
|
if not request.htmx.target:
|
||||||
table.embedded = True
|
table.embedded = True
|
||||||
# Hide selection checkboxes
|
# Hide selection checkboxes
|
||||||
if 'pk' in table.base_columns:
|
if 'pk' in table.base_columns:
|
||||||
|
@ -17,6 +17,7 @@ from extras.signals import clear_events
|
|||||||
from utilities.error_handlers import handle_protectederror
|
from utilities.error_handlers import handle_protectederror
|
||||||
from utilities.exceptions import AbortRequest, PermissionsViolation
|
from utilities.exceptions import AbortRequest, PermissionsViolation
|
||||||
from utilities.forms import ConfirmationForm, restrict_form_fields
|
from utilities.forms import ConfirmationForm, restrict_form_fields
|
||||||
|
from utilities.htmx import htmx_partial
|
||||||
from utilities.permissions import get_permission_for_model
|
from utilities.permissions import get_permission_for_model
|
||||||
from utilities.querydict import normalize_querydict, prepare_cloned_fields
|
from utilities.querydict import normalize_querydict, prepare_cloned_fields
|
||||||
from utilities.views import GetReturnURLMixin, get_viewname
|
from utilities.views import GetReturnURLMixin, get_viewname
|
||||||
@ -138,7 +139,7 @@ class ObjectChildrenView(ObjectView, ActionsMixin, TableMixin):
|
|||||||
table = self.get_table(table_data, request, has_bulk_actions)
|
table = self.get_table(table_data, request, has_bulk_actions)
|
||||||
|
|
||||||
# If this is an HTMX request, return only the rendered table HTML
|
# If this is an HTMX request, return only the rendered table HTML
|
||||||
if request.htmx:
|
if htmx_partial(request):
|
||||||
return render(request, 'htmx/table.html', {
|
return render(request, 'htmx/table.html', {
|
||||||
'object': instance,
|
'object': instance,
|
||||||
'table': table,
|
'table': table,
|
||||||
@ -226,7 +227,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView):
|
|||||||
restrict_form_fields(form, request.user)
|
restrict_form_fields(form, request.user)
|
||||||
|
|
||||||
# If this is an HTMX request, return only the rendered form HTML
|
# If this is an HTMX request, return only the rendered form HTML
|
||||||
if request.htmx:
|
if htmx_partial(request):
|
||||||
return render(request, 'htmx/form.html', {
|
return render(request, 'htmx/form.html', {
|
||||||
'form': form,
|
'form': form,
|
||||||
})
|
})
|
||||||
@ -482,7 +483,7 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView):
|
|||||||
instance = self.alter_object(self.queryset.model(), request)
|
instance = self.alter_object(self.queryset.model(), request)
|
||||||
|
|
||||||
# If this is an HTMX request, return only the rendered form HTML
|
# If this is an HTMX request, return only the rendered form HTML
|
||||||
if request.htmx:
|
if htmx_partial(request):
|
||||||
return render(request, 'htmx/form.html', {
|
return render(request, 'htmx/form.html', {
|
||||||
'form': form,
|
'form': form,
|
||||||
})
|
})
|
||||||
|
@ -17,6 +17,7 @@ from netbox.forms import SearchForm
|
|||||||
from netbox.search import LookupTypes
|
from netbox.search import LookupTypes
|
||||||
from netbox.search.backends import search_backend
|
from netbox.search.backends import search_backend
|
||||||
from netbox.tables import SearchTable
|
from netbox.tables import SearchTable
|
||||||
|
from utilities.htmx import htmx_partial
|
||||||
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
from utilities.paginator import EnhancedPaginator, get_paginate_count
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -104,7 +105,7 @@ class SearchView(View):
|
|||||||
}).configure(table)
|
}).configure(table)
|
||||||
|
|
||||||
# If this is an HTMX request, return only the rendered table HTML
|
# If this is an HTMX request, return only the rendered table HTML
|
||||||
if request.htmx:
|
if htmx_partial(request):
|
||||||
return render(request, 'htmx/table.html', {
|
return render(request, 'htmx/table.html', {
|
||||||
'table': table,
|
'table': table,
|
||||||
})
|
})
|
||||||
|
BIN
netbox/project-static/dist/netbox.css
vendored
BIN
netbox/project-static/dist/netbox.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -1,11 +1,12 @@
|
|||||||
import { getElements, isTruthy } from './util';
|
|
||||||
import { initButtons } from './buttons';
|
import { initButtons } from './buttons';
|
||||||
|
import { initClipboard } from './clipboard'
|
||||||
import { initSelects } from './select';
|
import { initSelects } from './select';
|
||||||
import { initObjectSelector } from './objectSelector';
|
import { initObjectSelector } from './objectSelector';
|
||||||
import { initBootstrap } from './bs';
|
import { initBootstrap } from './bs';
|
||||||
|
import { initMessages } from './messages';
|
||||||
|
|
||||||
function initDepedencies(): void {
|
function initDepedencies(): void {
|
||||||
for (const init of [initButtons, initSelects, initObjectSelector, initBootstrap]) {
|
for (const init of [initButtons, initClipboard, initSelects, initObjectSelector, initBootstrap, initMessages]) {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -15,16 +16,5 @@ function initDepedencies(): void {
|
|||||||
* elements.
|
* elements.
|
||||||
*/
|
*/
|
||||||
export function initHtmx(): void {
|
export function initHtmx(): void {
|
||||||
for (const element of getElements('[hx-target]')) {
|
document.addEventListener('htmx:afterSettle', initDepedencies);
|
||||||
const targetSelector = element.getAttribute('hx-target');
|
|
||||||
if (isTruthy(targetSelector)) {
|
|
||||||
for (const target of getElements(targetSelector)) {
|
|
||||||
target.addEventListener('htmx:afterSettle', initDepedencies);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const element of getElements('[hx-trigger=load]')) {
|
|
||||||
element.addEventListener('htmx:afterSettle', initDepedencies);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
@import '../node_modules/@tabler/core/src/scss/vendor/tom-select';
|
@import '../node_modules/@tabler/core/src/scss/vendor/tom-select';
|
||||||
|
|
||||||
// Overrides of external libraries
|
// Overrides of external libraries
|
||||||
|
@import 'overrides/bootstrap';
|
||||||
@import 'overrides/tabler';
|
@import 'overrides/tabler';
|
||||||
|
|
||||||
// Transitional styling to ease migration of templates from NetBox v3.x
|
// Transitional styling to ease migration of templates from NetBox v3.x
|
||||||
|
11
netbox/project-static/styles/overrides/_bootstrap.scss
Normal file
11
netbox/project-static/styles/overrides/_bootstrap.scss
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// Disable smooth scrolling for intra-page links
|
||||||
|
html {
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent dropdown menus from being clipped inside responsive tables
|
||||||
|
.table-responsive {
|
||||||
|
.dropdown, .btn-group, .btn-group-vertical {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,7 @@
|
|||||||
{% block title %}{% trans "User Preferences" %}{% endblock %}
|
{% block title %}{% trans "User Preferences" %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form method="post" action="" id="preferences-update">
|
<form method="post" action="" hx-disable="true" id="preferences-update">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
{# Built-in preferences #}
|
{# Built-in preferences #}
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width, viewport-fit=cover" />
|
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width, viewport-fit=cover" />
|
||||||
|
<meta name="htmx-config" content='{"scrollBehavior": "auto"}'>
|
||||||
|
|
||||||
{# Page title #}
|
{# Page title #}
|
||||||
<title>{% block title %}{% trans "Home" %}{% endblock %} | NetBox</title>
|
<title>{% block title %}{% trans "Home" %}{% endblock %} | NetBox</title>
|
||||||
|
@ -58,8 +58,48 @@ Blocks:
|
|||||||
<i class="mdi mdi-lightbulb-on"></i>
|
<i class="mdi mdi-lightbulb-on"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# User menu #}
|
{# User menu #}
|
||||||
{% include 'inc/user_menu.html' %}
|
{% if request.user.is_authenticated %}
|
||||||
|
<div class="nav-item dropdown">
|
||||||
|
<a href="#" class="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown" aria-label="Open user menu">
|
||||||
|
<div class="d-xl-block ps-2">
|
||||||
|
<div>{{ request.user }}</div>
|
||||||
|
<div class="mt-1 small text-secondary">{% if request.user.is_staff %}Staff{% else %}User{% endif %}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow" {% htmx_boost %}>
|
||||||
|
{% if config.DJANGO_ADMIN_ENABLED and request.user.is_staff %}
|
||||||
|
<a class="dropdown-item" href="{% url 'admin:index' %}">
|
||||||
|
<i class="mdi mdi-cog"></i> {% trans "Django Admin" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{% url 'account:profile' %}" class="dropdown-item">
|
||||||
|
<i class="mdi mdi-account"></i> {% trans "Profile" %}
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'account:bookmarks' %}" class="dropdown-item">
|
||||||
|
<i class="mdi mdi-bookmark"></i> {% trans "Bookmarks" %}
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'account:preferences' %}" class="dropdown-item">
|
||||||
|
<i class="mdi mdi-wrench"></i> {% trans "Preferences" %}
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'account:usertoken_list' %}" class="dropdown-item">
|
||||||
|
<i class="mdi mdi-key"></i> {% trans "API Tokens" %}
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<a href="{% url 'logout' %}" class="dropdown-item">
|
||||||
|
<i class="mdi mdi-logout-variant"></i> {% trans "Log Out" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="btn-group ps-2">
|
||||||
|
<a class="btn btn-primary" type="button" href="{% url 'login' %}?next={{ request.path }}">
|
||||||
|
<i class="mdi mdi-login-variant"></i> {% trans "Log In" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{# /User menu #}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Search box #}
|
{# Search box #}
|
||||||
@ -79,6 +119,7 @@ Blocks:
|
|||||||
|
|
||||||
{# Page content #}
|
{# Page content #}
|
||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
|
<div id="page-content" {% htmx_boost %}>
|
||||||
|
|
||||||
{# Page header #}
|
{# Page header #}
|
||||||
{% block header %}
|
{% block header %}
|
||||||
@ -122,6 +163,8 @@ Blocks:
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{# /Bottom banner #}
|
{# /Bottom banner #}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
{# Page footer #}
|
{# Page footer #}
|
||||||
<footer class="footer footer-transparent d-print-none py-2">
|
<footer class="footer footer-transparent d-print-none py-2">
|
||||||
<div class="container-fluid d-flex justify-content-between align-items-center">
|
<div class="container-fluid d-flex justify-content-between align-items-center">
|
||||||
@ -173,7 +216,7 @@ Blocks:
|
|||||||
{# /Footer links #}
|
{# /Footer links #}
|
||||||
|
|
||||||
{# Footer text #}
|
{# Footer text #}
|
||||||
<ul class="list-inline list-inline-dots mb-0">
|
<ul class="list-inline list-inline-dots mb-0" id="footer-stamp" hx-swap-oob="true">
|
||||||
<li class="list-inline-item">
|
<li class="list-inline-item">
|
||||||
{% annotated_now %} {% now 'T' %}
|
{% annotated_now %} {% now 'T' %}
|
||||||
</li>
|
</li>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if 'bulk_rename' in actions %}
|
{% if 'bulk_rename' in actions %}
|
||||||
{% with bulk_rename_view=model|validated_viewname:"bulk_rename" %}
|
{% with bulk_rename_view=model|validated_viewname:"bulk_rename" %}
|
||||||
<button type="submit" name="_rename" formaction="{% url bulk_rename_view %}" class="btn btn-outline-warning">
|
<button type="submit" name="_rename" {% formaction %}="{% url bulk_rename_view %}" class="btn btn-outline-warning">
|
||||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename Selected" %}
|
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename Selected" %}
|
||||||
</button>
|
</button>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
{% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}
|
{% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}
|
||||||
{% if 'bulk_edit' in actions and bulk_edit_view %}
|
{% if 'bulk_edit' in actions and bulk_edit_view %}
|
||||||
<button type="submit" name="_edit"
|
<button type="submit" name="_edit"
|
||||||
formaction="{% url bulk_edit_view %}?device={{ object.pk }}&return_url={{ return_url }}"
|
{% formaction %}="{% url bulk_edit_view %}?device={{ object.pk }}&return_url={{ return_url }}"
|
||||||
class="btn btn-warning">
|
class="btn btn-warning">
|
||||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
|
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
|
||||||
</button>
|
</button>
|
||||||
@ -14,7 +14,7 @@
|
|||||||
{% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %}
|
{% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %}
|
||||||
{% if 'bulk_rename' in actions and bulk_rename_view %}
|
{% if 'bulk_rename' in actions and bulk_rename_view %}
|
||||||
<button type="submit" name="_rename"
|
<button type="submit" name="_rename"
|
||||||
formaction="{% url bulk_rename_view %}?return_url={{ return_url }}"
|
{% formaction %}="{% url bulk_rename_view %}?return_url={{ return_url }}"
|
||||||
class="btn btn-outline-warning">
|
class="btn btn-outline-warning">
|
||||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
|
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
|
||||||
</button>
|
</button>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||||
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||||
<button type="submit" name="_disconnect"
|
<button type="submit" name="_disconnect"
|
||||||
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||||
class="btn btn-outline-danger">
|
class="btn btn-outline-danger">
|
||||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||||
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||||
<button type="submit" name="_disconnect"
|
<button type="submit" name="_disconnect"
|
||||||
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||||
class="btn btn-outline-danger">
|
class="btn btn-outline-danger">
|
||||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||||
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||||
<button type="submit" name="_disconnect"
|
<button type="submit" name="_disconnect"
|
||||||
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||||
class="btn btn-outline-danger">
|
class="btn btn-outline-danger">
|
||||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||||
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||||
<button type="submit" name="_disconnect"
|
<button type="submit" name="_disconnect"
|
||||||
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||||
class="btn btn-outline-danger">
|
class="btn btn-outline-danger">
|
||||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||||
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||||
<button type="submit" name="_disconnect"
|
<button type="submit" name="_disconnect"
|
||||||
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||||
class="btn btn-outline-danger">
|
class="btn btn-outline-danger">
|
||||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||||
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||||
<button type="submit" name="_disconnect"
|
<button type="submit" name="_disconnect"
|
||||||
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||||
class="btn btn-outline-danger">
|
class="btn btn-outline-danger">
|
||||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
{% with bulk_disconnect_view=child_model|validated_viewname:"bulk_disconnect" %}
|
||||||
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
{% if 'bulk_disconnect' in actions and bulk_disconnect_view %}
|
||||||
<button type="submit" name="_disconnect"
|
<button type="submit" name="_disconnect"
|
||||||
formaction="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
{% formaction %}="{% url bulk_disconnect_view %}?return_url={{ return_url }}"
|
||||||
class="btn btn-outline-danger">
|
class="btn btn-outline-danger">
|
||||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -11,63 +11,63 @@
|
|||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
{% if perms.dcim.add_consoleport %}
|
{% if perms.dcim.add_consoleport %}
|
||||||
<li>
|
<li>
|
||||||
<button type="submit" formaction="{% url 'dcim:device_bulk_add_consoleport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_consoleport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||||
{% trans "Console Ports" %}
|
{% trans "Console Ports" %}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_consoleserverport %}
|
{% if perms.dcim.add_consoleserverport %}
|
||||||
<li>
|
<li>
|
||||||
<button type="submit" formaction="{% url 'dcim:device_bulk_add_consoleserverport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item ">
|
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_consoleserverport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item ">
|
||||||
{% trans "Console Server Ports" %}
|
{% trans "Console Server Ports" %}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_powerport %}
|
{% if perms.dcim.add_powerport %}
|
||||||
<li>
|
<li>
|
||||||
<button type="submit" formaction="{% url 'dcim:device_bulk_add_powerport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_powerport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||||
{% trans "Power Ports" %}
|
{% trans "Power Ports" %}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_poweroutlet %}
|
{% if perms.dcim.add_poweroutlet %}
|
||||||
<li>
|
<li>
|
||||||
<button type="submit" formaction="{% url 'dcim:device_bulk_add_poweroutlet' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_poweroutlet' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||||
{% trans "Power Outlets" %}
|
{% trans "Power Outlets" %}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_interface %}
|
{% if perms.dcim.add_interface %}
|
||||||
<li>
|
<li>
|
||||||
<button type="submit" formaction="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}"
|
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_interface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||||
class="dropdown-item">{% trans "Interfaces" %}
|
{% trans "Interfaces" %}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_rearport %}
|
{% if perms.dcim.add_rearport %}
|
||||||
<li>
|
<li>
|
||||||
<button type="submit" formaction="{% url 'dcim:device_bulk_add_rearport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_rearport' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||||
{% trans "Rear Ports" %}
|
{% trans "Rear Ports" %}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_devicebay %}
|
{% if perms.dcim.add_devicebay %}
|
||||||
<li>
|
<li>
|
||||||
<button type="submit" formaction="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_devicebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||||
{% trans "Device Bays" %}
|
{% trans "Device Bays" %}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_modulebay %}
|
{% if perms.dcim.add_modulebay %}
|
||||||
<li>
|
<li>
|
||||||
<button type="submit" formaction="{% url 'dcim:device_bulk_add_modulebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_modulebay' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||||
{% trans "Module Bays" %}
|
{% trans "Module Bays" %}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.add_inventoryitem %}
|
{% if perms.dcim.add_inventoryitem %}
|
||||||
<li>
|
<li>
|
||||||
<button type="submit" formaction="{% url 'dcim:device_bulk_add_inventoryitem' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
<button type="submit" {% formaction %}="{% url 'dcim:device_bulk_add_inventoryitem' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||||
{% trans "Inventory Items" %}
|
{% trans "Inventory Items" %}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
@ -78,7 +78,7 @@
|
|||||||
{% if 'bulk_edit' in actions %}
|
{% if 'bulk_edit' in actions %}
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
{% bulk_edit_button model query_params=request.GET %}
|
{% bulk_edit_button model query_params=request.GET %}
|
||||||
<button type="submit" name="_rename" formaction="{% url 'dcim:device_bulk_rename' %}?return_url={% url 'dcim:device_list' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-outline-warning">
|
<button type="submit" name="_rename" {% formaction %}="{% url 'dcim:device_bulk_rename' %}?return_url={% url 'dcim:device_list' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-outline-warning">
|
||||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
{% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}
|
{% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}
|
||||||
{% if 'bulk_edit' in actions and bulk_edit_view %}
|
{% if 'bulk_edit' in actions and bulk_edit_view %}
|
||||||
<button type="submit" name="_edit"
|
<button type="submit" name="_edit"
|
||||||
formaction="{% url bulk_edit_view %}?device={{ object.pk }}&return_url={{ return_url }}"
|
{% formaction %}="{% url bulk_edit_view %}?device={{ object.pk }}&return_url={{ return_url }}"
|
||||||
class="btn btn-warning">
|
class="btn btn-warning">
|
||||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
|
<i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
|
||||||
</button>
|
</button>
|
||||||
@ -16,7 +16,7 @@
|
|||||||
{% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %}
|
{% with bulk_rename_view=child_model|validated_viewname:"bulk_rename" %}
|
||||||
{% if 'bulk_rename' in actions and bulk_rename_view %}
|
{% if 'bulk_rename' in actions and bulk_rename_view %}
|
||||||
<button type="submit" name="_rename"
|
<button type="submit" name="_rename"
|
||||||
formaction="{% url bulk_rename_view %}?return_url={{ return_url }}"
|
{% formaction %}="{% url bulk_rename_view %}?return_url={{ return_url }}"
|
||||||
class="btn btn-outline-warning">
|
class="btn btn-outline-warning">
|
||||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
|
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
|
||||||
</button>
|
</button>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<object data="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{face}}&render=svg{% if extra_params %}&{{ extra_params }}{% endif %}" class="rack_elevation"></object>
|
<object data="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{face}}&render=svg{% if extra_params %}&{{ extra_params }}{% endif %}" class="rack_elevation"></object>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center mt-3">
|
<div class="text-center mt-3">
|
||||||
<a class="btn btn-outline-primary" href="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{face}}&render=svg{% if extra_params %}&{{ extra_params }}{% endif %}">
|
<a class="btn btn-outline-primary" href="{% url 'dcim-api:rack-elevation' pk=object.pk %}?face={{face}}&render=svg{% if extra_params %}&{{ extra_params }}{% endif %}" hx-boost="false">
|
||||||
<i class="mdi mdi-file-download"></i> {% trans "Download SVG" %}
|
<i class="mdi mdi-file-download"></i> {% trans "Download SVG" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,13 +13,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-footer d-print-none">
|
<div class="card-footer d-print-none">
|
||||||
{% if table.rows %}
|
{% if table.rows %}
|
||||||
<button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_rename" %}?return_url={{ return_url }}" class="btn btn-warning">
|
<button type="submit" name="_edit" {% formaction %}="{% url table.Meta.model|viewname:"bulk_rename" %}?return_url={{ return_url }}" class="btn btn-warning">
|
||||||
<span class="mdi mdi-pencil-outline" aria-hidden="true"></span> {% trans "Rename" %}
|
<span class="mdi mdi-pencil-outline" aria-hidden="true"></span> {% trans "Rename" %}
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_edit" formaction="{% url table.Meta.model|viewname:"bulk_edit" %}?return_url={{ return_url }}" class="btn btn-warning">
|
<button type="submit" name="_edit" {% formaction %}="{% url table.Meta.model|viewname:"bulk_edit" %}?return_url={{ return_url }}" class="btn btn-warning">
|
||||||
<span class="mdi mdi-pencil" aria-hidden="true"></span> {% trans "Edit" %}
|
<span class="mdi mdi-pencil" aria-hidden="true"></span> {% trans "Edit" %}
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="_delete" formaction="{% url table.Meta.model|viewname:"bulk_delete" %}?return_url={{ return_url }}" class="btn btn-danger">
|
<button type="submit" name="_delete" {% formaction %}="{% url table.Meta.model|viewname:"bulk_delete" %}?return_url={{ return_url }}" class="btn btn-danger">
|
||||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
|
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -52,17 +52,17 @@
|
|||||||
{% htmx_table 'dcim:powerfeed_list' power_panel_id=object.pk %}
|
{% htmx_table 'dcim:powerfeed_list' power_panel_id=object.pk %}
|
||||||
<div class="card-footer d-print-none">
|
<div class="card-footer d-print-none">
|
||||||
{% if perms.dcim.change_powerfeed %}
|
{% if perms.dcim.change_powerfeed %}
|
||||||
<button type="submit" name="_edit" formaction="{% url 'dcim:powerfeed_bulk_edit' %}?return_url={% url 'dcim:powerpanel' pk=object.pk %}" class="btn btn-warning">
|
<button type="submit" name="_edit" {% formaction %}="{% url 'dcim:powerfeed_bulk_edit' %}?return_url={% url 'dcim:powerpanel' pk=object.pk %}" class="btn btn-warning">
|
||||||
<span class="mdi mdi-pencil" aria-hidden="true"></span> {% trans "Edit" %}
|
<span class="mdi mdi-pencil" aria-hidden="true"></span> {% trans "Edit" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.delete_cable %}
|
{% if perms.dcim.delete_cable %}
|
||||||
<button type="submit" name="_disconnect" formaction="{% url 'dcim:powerfeed_bulk_disconnect' %}?return_url={% url 'dcim:powerpanel' pk=object.pk %}" class="btn btn-outline-danger">
|
<button type="submit" name="_disconnect" {% formaction %}="{% url 'dcim:powerfeed_bulk_disconnect' %}?return_url={% url 'dcim:powerpanel' pk=object.pk %}" class="btn btn-outline-danger">
|
||||||
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
<span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> {% trans "Disconnect" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.delete_powerfeed %}
|
{% if perms.dcim.delete_powerfeed %}
|
||||||
<button type="submit" name="_delete" formaction="{% url 'dcim:powerfeed_bulk_delete' %}?return_url={% url 'dcim:powerpanel' pk=object.pk %}" class="btn btn-danger">
|
<button type="submit" name="_delete" {% formaction %}="{% url 'dcim:powerfeed_bulk_delete' %}?return_url={% url 'dcim:powerpanel' pk=object.pk %}" class="btn btn-danger">
|
||||||
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> {% trans "Delete" %}
|
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> {% trans "Delete" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
{% block bulk_buttons %}
|
{% block bulk_buttons %}
|
||||||
{% if perms.extras.sync_configcontext %}
|
{% if perms.extras.sync_configcontext %}
|
||||||
<button type="submit" name="_sync" formaction="{% url 'extras:configcontext_bulk_sync' %}" class="btn btn-primary">
|
<button type="submit" name="_sync" {% formaction %}="{% url 'extras:configcontext_bulk_sync' %}" class="btn btn-primary">
|
||||||
<i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync Data" %}
|
<i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync Data" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
{% block bulk_buttons %}
|
{% block bulk_buttons %}
|
||||||
{% if perms.extras.sync_configtemplate %}
|
{% if perms.extras.sync_configtemplate %}
|
||||||
<button type="submit" name="_sync" formaction="{% url 'extras:configtemplate_bulk_sync' %}" class="btn btn-primary">
|
<button type="submit" name="_sync" {% formaction %}="{% url 'extras:configtemplate_bulk_sync' %}" class="btn btn-primary">
|
||||||
<i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync Data" %}
|
<i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync Data" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% if htmx_url and has_permission %}
|
{% if htmx_url and has_permission %}
|
||||||
<div class="htmx-container" hx-get="{{ htmx_url }}" hx-trigger="load"></div>
|
<div class="htmx-container" hx-get="{{ htmx_url }}" hx-trigger="load" hx-target="this" hx-select="table" hx-swap="innerHTML"></div>
|
||||||
{% elif htmx_url %}
|
{% elif htmx_url %}
|
||||||
<div class="text-muted text-center">
|
<div class="text-muted text-center">
|
||||||
<i class="mdi mdi-lock-outline"></i> {% trans "No permission to view this content" %}.
|
<i class="mdi mdi-lock-outline"></i> {% trans "No permission to view this content" %}.
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
{% block bulk_buttons %}
|
{% block bulk_buttons %}
|
||||||
{% if perms.extras.sync_configcontext %}
|
{% if perms.extras.sync_configcontext %}
|
||||||
<button type="submit" name="_sync" formaction="{% url 'extras:exporttemplate_bulk_sync' %}" class="btn btn-primary">
|
<button type="submit" name="_sync" {% formaction %}="{% url 'extras:exporttemplate_bulk_sync' %}" class="btn btn-primary">
|
||||||
<i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync Data" %}
|
<i class="mdi mdi-sync" aria-hidden="true"></i> {% trans "Sync Data" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
{% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}
|
{% with bulk_edit_view=child_model|validated_viewname:"bulk_edit" %}
|
||||||
{% if 'bulk_edit' in actions and bulk_edit_view %}
|
{% if 'bulk_edit' in actions and bulk_edit_view %}
|
||||||
<button type="submit" name="_edit"
|
<button type="submit" name="_edit"
|
||||||
formaction="{% url bulk_edit_view %}?return_url={{ return_url }}"
|
{% formaction %}="{% url bulk_edit_view %}?return_url={{ return_url }}"
|
||||||
class="btn btn-warning">
|
class="btn btn-warning">
|
||||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit Selected" %}
|
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit Selected" %}
|
||||||
</button>
|
</button>
|
||||||
@ -35,7 +35,7 @@
|
|||||||
{% with bulk_delete_view=child_model|validated_viewname:"bulk_delete" %}
|
{% with bulk_delete_view=child_model|validated_viewname:"bulk_delete" %}
|
||||||
{% if 'bulk_delete' in actions and bulk_delete_view %}
|
{% if 'bulk_delete' in actions and bulk_delete_view %}
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
formaction="{% url bulk_delete_view %}?return_url={{ return_url }}"
|
{% formaction %}="{% url bulk_delete_view %}?return_url={{ return_url }}"
|
||||||
class="btn btn-danger">
|
class="btn btn-danger">
|
||||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete Selected" %}
|
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete Selected" %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -56,7 +56,7 @@ Context:
|
|||||||
<form action="" method="post" enctype="multipart/form-data" class="object-edit mt-5">
|
<form action="" method="post" enctype="multipart/form-data" class="object-edit mt-5">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div id="form_fields">
|
<div id="form_fields" hx-disinherit="hx-select hx-swap">
|
||||||
{% block form %}
|
{% block form %}
|
||||||
{% include 'htmx/form.html' %}
|
{% include 'htmx/form.html' %}
|
||||||
{% endblock form %}
|
{% endblock form %}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
|
||||||
<div id="django-messages" class="toast-container position-fixed bottom-0 end-0 p-3">
|
<div id="django-messages" class="toast-container position-fixed bottom-0 end-0 p-3" hx-swap-oob="true">
|
||||||
|
|
||||||
{# Non-Field Form Errors #}
|
{# Non-Field Form Errors #}
|
||||||
{% if form and form.non_field_errors %}
|
{% if form and form.non_field_errors %}
|
||||||
|
@ -2,7 +2,12 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% if page %}
|
{% if page %}
|
||||||
<div class="d-flex justify-content-between align-items-center border-top p-2">
|
<div
|
||||||
|
class="d-flex justify-content-between align-items-center border-top p-2"
|
||||||
|
hx-target="closest .htmx-container"
|
||||||
|
hx-disinherit="hx-select hx-swap"
|
||||||
|
{% if not table.embedded %}hx-push-url="true"{% endif %}
|
||||||
|
>
|
||||||
|
|
||||||
{# Pages carousel #}
|
{# Pages carousel #}
|
||||||
{% if paginator.num_pages > 1 %}
|
{% if paginator.num_pages > 1 %}
|
||||||
@ -13,12 +18,7 @@
|
|||||||
{% if page.has_previous %}
|
{% if page.has_previous %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
{% if htmx %}
|
{% if htmx %}
|
||||||
<a href="#"
|
<a href="#" hx-get="{{ table.htmx_url }}{% querystring request page=page.previous_page_number %}" class="page-link">
|
||||||
hx-get="{{ table.htmx_url }}{% querystring request page=page.previous_page_number %}"
|
|
||||||
hx-target="closest .htmx-container"
|
|
||||||
{% if not table.embedded %}hx-push-url="true"{% endif %}
|
|
||||||
class="page-link"
|
|
||||||
>
|
|
||||||
<i class="mdi mdi-chevron-left"></i>
|
<i class="mdi mdi-chevron-left"></i>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -34,12 +34,7 @@
|
|||||||
{% for p in page.smart_pages %}
|
{% for p in page.smart_pages %}
|
||||||
<li class="page-item{% if page.number == p %} active" aria-current="page{% endif %}">
|
<li class="page-item{% if page.number == p %} active" aria-current="page{% endif %}">
|
||||||
{% if p and htmx %}
|
{% if p and htmx %}
|
||||||
<a href="#"
|
<a href="#" hx-get="{{ table.htmx_url }}{% querystring request page=p %}" class="page-link">
|
||||||
hx-get="{{ table.htmx_url }}{% querystring request page=p %}"
|
|
||||||
hx-target="closest .htmx-container"
|
|
||||||
{% if not table.embedded %}hx-push-url="true"{% endif %}
|
|
||||||
class="page-link"
|
|
||||||
>
|
|
||||||
{{ p }}
|
{{ p }}
|
||||||
</a>
|
</a>
|
||||||
{% elif p %}
|
{% elif p %}
|
||||||
@ -57,12 +52,7 @@
|
|||||||
{% if page.has_next %}
|
{% if page.has_next %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
{% if htmx %}
|
{% if htmx %}
|
||||||
<a href="#"
|
<a href="#" hx-get="{{ table.htmx_url }}{% querystring request page=page.next_page_number %}" class="page-link">
|
||||||
hx-get="{{ table.htmx_url }}{% querystring request page=page.next_page_number %}"
|
|
||||||
hx-target="closest .htmx-container"
|
|
||||||
{% if not table.embedded %}hx-push-url="true"{% endif %}
|
|
||||||
class="page-link"
|
|
||||||
>
|
|
||||||
<i class="mdi mdi-chevron-right"></i>
|
<i class="mdi mdi-chevron-right"></i>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -97,12 +87,7 @@
|
|||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
{% for n in page.paginator.get_page_lengths %}
|
{% for n in page.paginator.get_page_lengths %}
|
||||||
{% if htmx %}
|
{% if htmx %}
|
||||||
<a href="#"
|
<a href="#" hx-get="{{ table.htmx_url }}{% querystring request per_page=n %}" class="dropdown-item">{{ n }}</a>
|
||||||
hx-get="{{ table.htmx_url }}{% querystring request per_page=n %}"
|
|
||||||
hx-target="closest .htmx-container"
|
|
||||||
{% if not table.embedded %}hx-push-url="true"{% endif %}
|
|
||||||
class="dropdown-item"
|
|
||||||
>{{ n }}</a>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% querystring request per_page=n %}" class="dropdown-item">{{ n }}</a>
|
<a href="{% querystring request per_page=n %}" class="dropdown-item">{{ n }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-auto d-print-none">
|
<div class="col-auto d-print-none">
|
||||||
<div class="input-group input-group-flat me-2 quicksearch">
|
<div class="input-group input-group-flat me-2 quicksearch" hx-disinherit="hx-select hx-swap">
|
||||||
<input type="search" results="5" name="q" id="quicksearch" class="form-control px-2 py-1" placeholder="Quick search"
|
<input type="search" results="5" name="q" id="quicksearch" class="form-control px-2 py-1" placeholder="Quick search"
|
||||||
hx-get="{{ request.full_path }}" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, search" />
|
hx-get="{{ request.full_path }}" hx-target="#object_list" hx-trigger="keyup changed delay:500ms, search" />
|
||||||
<span class="input-group-text py-1">
|
<span class="input-group-text py-1">
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
{% load django_tables2 %}
|
{% load django_tables2 %}
|
||||||
<table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
|
<table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
|
||||||
{% if table.show_header %}
|
{% if table.show_header %}
|
||||||
<thead>
|
<thead
|
||||||
|
hx-target="closest .htmx-container"
|
||||||
|
hx-disinherit="hx-select hx-swap"
|
||||||
|
{% if not table.embedded %} hx-push-url="true"{% endif %}
|
||||||
|
>
|
||||||
<tr>
|
<tr>
|
||||||
{% for column in table.columns %}
|
{% for column in table.columns %}
|
||||||
{% if column.orderable %}
|
{% if column.orderable %}
|
||||||
@ -10,16 +14,12 @@
|
|||||||
<div class="float-end">
|
<div class="float-end">
|
||||||
<a href="#"
|
<a href="#"
|
||||||
hx-get="{{ table.htmx_url }}{% querystring table.prefixed_order_by_field='' %}"
|
hx-get="{{ table.htmx_url }}{% querystring table.prefixed_order_by_field='' %}"
|
||||||
hx-target="closest .htmx-container"
|
|
||||||
{% if not table.embedded %}hx-push-url="true"{% endif %}
|
|
||||||
class="text-danger"
|
class="text-danger"
|
||||||
><i class="mdi mdi-close"></i></a>
|
><i class="mdi mdi-close"></i></a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="#"
|
<a href="#"
|
||||||
hx-get="{{ table.htmx_url }}{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}"
|
hx-get="{{ table.htmx_url }}{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}"
|
||||||
hx-target="closest .htmx-container"
|
|
||||||
{% if not table.embedded %}hx-push-url="true"{% endif %}
|
|
||||||
>{{ column.header }}</a>
|
>{{ column.header }}</a>
|
||||||
</th>
|
</th>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% if request.user.is_authenticated %}
|
|
||||||
<div class="nav-item dropdown">
|
|
||||||
<a href="#" class="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown" aria-label="Open user menu">
|
|
||||||
<div class="d-xl-block ps-2">
|
|
||||||
<div>{{ request.user }}</div>
|
|
||||||
<div class="mt-1 small text-secondary">{% if request.user.is_staff %}Staff{% else %}User{% endif %}</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
|
|
||||||
{% if config.DJANGO_ADMIN_ENABLED and request.user.is_staff %}
|
|
||||||
<a class="dropdown-item" href="{% url 'admin:index' %}">
|
|
||||||
<i class="mdi mdi-cog"></i> {% trans "Django Admin" %}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
<a href="{% url 'account:profile' %}" class="dropdown-item">
|
|
||||||
<i class="mdi mdi-account"></i> {% trans "Profile" %}
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'account:bookmarks' %}" class="dropdown-item">
|
|
||||||
<i class="mdi mdi-bookmark"></i> {% trans "Bookmarks" %}
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'account:preferences' %}" class="dropdown-item">
|
|
||||||
<i class="mdi mdi-wrench"></i> {% trans "Preferences" %}
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'account:usertoken_list' %}" class="dropdown-item">
|
|
||||||
<i class="mdi mdi-key"></i> {% trans "API Tokens" %}
|
|
||||||
</a>
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
<a href="{% url 'logout' %}" class="dropdown-item">
|
|
||||||
<i class="mdi mdi-logout-variant"></i> {% trans "Log Out" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="btn-group ps-2">
|
|
||||||
<a class="btn btn-primary" type="button" href="{% url 'login' %}?next={{ request.path }}">
|
|
||||||
<i class="mdi mdi-login-variant"></i> {% trans "Log In" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
@ -5,7 +5,7 @@
|
|||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
{% if 'bulk_remove_devices' in actions %}
|
{% if 'bulk_remove_devices' in actions %}
|
||||||
<button type="submit" name="_remove"
|
<button type="submit" name="_remove"
|
||||||
formaction="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}?return_url={{ return_url }}"
|
{% formaction %}="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}?return_url={{ return_url }}"
|
||||||
class="btn btn-danger">
|
class="btn btn-danger">
|
||||||
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> {% trans "Remove Selected" %}
|
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> {% trans "Remove Selected" %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
{% if 'bulk_rename' in actions %}
|
{% if 'bulk_rename' in actions %}
|
||||||
<button type="submit" name="_rename"
|
<button type="submit" name="_rename"
|
||||||
formaction="{% url 'virtualization:vminterface_bulk_rename' %}?return_url={{ return_url }}"
|
{% formaction %}="{% url 'virtualization:vminterface_bulk_rename' %}?return_url={{ return_url }}"
|
||||||
class="btn btn-outline-warning">
|
class="btn btn-outline-warning">
|
||||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
{% if 'bulk_rename' in actions %}
|
{% if 'bulk_rename' in actions %}
|
||||||
<button type="submit" name="_rename"
|
<button type="submit" name="_rename"
|
||||||
formaction="{% url 'virtualization:virtualdisk_bulk_rename' %}?return_url={{ return_url }}"
|
{% formaction %}="{% url 'virtualization:virtualdisk_bulk_rename' %}?return_url={{ return_url }}"
|
||||||
class="btn btn-outline-warning">
|
class="btn btn-outline-warning">
|
||||||
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
<i class="mdi mdi-pencil-outline" aria-hidden="true"></i> {% trans "Rename" %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -10,14 +10,14 @@
|
|||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
{% if perms.virtualization.add_vminterface %}
|
{% if perms.virtualization.add_vminterface %}
|
||||||
<li>
|
<li>
|
||||||
<button type="submit" formaction="{% url 'virtualization:virtualmachine_bulk_add_vminterface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
<button type="submit" {% formaction %}="{% url 'virtualization:virtualmachine_bulk_add_vminterface' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||||
{% trans "Interfaces" %}
|
{% trans "Interfaces" %}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.virtualization.add_virtualdisk %}
|
{% if perms.virtualization.add_virtualdisk %}
|
||||||
<li>
|
<li>
|
||||||
<button type="submit" formaction="{% url 'virtualization:virtualmachine_bulk_add_virtualdisk' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
<button type="submit" {% formaction %}="{% url 'virtualization:virtualmachine_bulk_add_virtualdisk' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="dropdown-item">
|
||||||
{% trans "Virtual Disks" %}
|
{% trans "Virtual Disks" %}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
@ -55,7 +55,8 @@ class UserConfigFormMetaclass(forms.models.ModelFormMetaclass):
|
|||||||
class UserConfigForm(forms.ModelForm, metaclass=UserConfigFormMetaclass):
|
class UserConfigForm(forms.ModelForm, metaclass=UserConfigFormMetaclass):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'locale.language', 'pagination.per_page', 'pagination.placement', 'ui.colormode', name=_('User Interface')
|
'locale.language', 'pagination.per_page', 'pagination.placement', 'ui.colormode', 'ui.htmx_navigation',
|
||||||
|
name=_('User Interface')
|
||||||
),
|
),
|
||||||
FieldSet('data_format', name=_('Miscellaneous')),
|
FieldSet('data_format', name=_('Miscellaneous')),
|
||||||
)
|
)
|
||||||
|
13
netbox/utilities/htmx.py
Normal file
13
netbox/utilities/htmx.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
__all__ = (
|
||||||
|
'htmx_partial',
|
||||||
|
)
|
||||||
|
|
||||||
|
PAGE_CONTAINER_ID = 'page-content'
|
||||||
|
|
||||||
|
|
||||||
|
def htmx_partial(request):
|
||||||
|
"""
|
||||||
|
Determines whether to render partial (versus complete) HTML content
|
||||||
|
in response to an HTMX request, based on the target element.
|
||||||
|
"""
|
||||||
|
return request.htmx and request.htmx.target != PAGE_CONTAINER_ID
|
@ -1,4 +1,5 @@
|
|||||||
<div class="card-body htmx-container table-responsive p-0"
|
<div class="card-body htmx-container table-responsive p-0"
|
||||||
hx-get="{% url viewname %}{% if url_params %}?{{ url_params.urlencode }}{% endif %}"
|
hx-get="{% url viewname %}{% if url_params %}?{{ url_params.urlencode }}{% endif %}"
|
||||||
hx-trigger="load"
|
hx-target="this"
|
||||||
|
hx-trigger="load" hx-select="table" hx-swap="innerHTML"
|
||||||
></div>
|
></div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% if url %}
|
{% if url %}
|
||||||
<button type="submit" name="_delete" formaction="{{ url }}" class="btn btn-red">
|
<button type="submit" name="_delete" {% formaction %}="{{ url }}" class="btn btn-red">
|
||||||
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete Selected" %}
|
<i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> {% trans "Delete Selected" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% if url %}
|
{% if url %}
|
||||||
<button type="submit" name="_edit" formaction="{{ url }}" class="btn btn-yellow">
|
<button type="submit" name="_edit" {% formaction %}="{{ url }}" class="btn btn-yellow">
|
||||||
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit Selected" %}
|
<i class="mdi mdi-pencil" aria-hidden="true"></i> {% trans "Edit Selected" %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
<a href="#"
|
<a href="#"
|
||||||
hx-get="{{ url }}"
|
hx-get="{{ url }}"
|
||||||
hx-target="#htmx-modal-content"
|
hx-target="#htmx-modal-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-select="form"
|
||||||
class="btn btn-red"
|
class="btn btn-red"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
data-bs-target="#htmx-modal"
|
data-bs-target="#htmx-modal"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
{% load navigation %}
|
||||||
|
|
||||||
<ul class="navbar-nav pt-lg-2">
|
<ul class="navbar-nav pt-lg-2" {% htmx_boost %}>
|
||||||
{% for menu, groups in nav_items %}
|
{% for menu, groups in nav_items %}
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ __all__ = (
|
|||||||
'checkmark',
|
'checkmark',
|
||||||
'copy_content',
|
'copy_content',
|
||||||
'customfield_value',
|
'customfield_value',
|
||||||
|
'formaction',
|
||||||
'tag',
|
'tag',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -113,3 +114,14 @@ def htmx_table(context, viewname, return_url=None, **kwargs):
|
|||||||
'viewname': viewname,
|
'viewname': viewname,
|
||||||
'url_params': url_params,
|
'url_params': url_params,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def formaction(context):
|
||||||
|
"""
|
||||||
|
Replace the 'formaction' attribute on an HTML element with the appropriate HTMX attributes
|
||||||
|
if HTMX navigation is enabled (per the user's preferences).
|
||||||
|
"""
|
||||||
|
if context.get('htmx_navigation', False):
|
||||||
|
return 'hx-push-url="true" hx-post'
|
||||||
|
return 'formaction'
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
from typing import Dict
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.template import Context
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from netbox.navigation.menu import MENUS
|
from netbox.navigation.menu import MENUS
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'nav',
|
'nav',
|
||||||
|
'htmx_boost',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ register = template.Library()
|
|||||||
|
|
||||||
|
|
||||||
@register.inclusion_tag("navigation/menu.html", takes_context=True)
|
@register.inclusion_tag("navigation/menu.html", takes_context=True)
|
||||||
def nav(context: Context) -> Dict:
|
def nav(context):
|
||||||
"""
|
"""
|
||||||
Render the navigation menu.
|
Render the navigation menu.
|
||||||
"""
|
"""
|
||||||
@ -40,6 +40,31 @@ def nav(context: Context) -> Dict:
|
|||||||
nav_items.append((menu, groups))
|
nav_items.append((menu, groups))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"nav_items": nav_items,
|
'nav_items': nav_items,
|
||||||
"request": context["request"]
|
'htmx_navigation': context['htmx_navigation']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag(takes_context=True)
|
||||||
|
def htmx_boost(context, target='#page-content', select='#page-content'):
|
||||||
|
"""
|
||||||
|
Renders the HTML attributes needed to effect HTMX boosting within an element if
|
||||||
|
HTMX navigation is enabled for the request. The target and select parameters are
|
||||||
|
rendered as `hx-target` and `hx-select`, respectively. For example:
|
||||||
|
|
||||||
|
<div id="page-content" {% htmx_boost %}>
|
||||||
|
|
||||||
|
If HTMX navigation is not enabled, the tag renders no content.
|
||||||
|
"""
|
||||||
|
if not context.get('htmx_navigation', False):
|
||||||
|
return ''
|
||||||
|
hx_params = {
|
||||||
|
'hx-boost': 'true',
|
||||||
|
'hx-target': target,
|
||||||
|
'hx-select': select,
|
||||||
|
'hx-swap': 'outerHTML show:window:top',
|
||||||
|
}
|
||||||
|
htmx_params = ' '.join([
|
||||||
|
f'{k}="{v}"' for k, v in hx_params.items()
|
||||||
|
])
|
||||||
|
return mark_safe(htmx_params)
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import decimal
|
||||||
|
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
@ -112,7 +114,7 @@ class VirtualMachine(ContactsMixin, RenderConfigMixin, ConfigContextModel, Prima
|
|||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('vCPUs'),
|
verbose_name=_('vCPUs'),
|
||||||
validators=(
|
validators=(
|
||||||
MinValueValidator(0.01),
|
MinValueValidator(decimal.Decimal(0.01)),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
memory = models.PositiveIntegerField(
|
memory = models.PositiveIntegerField(
|
||||||
|
@ -47,7 +47,8 @@ class TunnelSerializer(NetBoxModelSerializer):
|
|||||||
group = TunnelGroupSerializer(
|
group = TunnelGroupSerializer(
|
||||||
nested=True,
|
nested=True,
|
||||||
required=False,
|
required=False,
|
||||||
allow_null=True
|
allow_null=True,
|
||||||
|
default=None
|
||||||
)
|
)
|
||||||
encapsulation = ChoiceField(
|
encapsulation = ChoiceField(
|
||||||
choices=TunnelEncapsulationChoices
|
choices=TunnelEncapsulationChoices
|
||||||
|
@ -1,28 +1,28 @@
|
|||||||
Django==5.0.3
|
Django==5.0.3
|
||||||
django-cors-headers==4.3.1
|
django-cors-headers==4.3.1
|
||||||
django-debug-toolbar==4.3.0
|
django-debug-toolbar==4.3.0
|
||||||
django-filter==24.1
|
django-filter==24.2
|
||||||
django-htmx==1.17.3
|
django-htmx==1.17.3
|
||||||
django-mptt==0.14.0
|
django-mptt==0.16.0
|
||||||
django-pglocks==1.0.4
|
django-pglocks==1.0.4
|
||||||
django-prometheus==2.3.1
|
django-prometheus==2.3.1
|
||||||
django-redis==5.4.0
|
django-redis==5.4.0
|
||||||
django-rich==1.8.0
|
django-rich==1.8.0
|
||||||
django-rq==2.10.1
|
django-rq==2.10.2
|
||||||
django-taggit==5.0.1
|
django-taggit==5.0.1
|
||||||
django-tables2==2.7.0
|
django-tables2==2.7.0
|
||||||
django-timezone-field==6.1.0
|
django-timezone-field==6.1.0
|
||||||
djangorestframework==3.14.0
|
djangorestframework==3.15.1
|
||||||
drf-spectacular==0.27.1
|
drf-spectacular==0.27.1
|
||||||
drf-spectacular-sidecar==2024.3.4
|
drf-spectacular-sidecar==2024.3.4
|
||||||
feedparser==6.0.11
|
feedparser==6.0.11
|
||||||
gunicorn==21.2.0
|
gunicorn==21.2.0
|
||||||
Jinja2==3.1.3
|
Jinja2==3.1.3
|
||||||
Markdown==3.5.2
|
Markdown==3.6
|
||||||
mkdocs-material==9.5.13
|
mkdocs-material==9.5.15
|
||||||
mkdocstrings[python-legacy]==0.24.1
|
mkdocstrings[python-legacy]==0.24.1
|
||||||
netaddr==1.2.1
|
netaddr==1.2.1
|
||||||
nh3==0.2.15
|
nh3==0.2.17
|
||||||
Pillow==10.2.0
|
Pillow==10.2.0
|
||||||
psycopg[c,pool]==3.1.18
|
psycopg[c,pool]==3.1.18
|
||||||
PyYAML==6.0.1
|
PyYAML==6.0.1
|
||||||
@ -32,5 +32,5 @@ social-auth-core[openidconnect]==4.5.3
|
|||||||
strawberry-graphql==0.221.1
|
strawberry-graphql==0.221.1
|
||||||
strawberry-graphql-django==0.34.0
|
strawberry-graphql-django==0.34.0
|
||||||
svgwrite==1.4.3
|
svgwrite==1.4.3
|
||||||
tablib==3.5.0
|
tablib==3.6.0
|
||||||
tzdata==2024.1
|
tzdata==2024.1
|
||||||
|
Loading…
Reference in New Issue
Block a user