diff --git a/docs/customization/custom-fields.md b/docs/customization/custom-fields.md index 7dc82e179..612faefed 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/netbox/extras/choices.py b/netbox/extras/choices.py index 92d09e2ad..878d9df6a 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 8141ca76d..f5ec3ce94 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -25,7 +25,7 @@ from utilities.forms.fields import ( 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 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): """ 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 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): 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 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): 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 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): 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 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): 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 the format YYYY-MM-DD.") + 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 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/utilities/templates/builtins/customfield_value.html b/netbox/utilities/templates/builtins/customfield_value.html index b3bccd716..a2b27ed2a 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 %}