Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions django/contrib/gis/gdal/field.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from ctypes import byref, c_int
from ctypes import byref, c_float, c_int
from datetime import date, datetime, time

from django.contrib.gis.gdal.base import GDALBase
Expand Down Expand Up @@ -73,8 +73,9 @@ def as_datetime(self):
"Retrieve the Field's value as a tuple of date & time components."
if not self.is_set:
return None
yy, mm, dd, hh, mn, ss, tz = [c_int() for i in range(7)]
status = capi.get_field_as_datetime(
yy, mm, dd, hh, mn, tz = [c_int() for _ in range(6)]
ss = c_float()
status = capi.get_field_as_datetime_x(
self._feat.ptr,
self._index,
byref(yy),
Expand Down Expand Up @@ -192,7 +193,17 @@ def value(self):
# 100=GMT, 104=GMT+1, 80=GMT-5, etc.
try:
yy, mm, dd, hh, mn, ss, tz = self.as_datetime()
return datetime(yy.value, mm.value, dd.value, hh.value, mn.value, ss.value)
seconds = int(ss.value)
milliseconds = int(round((ss.value - seconds) * 1000))
return datetime(
yy.value,
mm.value,
dd.value,
hh.value,
mn.value,
seconds,
milliseconds * 1000,
)
except (TypeError, ValueError, GDALException):
return None

Expand All @@ -203,7 +214,9 @@ def value(self):
"Return a Python `time` object for this OFTTime field."
try:
yy, mm, dd, hh, mn, ss, tz = self.as_datetime()
return time(hh.value, mn.value, ss.value)
seconds = int(ss.value)
milliseconds = int(round((ss.value - seconds) * 1000))
return time(hh.value, mn.value, seconds, milliseconds * 1000)
except (ValueError, GDALException):
return None

Expand Down
8 changes: 7 additions & 1 deletion django/contrib/gis/gdal/prototypes/ds.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
OGR_Fld_* routines are relevant here.
"""

from ctypes import POINTER, c_char_p, c_double, c_int, c_long, c_uint, c_void_p
from ctypes import POINTER, c_char_p, c_double, c_float, c_int, c_long, c_uint, c_void_p

from django.contrib.gis.gdal.envelope import OGREnvelope
from django.contrib.gis.gdal.libgdal import lgdal
Expand All @@ -21,6 +21,7 @@
)

c_int_p = POINTER(c_int) # shortcut type
c_float_p = POINTER(c_float)

GDAL_OF_READONLY = 0x00
GDAL_OF_UPDATE = 0x01
Expand Down Expand Up @@ -91,6 +92,11 @@
lgdal.OGR_F_GetFieldAsDateTime,
[c_void_p, c_int, c_int_p, c_int_p, c_int_p, c_int_p, c_int_p, c_int_p],
)
get_field_as_datetime_x = int_output(
lgdal.OGR_F_GetFieldAsDateTimeEx,
[c_void_p, c_int, c_int_p, c_int_p, c_int_p, c_int_p, c_int_p, c_float_p, c_int_p],
)

get_field_as_double = double_output(lgdal.OGR_F_GetFieldAsDouble, [c_void_p, c_int])
get_field_as_integer = int_output(lgdal.OGR_F_GetFieldAsInteger, [c_void_p, c_int])
get_field_as_integer64 = int64_output(
Expand Down
10 changes: 8 additions & 2 deletions docs/ref/contrib/gis/gdal.txt
Original file line number Diff line number Diff line change
Expand Up @@ -470,12 +470,18 @@ __ https://gdal.org/drivers/vector/

.. method:: as_datetime()

Returns the value of the field as a tuple of date and time components:
Returns the value of the field as a tuple of date and time components with
millisecond precision:

.. code-block:: pycon

>>> city["Created"].as_datetime()
(c_long(1999), c_long(5), c_long(23), c_long(0), c_long(0), c_long(0), c_long(0))
(c_long(1999), c_long(5), c_long(23), c_long(0), c_long(0), c_float(0.0), c_long(0))

.. versionchanged:: 6.2

The seconds component is now returned as a ``c_float`` with millisecond
precision instead of a ``c_int``.

``Driver``
----------
Expand Down
10 changes: 9 additions & 1 deletion docs/releases/6.2.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ Minor features
:mod:`django.contrib.gis`
~~~~~~~~~~~~~~~~~~~~~~~~~

* ...
* :class:`~django.contrib.gis.gdal.DataSource` now preserves millisecond
precision when reading time and datetime fields.

:mod:`django.contrib.messages`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -276,6 +277,13 @@ backends.
consistently returning an HTTP 403 response for staff users without the view
or change permission regardless of whether the object exists.

:mod:`django.contrib.gis`
-------------------------

* The seconds value returned by
:meth:`~django.contrib.gis.gdal.Field.as_datetime` is now a ``c_float``
rather than a ``c_int``.

Miscellaneous
-------------

Expand Down
5 changes: 4 additions & 1 deletion tests/gis_tests/data/has_nulls/has_nulls.geojson
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"properties": {
"uuid": "1378c26f-cbe6-44b0-929f-eb330d4991f5",
"boolean": true,
"datetime": "1994-08-14T11:32:14+0000",
"datetime": "1994-08-14T11:32:14.123+0000",
"time": "11:32:14.123",
"name": "Philadelphia",
"integer": 5,
"num": 1.001
Expand Down Expand Up @@ -58,6 +59,7 @@
"type": "Feature",
"properties": {
"uuid": "fa2ba67c-a135-4338-b924-a9622b5d869f",
"time": "00:00:00",
"integer": null,
"num": null
},
Expand Down Expand Up @@ -98,6 +100,7 @@
"properties": {
"uuid": "4494c1f3-55ab-4256-b365-12115cb388d5",
"datetime": "2018-11-29T03:02:52+0000",
"time": "03:02:52",
"name": "north",
"integer": 8,
"num": 0.0,
Expand Down
21 changes: 17 additions & 4 deletions tests/gis_tests/gdal_tests/test_ds.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import os
import re
from datetime import datetime
from datetime import datetime, time
from pathlib import Path

from django.contrib.gis.gdal import DataSource, Envelope, GDALException, OGRGeometry
from django.contrib.gis.gdal.field import OFTDateTime, OFTInteger, OFTReal, OFTString
from django.contrib.gis.gdal.field import (
OFTDateTime,
OFTInteger,
OFTReal,
OFTString,
OFTTime,
)
from django.contrib.gis.geos import GEOSGeometry
from django.test import SimpleTestCase

Expand Down Expand Up @@ -74,7 +80,7 @@
TestDS(
"has_nulls",
nfeat=3,
nfld=6,
nfld=7,
geom="POLYGON",
gtype=3,
driver="GeoJSON",
Expand All @@ -85,6 +91,7 @@
"num": OFTReal,
"integer": OFTInteger,
"datetime": OFTDateTime,
"time": OFTTime,
"boolean": OFTInteger,
},
extent=(-75.274200, 39.846504, -74.959717, 40.119040), # Got extent from QGIS
Expand All @@ -99,10 +106,11 @@
"integer": [5, None, 8],
"boolean": [True, None, False],
"datetime": [
datetime.strptime("1994-08-14T11:32:14", datetime_format),
datetime(1994, 8, 14, 11, 32, 14, 123000),
None,
datetime.strptime("2018-11-29T03:02:52", datetime_format),
],
"time": [time(11, 32, 14, 123000), time(0), time(3, 2, 52)],
},
fids=range(3),
),
Expand Down Expand Up @@ -353,3 +361,8 @@ def test_nonexistent_field(self):
msg = "invalid field name: nonexistent"
with self.assertRaisesMessage(GDALException, msg):
ds[0].get_fields("nonexistent")

def test_datetime_with_milliseconds(self):
feature = DataSource(get_ds_file("has_nulls", "geojson"))[0][0]
for field_name in "datetime", "time":
self.assertEqual(feature.get(field_name).microsecond, 123000)
Loading