diff --git a/netbox/core/migrations/0016_job_log_entries.py b/netbox/core/migrations/0016_job_log_entries.py index 26d2ba24d..030bd4e38 100644 --- a/netbox/core/migrations/0016_job_log_entries.py +++ b/netbox/core/migrations/0016_job_log_entries.py @@ -2,6 +2,8 @@ import django.contrib.postgres.fields import django.core.serializers.json from django.db import migrations, models +import utilities.json + class Migration(migrations.Migration): @@ -15,6 +17,7 @@ class Migration(migrations.Migration): name='log_entries', field=django.contrib.postgres.fields.ArrayField( base_field=models.JSONField( + decoder=utilities.json.JobLogDecoder, encoder=django.core.serializers.json.DjangoJSONEncoder ), blank=True, diff --git a/netbox/core/models/jobs.py b/netbox/core/models/jobs.py index d6b4d58e8..863034352 100644 --- a/netbox/core/models/jobs.py +++ b/netbox/core/models/jobs.py @@ -20,6 +20,7 @@ from core.choices import JobStatusChoices from core.dataclasses import JobLogEntry from core.models import ObjectType from core.signals import job_end, job_start +from utilities.json import JobLogDecoder from utilities.querysets import RestrictedQuerySet from utilities.rqworker import get_queue_for_model @@ -111,7 +112,7 @@ class Job(models.Model): log_entries = ArrayField( base_field=models.JSONField( encoder=DjangoJSONEncoder, - # TODO: Specify a decoder to handle ISO 8601 timestamps + decoder=JobLogDecoder, ), blank=True, default=list, diff --git a/netbox/core/tables/jobs.py b/netbox/core/tables/jobs.py index 2387a1864..d3c536953 100644 --- a/netbox/core/tables/jobs.py +++ b/netbox/core/tables/jobs.py @@ -62,7 +62,8 @@ class JobTable(NetBoxTable): class JobLogEntryTable(BaseTable): - timestamp = tables.Column( + timestamp = columns.DateTimeColumn( + timespec='milliseconds', verbose_name=_('Time'), ) level = tables.Column( diff --git a/netbox/utilities/json.py b/netbox/utilities/json.py index 3114be1bf..eaf45feea 100644 --- a/netbox/utilities/json.py +++ b/netbox/utilities/json.py @@ -1,10 +1,14 @@ import decimal +import json from django.core.serializers.json import DjangoJSONEncoder +from utilities.datetime import datetime_from_timestamp + __all__ = ( 'ConfigJSONEncoder', 'CustomFieldJSONEncoder', + 'JobLogDecoder', ) @@ -29,3 +33,21 @@ class ConfigJSONEncoder(DjangoJSONEncoder): return type(o).__name__ return super().default(o) + + +class JobLogDecoder(json.JSONDecoder): + """ + Deserialize JobLogEntry timestamps. + """ + def __init__(self, *args, **kwargs): + kwargs['object_hook'] = self._deserialize_entry + super().__init__(*args, **kwargs) + + def _deserialize_entry(self, obj: dict) -> dict: + if obj.get('timestamp'): + # Deserialize a timestamp string to a native datetime object + try: + obj['timestamp'] = datetime_from_timestamp(obj['timestamp']) + except ValueError: + pass + return obj