diff --git a/netbox/account/tables.py b/netbox/account/tables.py index 6655a7f82..bcc0a0ccd 100644 --- a/netbox/account/tables.py +++ b/netbox/account/tables.py @@ -30,10 +30,12 @@ class UserTokenTable(NetBoxTable): write_enabled = columns.BooleanColumn( verbose_name=_('Write Enabled') ) - created = columns.DateColumn( + created = columns.DateTimeColumn( + timespec='minutes', verbose_name=_('Created'), ) - expires = columns.DateColumn( + expires = columns.DateTimeColumn( + timespec='minutes', verbose_name=_('Expires'), ) last_used = columns.DateTimeColumn( diff --git a/netbox/core/tables/tasks.py b/netbox/core/tables/tasks.py index 531ec6375..f53e598b5 100644 --- a/netbox/core/tables/tasks.py +++ b/netbox/core/tables/tasks.py @@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _ from django_tables2.utils import A from core.tables.columns import RQJobStatusColumn -from netbox.tables import BaseTable +from netbox.tables import BaseTable, columns class BackgroundQueueTable(BaseTable): @@ -75,13 +75,13 @@ class BackgroundTaskTable(BaseTable): linkify=("core:background_task", [A("id")]), verbose_name=_("ID") ) - created_at = tables.DateTimeColumn( + created_at = columns.DateTimeColumn( verbose_name=_("Created") ) - enqueued_at = tables.DateTimeColumn( + enqueued_at = columns.DateTimeColumn( verbose_name=_("Enqueued") ) - ended_at = tables.DateTimeColumn( + ended_at = columns.DateTimeColumn( verbose_name=_("Ended") ) status = RQJobStatusColumn( @@ -117,7 +117,7 @@ class WorkerTable(BaseTable): state = tables.Column( verbose_name=_("State") ) - birth_date = tables.DateTimeColumn( + birth_date = columns.DateTimeColumn( verbose_name=_("Birth") ) pid = tables.Column( diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 16f10b485..49249eaa0 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -732,7 +732,7 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat def __str__(self): created = timezone.localtime(self.created) - return f"{date_format(created, format='SHORT_DATETIME_FORMAT')} ({self.get_kind_display()})" + return f"{created.date().isoformat()} {created.time().isoformat(timespec='minutes')} ({self.get_kind_display()})" def get_absolute_url(self): return reverse('extras:journalentry', args=[self.pk]) diff --git a/netbox/extras/tables/tables.py b/netbox/extras/tables/tables.py index a0f504931..04bebd603 100644 --- a/netbox/extras/tables/tables.py +++ b/netbox/extras/tables/tables.py @@ -432,10 +432,10 @@ class ConfigTemplateTable(NetBoxTable): class ObjectChangeTable(NetBoxTable): - time = tables.DateTimeColumn( + time = columns.DateTimeColumn( verbose_name=_('Time'), - linkify=True, - format=settings.SHORT_DATETIME_FORMAT + timespec='minutes', + linkify=True ) user_name = tables.Column( verbose_name=_('Username') @@ -475,10 +475,10 @@ class ObjectChangeTable(NetBoxTable): class JournalEntryTable(NetBoxTable): - created = tables.DateTimeColumn( + created = columns.DateTimeColumn( verbose_name=_('Created'), - linkify=True, - format=settings.SHORT_DATETIME_FORMAT + timespec='minutes', + linkify=True ) assigned_object_type = columns.ContentTypeColumn( verbose_name=_('Object Type') diff --git a/netbox/netbox/tables/columns.py b/netbox/netbox/tables/columns.py index 193bf8a17..c37bb1b0d 100644 --- a/netbox/netbox/tables/columns.py +++ b/netbox/netbox/tables/columns.py @@ -10,7 +10,6 @@ from django.db.models import DateField, DateTimeField from django.template import Context, Template from django.urls import reverse from django.utils.dateparse import parse_date -from django.utils.formats import date_format from django.utils.html import escape from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ @@ -52,18 +51,17 @@ __all__ = ( # @library.register -class DateColumn(tables.DateColumn): +class DateColumn(tables.Column): """ - Overrides the default implementation of DateColumn to better handle null values, returning a default value for - tables and null when exporting data. It is registered in the tables library to use this class instead of the - default, making this behavior consistent in all fields of type DateField. + Render a datetime.date in ISO 8601 format. """ def render(self, value): if value: - return date_format(value, format="SHORT_DATE_FORMAT") + return value.isoformat() def value(self, value): - return value + if value: + return value.isoformat() @classmethod def from_field(cls, field, **kwargs): @@ -72,16 +70,24 @@ class DateColumn(tables.DateColumn): @library.register -class DateTimeColumn(tables.DateTimeColumn): +class DateTimeColumn(tables.Column): """ - Overrides the default implementation of DateTimeColumn to better handle null values, returning a default value for - tables and null when exporting data. It is registered in the tables library to use this class instead of the - default, making this behavior consistent in all fields of type DateTimeField. + Render a datetime.datetime in ISO 8601 format. + + Args: + timespec: Granularity specification; passed through to datetime.isoformat() """ + def __init__(self, *args, timespec='seconds', **kwargs): + self.timespec = timespec + super().__init__(*args, **kwargs) + + def render(self, value): + if value: + return f"{value.date().isoformat()} {value.time().isoformat(timespec=self.timespec)}" + def value(self, value): if value: - return date_format(value, format="SHORT_DATETIME_FORMAT") - return None + return value.isoformat() @classmethod def from_field(cls, field, **kwargs): @@ -498,7 +504,7 @@ class CustomFieldColumn(tables.Column): if self.customfield.type == CustomFieldTypeChoices.TYPE_LONGTEXT and value: return render_markdown(value) if self.customfield.type == CustomFieldTypeChoices.TYPE_DATE and value: - return date_format(parse_date(value), format="SHORT_DATE_FORMAT") + return parse_date(value).isoformat() if value is not None: obj = self.customfield.deserialize(value) return mark_safe(self._linkify_item(obj)) diff --git a/netbox/templates/account/profile.html b/netbox/templates/account/profile.html index 53122168f..f494df910 100644 --- a/netbox/templates/account/profile.html +++ b/netbox/templates/account/profile.html @@ -31,11 +31,11 @@
{% trans "Created" %} | -{{ object.created|annotated_date }} | +{{ object.created|isodatetime }} |
---|---|---|
{% trans "Scheduled" %} | - {{ object.scheduled|annotated_date|placeholder }} + {{ object.scheduled|isodatetime|placeholder }} {% if object.interval %} ({% blocktrans with interval=object.interval %}every {{ interval }} minutes{% endblocktrans %}) {% endif %} @@ -62,11 +62,11 @@ | |
{% trans "Started" %} | -{{ object.started|annotated_date|placeholder }} | +{{ object.started|isodatetime|placeholder }} |
{% trans "Completed" %} | -{{ object.completed|annotated_date|placeholder }} | +{{ object.completed|isodatetime|placeholder }} |
{% if job.started %} - {% trans "Started" %}: {{ job.started|annotated_date }} + {% trans "Started" %}: {{ job.started|isodatetime }} {% elif job.scheduled %} - {% trans "Scheduled for" %}: {{ job.scheduled|annotated_date }} ({{ job.scheduled|naturaltime }}) + {% trans "Scheduled for" %}: {{ job.scheduled|isodatetime }} {% else %} - {% trans "Created" %}: {{ job.created|annotated_date }} + {% trans "Created" %}: {{ job.created|isodatetime }} {% endif %} {% if job.completed %} {% trans "Duration" %}: {{ job.duration }} diff --git a/netbox/templates/extras/journalentry.html b/netbox/templates/extras/journalentry.html index 7a4c06b25..29c0a263c 100644 --- a/netbox/templates/extras/journalentry.html +++ b/netbox/templates/extras/journalentry.html @@ -20,7 +20,7 @@
{% trans "Time" %} | -- {{ object.time|annotated_date }} - | +{{ object.time|isodatetime }} | |
---|---|---|---|
{% trans "User" %} | diff --git a/netbox/templates/extras/script_list.html b/netbox/templates/extras/script_list.html index dc7a168f8..7ce5ca6eb 100644 --- a/netbox/templates/extras/script_list.html +++ b/netbox/templates/extras/script_list.html @@ -67,7 +67,7 @@{{ script.description|markdown|placeholder }} | {% if last_job %}- {{ last_job.created|annotated_date }} + {{ last_job.created|isodatetime }} |
{% badge last_job.get_status_display last_job.get_status_color %}
diff --git a/netbox/templates/extras/script_result.html b/netbox/templates/extras/script_result.html
index 276e2562a..ee1127c8a 100644
--- a/netbox/templates/extras/script_result.html
+++ b/netbox/templates/extras/script_result.html
@@ -17,7 +17,7 @@
- {% trans "Created" %} {{ object.created|annotated_date }}
+ {% trans "Created" %} {{ object.created|isodatetime:"minutes" }}
{% if object.last_updated %}
·
- {% trans "Updated" %} {{ object.last_updated|timesince }} {% trans "ago" %}
+ {% trans "Updated" %} {{ object.last_updated|isodatetime:"minutes" }}
{% endif %}
{% endblock subtitle %}
diff --git a/netbox/templates/ipam/aggregate.html b/netbox/templates/ipam/aggregate.html
index 6819e5448..a6415463e 100644
--- a/netbox/templates/ipam/aggregate.html
+++ b/netbox/templates/ipam/aggregate.html
@@ -37,7 +37,7 @@
|
{% trans "Date Added" %} | -{{ object.date_added|annotated_date|placeholder }} | +{{ object.date_added|isodate|placeholder }} | |
{% trans "Description" %} | diff --git a/netbox/templates/users/token.html b/netbox/templates/users/token.html index 2d1858323..968c1d421 100644 --- a/netbox/templates/users/token.html +++ b/netbox/templates/users/token.html @@ -1,6 +1,6 @@ {% extends 'generic/object.html' %} -{% load i18n %} {% load helpers %} +{% load i18n %} {% load render_table from django_tables2 %} {% block title %}{% trans "Token" %} {{ object }}{% endblock %} @@ -33,15 +33,15 @@|||
{% trans "Created" %} | -{{ object.created|annotated_date }} | +{{ object.created|isodatetime }} | |
{% trans "Expires" %} | -{{ object.expires|placeholder }} | +{{ object.expires|isodatetime|placeholder }} | |
{% trans "Last used" %} | -{{ object.last_used|placeholder }} | +{{ object.last_used|isodatetime|placeholder }} | |
{% trans "Allowed IPs" %} | diff --git a/netbox/templates/users/user.html b/netbox/templates/users/user.html index 0dd12fb52..c708fb963 100644 --- a/netbox/templates/users/user.html +++ b/netbox/templates/users/user.html @@ -27,11 +27,11 @@|||
{% trans "Account Created" %} | -{{ object.date_joined|annotated_date }} | +{{ object.date_joined|isodate }} | |
{% trans "Last Login" %} | -{{ object.last_login|annotated_date }} | +{{ object.last_login|isodatetime:"minutes"|placeholder }} | |
{% trans "Active" %} | diff --git a/netbox/utilities/templates/builtins/customfield_value.html b/netbox/utilities/templates/builtins/customfield_value.html index 6091fd411..462e62b86 100644 --- a/netbox/utilities/templates/builtins/customfield_value.html +++ b/netbox/utilities/templates/builtins/customfield_value.html @@ -9,9 +9,9 @@ {% elif customfield.type == 'boolean' and value == False %} {% checkmark value false="False" %} {% elif customfield.type == 'date' and value %} - {{ value|annotated_date }} + {{ value|isodate }} {% elif customfield.type == 'datetime' and value %} - {{ value|annotated_date }} + {{ value|isodate }} {{ value|isodatetime }} {% elif customfield.type == 'url' and value %} {{ value|truncatechars:70 }} {% elif customfield.type == 'json' and value %} diff --git a/netbox/utilities/templatetags/builtins/filters.py b/netbox/utilities/templatetags/builtins/filters.py index 3d7cfec2f..842f7b14a 100644 --- a/netbox/utilities/templatetags/builtins/filters.py +++ b/netbox/utilities/templatetags/builtins/filters.py @@ -5,6 +5,7 @@ import re import yaml from django import template from django.contrib.contenttypes.models import ContentType +from django.contrib.humanize.templatetags.humanize import naturaltime from django.utils.html import escape from django.utils.safestring import mark_safe from markdown import markdown @@ -20,6 +21,9 @@ __all__ = ( 'content_type', 'content_type_id', 'fgcolor', + 'isodate', + 'isodatetime', + 'isotime', 'linkify', 'meta', 'placeholder', @@ -202,3 +206,36 @@ def render_yaml(value): {{ data_dict|yaml }} """ return yaml.dump(json.loads(json.dumps(value))) + + +# +# Time & date +# + +@register.filter() +def isodate(value): + if type(value) is datetime.date: + text = value.isoformat() + elif type(value) is datetime.datetime: + text = value.date().isoformat() + else: + return '' + return mark_safe(f'{text}') + + +@register.filter() +def isotime(value, spec='seconds'): + if type(value) is datetime.time: + return value.isoformat(timespec=spec) + if type(value) is datetime.datetime: + return value.time().isoformat(timespec=spec) + return '' + + +@register.filter() +def isodatetime(value, spec='seconds'): + if type(value) is datetime.datetime: + text = f'{isodate(value)} {isotime(value, spec=spec)}' + else: + return '' + return mark_safe(f'{text}')