@@ -19,9 +19,7 @@
Key |
-
-
-
+ {% copy_content "token_id" %}
{{ key }}
|
diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html
index 51fd8aa80..3d3b498ad 100644
--- a/netbox/templates/virtualization/virtualmachine.html
+++ b/netbox/templates/virtualization/virtualmachine.html
@@ -46,12 +46,13 @@
Primary IPv4 |
{% if object.primary_ip4 %}
- {{ object.primary_ip4.address.ip }}
+ {{ object.primary_ip4.address.ip }}
{% if object.primary_ip4.nat_inside %}
(NAT for {{ object.primary_ip4.nat_inside.address.ip }})
{% elif object.primary_ip4.nat_outside.exists %}
(NAT: {% for nat in object.primary_ip4.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %})
{% endif %}
+ {% copy_content "primary_ip4" %}
{% else %}
{{ ''|placeholder }}
{% endif %}
@@ -61,12 +62,13 @@
| Primary IPv6 |
{% if object.primary_ip6 %}
- {{ object.primary_ip6.address.ip }}
+ {{ object.primary_ip6.address.ip }}
{% if object.primary_ip6.nat_inside %}
(NAT for {{ object.primary_ip6.nat_inside.address.ip }})
{% elif object.primary_ip6.nat_outside.exists %}
(NAT: {% for nat in object.primary_ip6.nat_outside.all %}{{ nat.address.ip }}{% if not forloop.last %}, {% endif %}{% endfor %})
{% endif %}
+ {% copy_content "primary_ip6" %}
{% else %}
{{ ''|placeholder }}
{% endif %}
diff --git a/netbox/tenancy/models/contacts.py b/netbox/tenancy/models/contacts.py
index 440541b5f..1df5e3305 100644
--- a/netbox/tenancy/models/contacts.py
+++ b/netbox/tenancy/models/contacts.py
@@ -136,3 +136,8 @@ class ContactAssignment(ChangeLoggedModel):
def get_absolute_url(self):
return reverse('tenancy:contact', args=[self.contact.pk])
+
+ def to_objectchange(self, action):
+ objectchange = super().to_objectchange(action)
+ objectchange.related_object = self.object
+ return objectchange
diff --git a/netbox/tenancy/tables/contacts.py b/netbox/tenancy/tables/contacts.py
index 0c697af79..7de8ffceb 100644
--- a/netbox/tenancy/tables/contacts.py
+++ b/netbox/tenancy/tables/contacts.py
@@ -1,4 +1,5 @@
import django_tables2 as tables
+from django_tables2.utils import Accessor
from netbox.tables import NetBoxTable, columns
from tenancy.models import *
@@ -90,11 +91,40 @@ class ContactAssignmentTable(NetBoxTable):
role = tables.Column(
linkify=True
)
+ contact_title = tables.Column(
+ accessor=Accessor('contact__title'),
+ verbose_name='Contact Title'
+ )
+ contact_phone = tables.Column(
+ accessor=Accessor('contact__phone'),
+ verbose_name='Contact Phone'
+ )
+ contact_email = tables.Column(
+ accessor=Accessor('contact__email'),
+ verbose_name='Contact Email'
+ )
+ contact_address = tables.Column(
+ accessor=Accessor('contact__address'),
+ verbose_name='Contact Address'
+ )
+ contact_link = tables.Column(
+ accessor=Accessor('contact__link'),
+ verbose_name='Contact Link'
+ )
+ contact_description = tables.Column(
+ accessor=Accessor('contact__description'),
+ verbose_name='Contact Description'
+ )
actions = columns.ActionsColumn(
actions=('edit', 'delete')
)
class Meta(NetBoxTable.Meta):
model = ContactAssignment
- fields = ('pk', 'content_type', 'object', 'contact', 'role', 'priority', 'actions')
- default_columns = ('pk', 'content_type', 'object', 'contact', 'role', 'priority')
+ fields = (
+ 'pk', 'content_type', 'object', 'contact', 'role', 'priority', 'contact_title', 'contact_phone',
+ 'contact_email', 'contact_address', 'contact_link', 'contact_description', 'actions'
+ )
+ default_columns = (
+ 'pk', 'content_type', 'object', 'contact', 'role', 'priority', 'contact_email', 'contact_phone'
+ )
diff --git a/netbox/users/tables.py b/netbox/users/tables.py
index 0f1484887..cea50b10f 100644
--- a/netbox/users/tables.py
+++ b/netbox/users/tables.py
@@ -12,9 +12,7 @@ ALLOWED_IPS = """{{ value|join:", " }}"""
COPY_BUTTON = """
{% if settings.ALLOW_TOKEN_RETRIEVAL %}
-
-
-
+ {% copy_content record.pk prefix="token_" color="success" %}
{% endif %}
"""
diff --git a/netbox/users/views.py b/netbox/users/views.py
index 4dcdaebab..ad80fdfe5 100644
--- a/netbox/users/views.py
+++ b/netbox/users/views.py
@@ -160,7 +160,9 @@ class ProfileView(LoginRequiredMixin, View):
def get(self, request):
# Compile changelog table
- changelog = ObjectChange.objects.restrict(request.user, 'view').filter(user=request.user).prefetch_related(
+ changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
+ user=request.user
+ ).prefetch_related(
'changed_object_type'
)[:20]
changelog_table = ObjectChangeTable(changelog)
diff --git a/netbox/utilities/templates/builtins/copy_content.html b/netbox/utilities/templates/builtins/copy_content.html
new file mode 100644
index 000000000..9025a71a1
--- /dev/null
+++ b/netbox/utilities/templates/builtins/copy_content.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/netbox/utilities/templatetags/builtins/tags.py b/netbox/utilities/templatetags/builtins/tags.py
index dc86586e7..35aec1000 100644
--- a/netbox/utilities/templatetags/builtins/tags.py
+++ b/netbox/utilities/templatetags/builtins/tags.py
@@ -1,9 +1,12 @@
from django import template
from django.http import QueryDict
+from utilities.utils import dict_to_querydict
+
__all__ = (
'badge',
'checkmark',
+ 'copy_content',
'customfield_value',
'tag',
)
@@ -77,6 +80,17 @@ def checkmark(value, show_false=True, true='Yes', false='No'):
}
+@register.inclusion_tag('builtins/copy_content.html')
+def copy_content(target, prefix=None, color='primary'):
+ """
+ Display a copy button to copy the content of a field.
+ """
+ return {
+ 'target': f'#{prefix or ""}{target}',
+ 'color': f'btn-{color}'
+ }
+
+
@register.inclusion_tag('builtins/htmx_table.html', takes_context=True)
def htmx_table(context, viewname, return_url=None, **kwargs):
"""
@@ -87,8 +101,7 @@ def htmx_table(context, viewname, return_url=None, **kwargs):
viewname: The name of the view to use for the HTMX request (e.g. `dcim:site_list`)
return_url: The URL to pass as the `return_url`. If not provided, the current request's path will be used.
"""
- url_params = QueryDict(mutable=True)
- url_params.update(kwargs)
+ url_params = dict_to_querydict(kwargs)
url_params['return_url'] = return_url or context['request'].path
return {
'viewname': viewname,
diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py
index 4b4a2631a..114397dae 100644
--- a/netbox/utilities/utils.py
+++ b/netbox/utilities/utils.py
@@ -11,8 +11,9 @@ from django.core import serializers
from django.db.models import Count, OuterRef, Subquery
from django.db.models.functions import Coalesce
from django.http import QueryDict
-from django.utils.html import escape
from django.utils import timezone
+from django.utils.datastructures import MultiValueDict
+from django.utils.html import escape
from django.utils.timezone import localtime
from jinja2.sandbox import SandboxedEnvironment
from mptt.models import MPTTModel
@@ -231,6 +232,19 @@ def dict_to_filter_params(d, prefix=''):
return params
+def dict_to_querydict(d, mutable=True):
+ """
+ Create a QueryDict instance from a regular Python dictionary.
+ """
+ qd = QueryDict(mutable=True)
+ for k, v in d.items():
+ item = MultiValueDict({k: v}) if isinstance(v, (list, tuple, set)) else {k: v}
+ qd.update(item)
+ if not mutable:
+ qd._mutable = False
+ return qd
+
+
def normalize_querydict(querydict):
"""
Convert a QueryDict to a normal, mutable dictionary, preserving list values. For example,
diff --git a/requirements.txt b/requirements.txt
index 2ffcd852b..f707c60c5 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
bleach==6.0.0
-boto3==1.26.156
+boto3==1.28.1
Django==4.2.2
-django-cors-headers==4.1.0
+django-cors-headers==4.2.0
django-debug-toolbar==4.1.0
django-filter==23.2
django-graphiql-debug-toolbar==0.2.0
@@ -9,27 +9,27 @@ django-mptt==0.14
django-pglocks==1.0.4
django-prometheus==2.3.1
django-redis==5.3.0
-django-rich==1.6.0
+django-rich==1.7.0
django-rq==2.8.1
-django-tables2==2.5.3
+django-tables2==2.6.0
django-taggit==4.0.0
django-timezone-field==5.1
djangorestframework==3.14.0
-drf-spectacular==0.26.2
-drf-spectacular-sidecar==2023.6.1
+drf-spectacular==0.26.3
+drf-spectacular-sidecar==2023.7.1
dulwich==0.21.5
feedparser==6.0.10
graphene-django==3.0.0
gunicorn==20.1.0
Jinja2==3.1.2
Markdown==3.3.7
-mkdocs-material==9.1.16
+mkdocs-material==9.1.18
mkdocstrings[python-legacy]==0.22.0
netaddr==0.8.0
-Pillow==9.5.0
+Pillow==10.0.0
psycopg[binary,pool]==3.1.9
PyYAML==6.0
-sentry-sdk==1.25.1
+sentry-sdk==1.28.0
social-auth-app-django==5.2.0
social-auth-core[openidconnect]==4.4.2
svgwrite==1.4.3
|