diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 672f43b09..5e456d0df 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.5.0 + placeholder: v3.5.3 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 1f8fdebd4..e6a5e76c2 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,10 +3,13 @@ blank_issues_enabled: false contact_links: - name: 📖 Contributing Policy url: https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md - about: "Please read through our contributing policy before opening an issue or pull request" + about: "Please read through our contributing policy before opening an issue or pull request." - name: ❓ Discussion url: https://github.com/netbox-community/netbox/discussions - about: "If you're just looking for help, try starting a discussion instead" + about: "If you're just looking for help, try starting a discussion instead." + - name: 💡 Plugin Idea + url: https://plugin-ideas.netbox.dev + about: "Have an idea for a plugin? Head over to the ideas board!" - name: 💬 Community Slack - url: https://netdev.chat/ - about: "Join #netbox on the NetDev Community Slack for assistance with installation issues and other problems" + url: https://netdev.chat + about: "Join #netbox on the NetDev Community Slack for assistance with installation issues and other problems." diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 842454d6b..e317dd64c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.5.0 + placeholder: v3.5.3 validations: required: true - type: dropdown diff --git a/README.md b/README.md index 480f0f856..6e2b34fb8 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@
The premiere source of truth powering network automation
+00ff00
)')),
}
+
+
+class JournalEntryImportForm(NetBoxModelImportForm):
+ assigned_object_type = CSVContentTypeField(
+ queryset=ContentType.objects.all(),
+ label=_('Assigned object type'),
+ )
+ kind = CSVChoiceField(
+ choices=JournalEntryKindChoices,
+ help_text=_('The classification of entry')
+ )
+
+ class Meta:
+ model = JournalEntry
+ fields = (
+ 'assigned_object_type', 'assigned_object_id', 'created_by', 'kind', 'comments', 'tags'
+ )
diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py
index 056302343..fae15d041 100644
--- a/netbox/extras/forms/filtersets.py
+++ b/netbox/extras/forms/filtersets.py
@@ -11,7 +11,7 @@ from extras.utils import FeatureQuery
from netbox.forms.base import NetBoxModelFilterSetForm
from tenancy.models import Tenant, TenantGroup
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
-from utilities.forms.fields import ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField
+from utilities.forms.fields import ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.widgets import APISelectMultiple, DateTimePicker
from virtualization.models import Cluster, ClusterGroup, ClusterType
from .mixins import SavedFiltersMixin
@@ -22,6 +22,7 @@ __all__ = (
'CustomFieldFilterForm',
'CustomLinkFilterForm',
'ExportTemplateFilterForm',
+ 'ImageAttachmentFilterForm',
'JournalEntryFilterForm',
'LocalConfigContextFilterForm',
'ObjectChangeFilterForm',
@@ -137,6 +138,20 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
)
+class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
+ fieldsets = (
+ (None, ('q', 'filter_id')),
+ ('Attributes', ('content_type_id', 'name',)),
+ )
+ content_type_id = ContentTypeChoiceField(
+ queryset=ContentType.objects.filter(FeatureQuery('custom_fields').get_query()),
+ required=False
+ )
+ name = forms.CharField(
+ required=False
+ )
+
+
class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
diff --git a/netbox/extras/lookups.py b/netbox/extras/lookups.py
index 77fe2301e..a8d89c943 100644
--- a/netbox/extras/lookups.py
+++ b/netbox/extras/lookups.py
@@ -7,12 +7,14 @@ class Empty(Lookup):
Filter on whether a string is empty.
"""
lookup_name = 'empty'
+ prepare_rhs = False
- def as_sql(self, qn, connection):
- lhs, lhs_params = self.process_lhs(qn, connection)
- rhs, rhs_params = self.process_rhs(qn, connection)
- params = lhs_params + rhs_params
- return 'CAST(LENGTH(%s) AS BOOLEAN) != %s' % (lhs, rhs), params
+ def as_sql(self, compiler, connection):
+ sql, params = compiler.compile(self.lhs)
+ if self.rhs:
+ return f"CAST(LENGTH({sql}) AS BOOLEAN) IS NOT TRUE", params
+ else:
+ return f"CAST(LENGTH({sql}) AS BOOLEAN) IS TRUE", params
class NetContainsOrEquals(Lookup):
diff --git a/netbox/extras/management/commands/runscript.py b/netbox/extras/management/commands/runscript.py
index 76ceeb239..b42e9b47d 100644
--- a/netbox/extras/management/commands/runscript.py
+++ b/netbox/extras/management/commands/runscript.py
@@ -111,7 +111,7 @@ class Command(BaseCommand):
# Create the job
job = Job.objects.create(
- instance=module,
+ object=module,
name=script.name,
user=User.objects.filter(is_superuser=True).order_by('pk')[0],
job_id=uuid.uuid4()
diff --git a/netbox/extras/migrations/0066_customfield_name_validation.py b/netbox/extras/migrations/0066_customfield_name_validation.py
index 7a768c10c..3d2c51399 100644
--- a/netbox/extras/migrations/0066_customfield_name_validation.py
+++ b/netbox/extras/migrations/0066_customfield_name_validation.py
@@ -13,6 +13,22 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='customfield',
name='name',
- field=models.CharField(max_length=50, unique=True, validators=[django.core.validators.RegexValidator(flags=re.RegexFlag['IGNORECASE'], message='Only alphanumeric characters and underscores are allowed.', regex='^[a-z0-9_]+$')]),
+ field=models.CharField(
+ max_length=50,
+ unique=True,
+ validators=[
+ django.core.validators.RegexValidator(
+ flags=re.RegexFlag['IGNORECASE'],
+ message='Only alphanumeric characters and underscores are allowed.',
+ regex='^[a-z0-9_]+$',
+ ),
+ django.core.validators.RegexValidator(
+ flags=re.RegexFlag['IGNORECASE'],
+ inverse_match=True,
+ message='Double underscores are not permitted in custom field names.',
+ regex=r'__',
+ ),
+ ],
+ ),
),
]
diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py
index 439d15edc..be3540f08 100644
--- a/netbox/extras/models/customfields.py
+++ b/netbox/extras/models/customfields.py
@@ -85,6 +85,12 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
message="Only alphanumeric characters and underscores are allowed.",
flags=re.IGNORECASE
),
+ RegexValidator(
+ regex=r'__',
+ message="Double underscores are not permitted in custom field names.",
+ flags=re.IGNORECASE,
+ inverse_match=True
+ ),
)
)
label = models.CharField(
diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py
index 16e4fb577..9433ab6b0 100644
--- a/netbox/extras/models/models.py
+++ b/netbox/extras/models/models.py
@@ -274,10 +274,10 @@ class CustomLink(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
:param context: The context passed to Jinja2
"""
- text = render_jinja2(self.link_text, context)
+ text = render_jinja2(self.link_text, context).strip()
if not text:
return {}
- link = render_jinja2(self.link_url, context)
+ link = render_jinja2(self.link_url, context).strip()
link_target = ' target="_blank"' if self.new_window else ''
# Sanitize link text
diff --git a/netbox/extras/models/scripts.py b/netbox/extras/models/scripts.py
index 1a7559e53..de48aae8e 100644
--- a/netbox/extras/models/scripts.py
+++ b/netbox/extras/models/scripts.py
@@ -1,4 +1,5 @@
import inspect
+import logging
from functools import cached_property
from django.db import models
@@ -16,6 +17,8 @@ __all__ = (
'ScriptModule',
)
+logger = logging.getLogger('netbox.data_backends')
+
class Script(WebhooksMixin, models.Model):
"""
@@ -53,7 +56,12 @@ class ScriptModule(PythonModuleMixin, JobsMixin, ManagedFile):
# For child objects in submodules use the full import path w/o the root module as the name
return cls.full_name.split(".", maxsplit=1)[1]
- module = self.get_module()
+ try:
+ module = self.get_module()
+ except Exception as e:
+ logger.debug(f"Failed to load script: {self.python_name} error: {e}")
+ module = None
+
scripts = {}
ordered = getattr(module, 'script_order', [])
diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py
index 59b45c059..9e4924532 100644
--- a/netbox/extras/tables/tables.py
+++ b/netbox/extras/tables/tables.py
@@ -13,6 +13,7 @@ __all__ = (
'CustomFieldTable',
'CustomLinkTable',
'ExportTemplateTable',
+ 'ImageAttachmentTable',
'JournalEntryTable',
'ObjectChangeTable',
'SavedFilterTable',
@@ -21,6 +22,14 @@ __all__ = (
'WebhookTable',
)
+IMAGEATTACHMENT_IMAGE = '''
+{% if record.image %}
+ {{ record }}
+{% else %}
+ —
+{% endif %}
+'''
+
class CustomFieldTable(NetBoxTable):
name = tables.Column(
@@ -72,6 +81,7 @@ class ExportTemplateTable(NetBoxTable):
linkify=True
)
is_synced = columns.BooleanColumn(
+ orderable=False,
verbose_name='Synced'
)
@@ -86,6 +96,31 @@ class ExportTemplateTable(NetBoxTable):
)
+class ImageAttachmentTable(NetBoxTable):
+ id = tables.Column(
+ linkify=False
+ )
+ content_type = columns.ContentTypeColumn()
+ parent = tables.Column(
+ linkify=True
+ )
+ image = tables.TemplateColumn(
+ template_code=IMAGEATTACHMENT_IMAGE,
+ )
+ size = tables.Column(
+ orderable=False,
+ verbose_name='Size (bytes)'
+ )
+
+ class Meta(NetBoxTable.Meta):
+ model = ImageAttachment
+ fields = (
+ 'pk', 'content_type', 'parent', 'image', 'name', 'image_height', 'image_width', 'size', 'created',
+ 'last_updated',
+ )
+ default_columns = ('content_type', 'parent', 'image', 'name', 'size', 'created')
+
+
class SavedFilterTable(NetBoxTable):
name = tables.Column(
linkify=True
@@ -195,6 +230,7 @@ class ConfigContextTable(NetBoxTable):
verbose_name='Active'
)
is_synced = columns.BooleanColumn(
+ orderable=False,
verbose_name='Synced'
)
@@ -219,6 +255,7 @@ class ConfigTemplateTable(NetBoxTable):
linkify=True
)
is_synced = columns.BooleanColumn(
+ orderable=False,
verbose_name='Synced'
)
tags = columns.TagColumn(
diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py
index 6a3a3d074..3fd0dc83e 100644
--- a/netbox/extras/tests/test_customfields.py
+++ b/netbox/extras/tests/test_customfields.py
@@ -29,6 +29,17 @@ class CustomFieldTest(TestCase):
cls.object_type = ContentType.objects.get_for_model(Site)
+ def test_invalid_name(self):
+ """
+ Try creating a CustomField with an invalid name.
+ """
+ with self.assertRaises(ValidationError):
+ # Invalid character
+ CustomField(name='?', type=CustomFieldTypeChoices.TYPE_TEXT).full_clean()
+ with self.assertRaises(ValidationError):
+ # Double underscores not permitted
+ CustomField(name='foo__bar', type=CustomFieldTypeChoices.TYPE_TEXT).full_clean()
+
def test_text_field(self):
value = 'Foobar!'
diff --git a/netbox/extras/urls.py b/netbox/extras/urls.py
index f04c53add..c4fc3d938 100644
--- a/netbox/extras/urls.py
+++ b/netbox/extras/urls.py
@@ -73,6 +73,7 @@ urlpatterns = [
path('config-templates/Height | ++ {{ object.device_type.u_height }}U + | +
---|---|
Weight | ++ {% if object.total_weight %} + {{ object.total_weight|floatformat }} Kilograms + {% else %} + {{ ''|placeholder }} + {% endif %} + | +
{{ context_data|pprint }}+
{{ context_data|pprint }}+
MAC Address | -{{ object.mac_address|placeholder }} | +{{ object.mac_address|placeholder }} | |||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
WWN | -{{ object.wwn|placeholder }} | +{{ object.wwn|placeholder }} | |||||||||||||||||||||||||||||||||||||||||||||||
VRF | diff --git a/netbox/templates/dcim/location.html b/netbox/templates/dcim/location.html index 193d93f9a..795aeb35f 100644 --- a/netbox/templates/dcim/location.html +++ b/netbox/templates/dcim/location.html @@ -65,7 +65,6 @@Physical Address | {% if object.physical_address %} - {{ object.physical_address|linebreaksbr }} {% else %} {{ ''|placeholder }} @@ -106,11 +108,13 @@ | + {% if config.MAPS_URL %} + + {% endif %}GPS Coordinates |
{% if object.latitude and object.longitude %}
- {{ object.latitude }}, {{ object.longitude }}
{% else %}
{{ ''|placeholder }}
@@ -127,7 +131,6 @@
{% include 'inc/panels/related_objects.html' with filter_name='site_id' %}
- {% include 'inc/panels/contacts.html' %}
Locations
diff --git a/netbox/templates/dcim/sitegroup.html b/netbox/templates/dcim/sitegroup.html
index 2cf8e7168..819022a34 100644
--- a/netbox/templates/dcim/sitegroup.html
+++ b/netbox/templates/dcim/sitegroup.html
@@ -42,7 +42,6 @@
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/contacts.html' %}
{% plugin_left_page object %}
diff --git a/netbox/templates/extras/dashboard/widgets/objectcounts.html b/netbox/templates/extras/dashboard/widgets/objectcounts.html
index d0e604c9a..8b68dc166 100644
--- a/netbox/templates/extras/dashboard/widgets/objectcounts.html
+++ b/netbox/templates/extras/dashboard/widgets/objectcounts.html
@@ -1,10 +1,8 @@
-{% load helpers %}
-
{% if counts %}
- {% for model, count in counts %}
+ {% for model, count, url in counts %}
{% if count != None %}
-
+
{{ model|meta:"verbose_name_plural"|bettertitle }}
{% empty %}
diff --git a/netbox/templates/inc/panels/contacts.html b/netbox/templates/inc/panels/contacts.html
deleted file mode 100644
index 359ad8d7e..000000000
--- a/netbox/templates/inc/panels/contacts.html
+++ /dev/null
@@ -1,63 +0,0 @@
-{% load helpers %}
-
-{{ count }}diff --git a/netbox/templates/extras/script_list.html b/netbox/templates/extras/script_list.html index bccbce589..9a67e2b10 100644 --- a/netbox/templates/extras/script_list.html +++ b/netbox/templates/extras/script_list.html @@ -37,43 +37,49 @@
{% include 'inc/sync_warning.html' with object=module %}
-
+ Script file at: {{module.full_path}} could not be loaded.
+
+ {% else %}
+
-
diff --git a/netbox/templates/inc/panels/custom_fields.html b/netbox/templates/inc/panels/custom_fields.html
index 45843eea5..7f5f4cc27 100644
--- a/netbox/templates/inc/panels/custom_fields.html
+++ b/netbox/templates/inc/panels/custom_fields.html
@@ -12,8 +12,15 @@
Contacts-
- {% with contacts=object.contacts.all %}
- {% if contacts.exists %}
-
- {% if perms.tenancy.add_contactassignment %}
-
- {% endif %}
-
None
- {% endif %}
- {% endwith %}
-
|
+ {% if config.MAPS_URL %}
+
+ {% endif %}