From 5517963b24eb2ecb28bbc98688ffd76820ebb908 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 28 Feb 2023 13:33:40 -0500 Subject: [PATCH] Closes #10729: Add date & time custom field type (#11857) * Add datetime custom field type * Update custom field tests --- docs/customization/custom-fields.md | 1 + docs/release-notes/version-3.5.md | 1 + netbox/extras/choices.py | 2 + netbox/extras/models/customfields.py | 32 +++++- netbox/extras/tests/test_customfields.py | 98 ++++++++++++++----- netbox/extras/tests/test_forms.py | 3 + .../templates/builtins/customfield_value.html | 2 + 7 files changed, 108 insertions(+), 31 deletions(-) diff --git a/docs/customization/custom-fields.md b/docs/customization/custom-fields.md index 7dc82e179b..612faefed7 100644 --- a/docs/customization/custom-fields.md +++ b/docs/customization/custom-fields.md @@ -16,6 +16,7 @@ Custom fields may be created by navigating to Customization > Custom Fields. Net * Decimal: A fixed-precision decimal number (4 decimal places) * Boolean: True or false * Date: A date in ISO 8601 format (YYYY-MM-DD) +* Date & time: A date and time in ISO 8601 format (YYYY-MM-DD HH:MM:SS) * URL: This will be presented as a link in the web UI * JSON: Arbitrary data stored in JSON format * Selection: A selection of one of several pre-defined custom choices diff --git a/docs/release-notes/version-3.5.md b/docs/release-notes/version-3.5.md index 6cbdefe185..50ce41c788 100644 --- a/docs/release-notes/version-3.5.md +++ b/docs/release-notes/version-3.5.md @@ -28,6 +28,7 @@ A new ASN range model has been introduced to facilitate the provisioning of new * [#9073](https://github.com/netbox-community/netbox/issues/9073) - Enable syncing config context data from remote sources * [#9653](https://github.com/netbox-community/netbox/issues/9653) - Enable setting a default platform for device types +* [#10729](https://github.com/netbox-community/netbox/issues/10729) - Add date & time custom field type * [#11254](https://github.com/netbox-community/netbox/issues/11254) - Introduce the `X-Request-ID` HTTP header to annotate the unique ID of each request for change logging * [#11440](https://github.com/netbox-community/netbox/issues/11440) - Add an `enabled` field for device type interfaces * [#11517](https://github.com/netbox-community/netbox/issues/11517) - Standardize the inclusion of related objects across the entire UI diff --git a/netbox/extras/choices.py b/netbox/extras/choices.py index 92d09e2ad7..878d9df6a3 100644 --- a/netbox/extras/choices.py +++ b/netbox/extras/choices.py @@ -13,6 +13,7 @@ class CustomFieldTypeChoices(ChoiceSet): TYPE_DECIMAL = 'decimal' TYPE_BOOLEAN = 'boolean' TYPE_DATE = 'date' + TYPE_DATETIME = 'datetime' TYPE_URL = 'url' TYPE_JSON = 'json' TYPE_SELECT = 'select' @@ -27,6 +28,7 @@ class CustomFieldTypeChoices(ChoiceSet): (TYPE_DECIMAL, 'Decimal'), (TYPE_BOOLEAN, 'Boolean (true/false)'), (TYPE_DATE, 'Date'), + (TYPE_DATETIME, 'Date & time'), (TYPE_URL, 'URL'), (TYPE_JSON, 'JSON'), (TYPE_SELECT, 'Selection'), diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 8141ca76dc..f5ec3ce947 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -25,7 +25,7 @@ DynamicModelMultipleChoiceField, JSONField, LaxURLField, ) from utilities.forms.utils import add_blank_choice -from utilities.forms.widgets import DatePicker +from utilities.forms.widgets import DatePicker, DateTimePicker from utilities.querysets import RestrictedQuerySet from utilities.validators import validate_regex @@ -306,8 +306,9 @@ def serialize(self, value): """ if value is None: return value - if self.type == CustomFieldTypeChoices.TYPE_DATE and type(value) is date: - return value.isoformat() + if self.type in (CustomFieldTypeChoices.TYPE_DATE, CustomFieldTypeChoices.TYPE_DATETIME): + if type(value) in (date, datetime): + return value.isoformat() if self.type == CustomFieldTypeChoices.TYPE_OBJECT: return value.pk if self.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: @@ -325,6 +326,11 @@ def deserialize(self, value): return date.fromisoformat(value) except ValueError: return value + if self.type == CustomFieldTypeChoices.TYPE_DATETIME: + try: + return datetime.fromisoformat(value) + except ValueError: + return value if self.type == CustomFieldTypeChoices.TYPE_OBJECT: model = self.object_type.model_class() return model.objects.filter(pk=value).first() @@ -380,6 +386,10 @@ def to_form_field(self, set_initial=True, enforce_required=True, enforce_visibil elif self.type == CustomFieldTypeChoices.TYPE_DATE: field = forms.DateField(required=required, initial=initial, widget=DatePicker()) + # Date & time + elif self.type == CustomFieldTypeChoices.TYPE_DATETIME: + field = forms.DateTimeField(required=required, initial=initial, widget=DateTimePicker()) + # Select elif self.type in (CustomFieldTypeChoices.TYPE_SELECT, CustomFieldTypeChoices.TYPE_MULTISELECT): choices = [(c, c) for c in self.choices] @@ -490,6 +500,10 @@ def to_filter(self, lookup_expr=None): elif self.type == CustomFieldTypeChoices.TYPE_DATE: filter_class = filters.MultiValueDateFilter + # Date & time + elif self.type == CustomFieldTypeChoices.TYPE_DATETIME: + filter_class = filters.MultiValueDateTimeFilter + # Select elif self.type == CustomFieldTypeChoices.TYPE_SELECT: filter_class = filters.MultiValueCharFilter @@ -558,9 +572,17 @@ def validate(self, value): elif self.type == CustomFieldTypeChoices.TYPE_DATE: if type(value) is not date: try: - datetime.strptime(value, '%Y-%m-%d') + date.fromisoformat(value) + except ValueError: + raise ValidationError("Date values must be in ISO 8601 format (YYYY-MM-DD).") + + # Validate date & time + elif self.type == CustomFieldTypeChoices.TYPE_DATETIME: + if type(value) is not datetime: + try: + datetime.fromisoformat(value) except ValueError: - raise ValidationError("Date values must be in the format YYYY-MM-DD.") + raise ValidationError("Date and time values must be in ISO 8601 format (YYYY-MM-DD HH:MM:SS).") # Validate selected choice elif self.type == CustomFieldTypeChoices.TYPE_SELECT: diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index d890e3ebe8..f29a11e0e9 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -1,3 +1,4 @@ +import datetime from decimal import Decimal from django.contrib.contenttypes.models import ContentType @@ -157,12 +158,12 @@ def test_boolean_field(self): self.assertIsNone(instance.custom_field_data.get(cf.name)) def test_date_field(self): - value = '2016-06-23' + value = datetime.date(2016, 6, 23) # Create a custom field & check that initial value is null cf = CustomField.objects.create( name='date_field', - type=CustomFieldTypeChoices.TYPE_TEXT, + type=CustomFieldTypeChoices.TYPE_DATE, required=False ) cf.content_types.set([self.object_type]) @@ -170,10 +171,35 @@ def test_date_field(self): self.assertIsNone(instance.custom_field_data[cf.name]) # Assign a value and check that it is saved - instance.custom_field_data[cf.name] = value + instance.custom_field_data[cf.name] = cf.serialize(value) instance.save() instance.refresh_from_db() - self.assertEqual(instance.custom_field_data[cf.name], value) + self.assertEqual(instance.cf[cf.name], value) + + # Delete the stored value and check that it is now null + instance.custom_field_data.pop(cf.name) + instance.save() + instance.refresh_from_db() + self.assertIsNone(instance.custom_field_data.get(cf.name)) + + def test_datetime_field(self): + value = datetime.datetime(2016, 6, 23, 9, 45, 0) + + # Create a custom field & check that initial value is null + cf = CustomField.objects.create( + name='date_field', + type=CustomFieldTypeChoices.TYPE_DATETIME, + required=False + ) + cf.content_types.set([self.object_type]) + instance = Site.objects.first() + self.assertIsNone(instance.custom_field_data[cf.name]) + + # Assign a value and check that it is saved + instance.custom_field_data[cf.name] = cf.serialize(value) + instance.save() + instance.refresh_from_db() + self.assertEqual(instance.cf[cf.name], value) # Delete the stored value and check that it is now null instance.custom_field_data.pop(cf.name) @@ -408,6 +434,7 @@ def setUpTestData(cls): CustomField(type=CustomFieldTypeChoices.TYPE_DECIMAL, name='decimal_field', default=123.45), CustomField(type=CustomFieldTypeChoices.TYPE_BOOLEAN, name='boolean_field', default=False), CustomField(type=CustomFieldTypeChoices.TYPE_DATE, name='date_field', default='2020-01-01'), + CustomField(type=CustomFieldTypeChoices.TYPE_DATETIME, name='datetime_field', default='2020-01-01T01:23:45'), CustomField(type=CustomFieldTypeChoices.TYPE_URL, name='url_field', default='http://example.com/1'), CustomField(type=CustomFieldTypeChoices.TYPE_JSON, name='json_field', default='{"x": "y"}'), CustomField( @@ -459,12 +486,13 @@ def setUpTestData(cls): custom_fields[3].name: Decimal('456.78'), custom_fields[4].name: True, custom_fields[5].name: '2020-01-02', - custom_fields[6].name: 'http://example.com/2', - custom_fields[7].name: '{"foo": 1, "bar": 2}', - custom_fields[8].name: 'Bar', - custom_fields[9].name: ['Bar', 'Baz'], - custom_fields[10].name: vlans[1].pk, - custom_fields[11].name: [vlans[2].pk, vlans[3].pk], + custom_fields[6].name: '2020-01-02 12:00:00', + custom_fields[7].name: 'http://example.com/2', + custom_fields[8].name: '{"foo": 1, "bar": 2}', + custom_fields[9].name: 'Bar', + custom_fields[10].name: ['Bar', 'Baz'], + custom_fields[11].name: vlans[1].pk, + custom_fields[12].name: [vlans[2].pk, vlans[3].pk], } sites[1].save() @@ -476,6 +504,7 @@ def test_get_custom_fields(self): CustomFieldTypeChoices.TYPE_DECIMAL: 'decimal', CustomFieldTypeChoices.TYPE_BOOLEAN: 'boolean', CustomFieldTypeChoices.TYPE_DATE: 'string', + CustomFieldTypeChoices.TYPE_DATETIME: 'string', CustomFieldTypeChoices.TYPE_URL: 'string', CustomFieldTypeChoices.TYPE_JSON: 'object', CustomFieldTypeChoices.TYPE_SELECT: 'string', @@ -511,6 +540,7 @@ def test_get_single_object_without_custom_field_data(self): 'decimal_field': None, 'boolean_field': None, 'date_field': None, + 'datetime_field': None, 'url_field': None, 'json_field': None, 'select_field': None, @@ -536,6 +566,7 @@ def test_get_single_object_with_custom_field_data(self): self.assertEqual(response.data['custom_fields']['decimal_field'], site2_cfvs['decimal_field']) self.assertEqual(response.data['custom_fields']['boolean_field'], site2_cfvs['boolean_field']) self.assertEqual(response.data['custom_fields']['date_field'], site2_cfvs['date_field']) + self.assertEqual(response.data['custom_fields']['datetime_field'], site2_cfvs['datetime_field']) self.assertEqual(response.data['custom_fields']['url_field'], site2_cfvs['url_field']) self.assertEqual(response.data['custom_fields']['json_field'], site2_cfvs['json_field']) self.assertEqual(response.data['custom_fields']['select_field'], site2_cfvs['select_field']) @@ -571,6 +602,7 @@ def test_create_single_object_with_defaults(self): self.assertEqual(response_cf['decimal_field'], cf_defaults['decimal_field']) self.assertEqual(response_cf['boolean_field'], cf_defaults['boolean_field']) self.assertEqual(response_cf['date_field'].isoformat(), cf_defaults['date_field']) + self.assertEqual(response_cf['datetime_field'].isoformat(), cf_defaults['datetime_field']) self.assertEqual(response_cf['url_field'], cf_defaults['url_field']) self.assertEqual(response_cf['json_field'], cf_defaults['json_field']) self.assertEqual(response_cf['select_field'], cf_defaults['select_field']) @@ -588,7 +620,8 @@ def test_create_single_object_with_defaults(self): self.assertEqual(site.custom_field_data['integer_field'], cf_defaults['integer_field']) self.assertEqual(site.custom_field_data['decimal_field'], cf_defaults['decimal_field']) self.assertEqual(site.custom_field_data['boolean_field'], cf_defaults['boolean_field']) - self.assertEqual(str(site.custom_field_data['date_field']), cf_defaults['date_field']) + self.assertEqual(site.custom_field_data['date_field'], cf_defaults['date_field']) + self.assertEqual(site.custom_field_data['datetime_field'], cf_defaults['datetime_field']) self.assertEqual(site.custom_field_data['url_field'], cf_defaults['url_field']) self.assertEqual(site.custom_field_data['json_field'], cf_defaults['json_field']) self.assertEqual(site.custom_field_data['select_field'], cf_defaults['select_field']) @@ -609,7 +642,8 @@ def test_create_single_object_with_values(self): 'integer_field': 456, 'decimal_field': 456.78, 'boolean_field': True, - 'date_field': '2020-01-02', + 'date_field': datetime.date(2020, 1, 2), + 'datetime_field': datetime.datetime(2020, 1, 2, 12, 0, 0), 'url_field': 'http://example.com/2', 'json_field': '{"foo": 1, "bar": 2}', 'select_field': 'Bar', @@ -632,7 +666,8 @@ def test_create_single_object_with_values(self): self.assertEqual(response_cf['integer_field'], data_cf['integer_field']) self.assertEqual(response_cf['decimal_field'], data_cf['decimal_field']) self.assertEqual(response_cf['boolean_field'], data_cf['boolean_field']) - self.assertEqual(response_cf['date_field'].isoformat(), data_cf['date_field']) + self.assertEqual(response_cf['date_field'], data_cf['date_field']) + self.assertEqual(response_cf['datetime_field'], data_cf['datetime_field']) self.assertEqual(response_cf['url_field'], data_cf['url_field']) self.assertEqual(response_cf['json_field'], data_cf['json_field']) self.assertEqual(response_cf['select_field'], data_cf['select_field']) @@ -650,7 +685,8 @@ def test_create_single_object_with_values(self): self.assertEqual(site.custom_field_data['integer_field'], data_cf['integer_field']) self.assertEqual(site.custom_field_data['decimal_field'], data_cf['decimal_field']) self.assertEqual(site.custom_field_data['boolean_field'], data_cf['boolean_field']) - self.assertEqual(str(site.custom_field_data['date_field']), data_cf['date_field']) + self.assertEqual(site.cf['date_field'], data_cf['date_field']) + self.assertEqual(site.cf['datetime_field'], data_cf['datetime_field']) self.assertEqual(site.custom_field_data['url_field'], data_cf['url_field']) self.assertEqual(site.custom_field_data['json_field'], data_cf['json_field']) self.assertEqual(site.custom_field_data['select_field'], data_cf['select_field']) @@ -697,6 +733,7 @@ def test_create_multiple_objects_with_defaults(self): self.assertEqual(response_cf['decimal_field'], cf_defaults['decimal_field']) self.assertEqual(response_cf['boolean_field'], cf_defaults['boolean_field']) self.assertEqual(response_cf['date_field'].isoformat(), cf_defaults['date_field']) + self.assertEqual(response_cf['datetime_field'].isoformat(), cf_defaults['datetime_field']) self.assertEqual(response_cf['url_field'], cf_defaults['url_field']) self.assertEqual(response_cf['json_field'], cf_defaults['json_field']) self.assertEqual(response_cf['select_field'], cf_defaults['select_field']) @@ -714,7 +751,8 @@ def test_create_multiple_objects_with_defaults(self): self.assertEqual(site.custom_field_data['integer_field'], cf_defaults['integer_field']) self.assertEqual(site.custom_field_data['decimal_field'], cf_defaults['decimal_field']) self.assertEqual(site.custom_field_data['boolean_field'], cf_defaults['boolean_field']) - self.assertEqual(str(site.custom_field_data['date_field']), cf_defaults['date_field']) + self.assertEqual(site.custom_field_data['date_field'], cf_defaults['date_field']) + self.assertEqual(site.custom_field_data['datetime_field'], cf_defaults['datetime_field']) self.assertEqual(site.custom_field_data['url_field'], cf_defaults['url_field']) self.assertEqual(site.custom_field_data['json_field'], cf_defaults['json_field']) self.assertEqual(site.custom_field_data['select_field'], cf_defaults['select_field']) @@ -732,7 +770,8 @@ def test_create_multiple_objects_with_values(self): 'integer_field': 456, 'decimal_field': 456.78, 'boolean_field': True, - 'date_field': '2020-01-02', + 'date_field': datetime.date(2020, 1, 2), + 'datetime_field': datetime.datetime(2020, 1, 2, 12, 0, 0), 'url_field': 'http://example.com/2', 'json_field': '{"foo": 1, "bar": 2}', 'select_field': 'Bar', @@ -773,7 +812,8 @@ def test_create_multiple_objects_with_values(self): self.assertEqual(response_cf['integer_field'], custom_field_data['integer_field']) self.assertEqual(response_cf['decimal_field'], custom_field_data['decimal_field']) self.assertEqual(response_cf['boolean_field'], custom_field_data['boolean_field']) - self.assertEqual(response_cf['date_field'].isoformat(), custom_field_data['date_field']) + self.assertEqual(response_cf['date_field'], custom_field_data['date_field']) + self.assertEqual(response_cf['datetime_field'], custom_field_data['datetime_field']) self.assertEqual(response_cf['url_field'], custom_field_data['url_field']) self.assertEqual(response_cf['json_field'], custom_field_data['json_field']) self.assertEqual(response_cf['select_field'], custom_field_data['select_field']) @@ -791,7 +831,8 @@ def test_create_multiple_objects_with_values(self): self.assertEqual(site.custom_field_data['integer_field'], custom_field_data['integer_field']) self.assertEqual(site.custom_field_data['decimal_field'], custom_field_data['decimal_field']) self.assertEqual(site.custom_field_data['boolean_field'], custom_field_data['boolean_field']) - self.assertEqual(str(site.custom_field_data['date_field']), custom_field_data['date_field']) + self.assertEqual(site.cf['date_field'], custom_field_data['date_field']) + self.assertEqual(site.cf['datetime_field'], custom_field_data['datetime_field']) self.assertEqual(site.custom_field_data['url_field'], custom_field_data['url_field']) self.assertEqual(site.custom_field_data['json_field'], custom_field_data['json_field']) self.assertEqual(site.custom_field_data['select_field'], custom_field_data['select_field']) @@ -826,6 +867,7 @@ def test_update_single_object_with_values(self): self.assertEqual(response_cf['decimal_field'], original_cfvs['decimal_field']) self.assertEqual(response_cf['boolean_field'], original_cfvs['boolean_field']) self.assertEqual(response_cf['date_field'], original_cfvs['date_field']) + self.assertEqual(response_cf['datetime_field'], original_cfvs['datetime_field']) self.assertEqual(response_cf['url_field'], original_cfvs['url_field']) self.assertEqual(response_cf['json_field'], original_cfvs['json_field']) self.assertEqual(response_cf['select_field'], original_cfvs['select_field']) @@ -844,6 +886,7 @@ def test_update_single_object_with_values(self): self.assertEqual(site2.cf['decimal_field'], original_cfvs['decimal_field']) self.assertEqual(site2.cf['boolean_field'], original_cfvs['boolean_field']) self.assertEqual(site2.cf['date_field'], original_cfvs['date_field']) + self.assertEqual(site2.cf['datetime_field'], original_cfvs['datetime_field']) self.assertEqual(site2.cf['url_field'], original_cfvs['url_field']) self.assertEqual(site2.cf['json_field'], original_cfvs['json_field']) self.assertEqual(site2.cf['select_field'], original_cfvs['select_field']) @@ -977,6 +1020,7 @@ def setUpTestData(cls): CustomField(name='decimal', type=CustomFieldTypeChoices.TYPE_DECIMAL), CustomField(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN), CustomField(name='date', type=CustomFieldTypeChoices.TYPE_DATE), + CustomField(name='datetime', type=CustomFieldTypeChoices.TYPE_DATETIME), CustomField(name='url', type=CustomFieldTypeChoices.TYPE_URL), CustomField(name='json', type=CustomFieldTypeChoices.TYPE_JSON), CustomField(name='select', type=CustomFieldTypeChoices.TYPE_SELECT, choices=[ @@ -995,10 +1039,10 @@ def test_import(self): Import a Site in CSV format, including a value for each CustomField. """ data = ( - ('name', 'slug', 'status', 'cf_text', 'cf_longtext', 'cf_integer', 'cf_decimal', 'cf_boolean', 'cf_date', 'cf_url', 'cf_json', 'cf_select', 'cf_multiselect'), - ('Site 1', 'site-1', 'active', 'ABC', 'Foo', '123', '123.45', 'True', '2020-01-01', 'http://example.com/1', '{"foo": 123}', 'Choice A', '"Choice A,Choice B"'), - ('Site 2', 'site-2', 'active', 'DEF', 'Bar', '456', '456.78', 'False', '2020-01-02', 'http://example.com/2', '{"bar": 456}', 'Choice B', '"Choice B,Choice C"'), - ('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', '', '', ''), + ('name', 'slug', 'status', 'cf_text', 'cf_longtext', 'cf_integer', 'cf_decimal', 'cf_boolean', 'cf_date', 'cf_datetime', 'cf_url', 'cf_json', 'cf_select', 'cf_multiselect'), + ('Site 1', 'site-1', 'active', 'ABC', 'Foo', '123', '123.45', 'True', '2020-01-01', '2020-01-01 12:00:00', 'http://example.com/1', '{"foo": 123}', 'Choice A', '"Choice A,Choice B"'), + ('Site 2', 'site-2', 'active', 'DEF', 'Bar', '456', '456.78', 'False', '2020-01-02', '2020-01-02 12:00:00', 'http://example.com/2', '{"bar": 456}', 'Choice B', '"Choice B,Choice C"'), + ('Site 3', 'site-3', 'active', '', '', '', '', '', '', '', '', '', '', ''), ) csv_data = '\n'.join(','.join(row) for row in data) @@ -1008,13 +1052,14 @@ def test_import(self): # Validate data for site 1 site1 = Site.objects.get(name='Site 1') - self.assertEqual(len(site1.custom_field_data), 10) + self.assertEqual(len(site1.custom_field_data), 11) self.assertEqual(site1.custom_field_data['text'], 'ABC') self.assertEqual(site1.custom_field_data['longtext'], 'Foo') self.assertEqual(site1.custom_field_data['integer'], 123) self.assertEqual(site1.custom_field_data['decimal'], 123.45) self.assertEqual(site1.custom_field_data['boolean'], True) - self.assertEqual(site1.custom_field_data['date'], '2020-01-01') + self.assertEqual(site1.cf['date'].isoformat(), '2020-01-01') + self.assertEqual(site1.cf['datetime'].isoformat(), '2020-01-01T12:00:00+00:00') self.assertEqual(site1.custom_field_data['url'], 'http://example.com/1') self.assertEqual(site1.custom_field_data['json'], {"foo": 123}) self.assertEqual(site1.custom_field_data['select'], 'Choice A') @@ -1022,13 +1067,14 @@ def test_import(self): # Validate data for site 2 site2 = Site.objects.get(name='Site 2') - self.assertEqual(len(site2.custom_field_data), 10) + self.assertEqual(len(site2.custom_field_data), 11) self.assertEqual(site2.custom_field_data['text'], 'DEF') self.assertEqual(site2.custom_field_data['longtext'], 'Bar') self.assertEqual(site2.custom_field_data['integer'], 456) self.assertEqual(site2.custom_field_data['decimal'], 456.78) self.assertEqual(site2.custom_field_data['boolean'], False) - self.assertEqual(site2.custom_field_data['date'], '2020-01-02') + self.assertEqual(site2.cf['date'].isoformat(), '2020-01-02') + self.assertEqual(site2.cf['datetime'].isoformat(), '2020-01-02T12:00:00+00:00') self.assertEqual(site2.custom_field_data['url'], 'http://example.com/2') self.assertEqual(site2.custom_field_data['json'], {"bar": 456}) self.assertEqual(site2.custom_field_data['select'], 'Choice B') diff --git a/netbox/extras/tests/test_forms.py b/netbox/extras/tests/test_forms.py index 35402bda34..722f32f0a8 100644 --- a/netbox/extras/tests/test_forms.py +++ b/netbox/extras/tests/test_forms.py @@ -32,6 +32,9 @@ def setUpTestData(cls): cf_date = CustomField.objects.create(name='date', type=CustomFieldTypeChoices.TYPE_DATE) cf_date.content_types.set([obj_type]) + cf_datetime = CustomField.objects.create(name='datetime', type=CustomFieldTypeChoices.TYPE_DATETIME) + cf_datetime.content_types.set([obj_type]) + cf_url = CustomField.objects.create(name='url', type=CustomFieldTypeChoices.TYPE_URL) cf_url.content_types.set([obj_type]) diff --git a/netbox/utilities/templates/builtins/customfield_value.html b/netbox/utilities/templates/builtins/customfield_value.html index b3bccd716a..a2b27ed2aa 100644 --- a/netbox/utilities/templates/builtins/customfield_value.html +++ b/netbox/utilities/templates/builtins/customfield_value.html @@ -9,6 +9,8 @@ {% checkmark value false="False" %} {% elif customfield.type == 'date' and value %} {{ value|annotated_date }} +{% elif customfield.type == 'datetime' and value %} + {{ value|annotated_date }} {% elif customfield.type == 'url' and value %} {{ value|truncatechars:70 }} {% elif customfield.type == 'json' and value %}