Reputation: 1906
I am using Django REST Framework, specifically a ModelSerializer
instance, to receive some date/time information, among other fields. The Django form which POSTs or PUTs to my view is using a single field for date, and separate fields for hour, minute, and am/pm.
I wrote a function to deal with recombining the values into a Python datetime object, but for some reason, when my function returns a correct datetime, the time portion is getting zero'ed out when the datetime is assigned back to the serializer object for saving.
I am new to DRF so maybe I just need to approach this another way altogether....
def roomeventrequest(request, roomevent_id):
"""
THIS IS THE VIEW
"""
...
elif request.method == 'PUT':
data = JSONParser().parse(request)
roomevent = RoomEvent.objects.get(pk=roomevent_id)
serializer = RoomEventSerializer(roomevent, data=data)
if serializer.is_valid():
serializer.data['start_datetime'] = _process_datetime(serializer.validated_data['start_datetime'],
data['start_hour'],
data['start_min'],
data['start_ampm'])
serializer.data['end_datetime'] = _process_datetime(serializer.validated_data['start_datetime'],
data['end_hour'],
data['end_min'],
data['start_ampm'])
print (serializer.data['start_datetime'])
print (serializer.data['end_datetime'])
serializer.save()
return JSONResponse(serializer.data, status=201)
return JSONResponse(serializer.errors, status=400)
def _process_datetime(date_obj, hour, minute, ampm):
print (date_obj)
if ampm == 'am' and hour == 12:
hour = 0
elif ampm == 'pm':
hour += 12
return_date = date_obj.replace(minute=int(minute), hour=int(hour))
print(return_date)
return return_date
And the above outputs the following from the print
statements:
2015-05-21 00:00:00
2015-05-21 08:00:00
2015-05-21 00:00:00
2015-05-21 09:00:00
2015-05-21T00:00:00
2015-05-21T00:00:00
Why is the resulting time portion blank? Where have I gotten off track here?
Upvotes: 1
Views: 1744
Reputation: 41671
The problem you are seeing is that you are modifying the serializer data from the outside, which doesn't actually propagate to the data used internally. So even though you are changing the start_datetime
and end_datetime
fields, internally DRF still sees the datetime
objects that only contain the date.
You have a few options
serializer.validated_data
(instead of serializer.data
) in your code. This is what is passed on to create
and update
.I would recommend avoiding #3 for now, as the validated_data
dictionary is designed to be read-only and that may be enforced in the future. So that leaves you with #1 and #2, both of which work require modifications to different parts of your code and work better for different situations.
The first option works best if your validation needs to return errors to the frontend that need to match the specific field, instead of just commenting on the incorrect date format. But it also requires the creation of a custom MultipartDatetimeSerializer
that is used for validating across all of the fields.
from datetime import date, datetime, time
class MultipartDatetimeSerializer(serializers.Serializer):
date = serializers.DateField()
hour = serializers.IntegerField(
min_value=1,
max_value=12
)
minute = serializers.IntegerField(
min_value=0,
max_value=59,
)
period = serializers.ChoiceField(
choices=(
('am', 'A.M.', ),
('pm', 'P.M.', ),
)
)
def to_internal_value(self, data):
parsed_data = super(MultipartDatetimeSerializer, self).to_internal_value(data)
hour = parsed_data['hour']
if parsed_data['period'] == 'pm':
hour += 12
elif hour == 12:
hour = 0
time_data = time(
hour=hour,
minute=parsed_data['minute']
)
return datetime.combine(
date=parsed_data['date'],
time=time_data
)
def to_representation(self, instance):
"""
Convert a datetime to a dictionary containing the
four different fields.
The period must be manually determined (and set), so there
is some pre-processing that has to happen here.
"""
obj = {
"date": instance.date,
"hour": instance.hour,
"minute": instance.minute,
}
if obj["hour"] > 12:
obj["hour"] -= 12
obj["period"] = 'pm'
else:
if obj["hour"] == 0:
obj["hour"] = 12
obj["period"] = 'am'
return super(MultipartDatetimeSerializer, self).to_representation(obj)
This serializer can now be used to split a datetime
into the date
, hour
, minute
, and period
components.
obj = datetime.now()
serializer = MultipartDatetimeSerializer(obj)
print(serializer.data)
As well as combine them back together
data = {
"date": "2015-01-01",
"hour": "11",
"minute": "59",
"period": "pm",
}
serializer = MultipartDatetimeSerializer(data=data)
if serializer.is_valid():
print(serializer.to_internal_value(serializers.validated_data))
else:
print(serializer.errors)
The second option works best if you just need to return an error saying that the data given is not an actual date. You can find a date format that closely matches what is being entered and then concatenate the incoming data to match that.
In your case, the closest date format appears to be %Y-%m-%d %I:%M %p
which will match a date like 2015-01-01 11:59 PM
.
So, all that is left is to set the date format of the date field on your serializer to accept the above format (as well as ISO 8601, the default), which is as simple as setting input_formats
on the field to
['iso-8601', '%Y-%m-%d %I:%M %p']
And changing the data passed to the serializer to concatenate the incoming values to match the field
data = JSONParser().parse(request)
data['start_datetime'] = "%s %s:%s %s" % (data['start_datetime'], data['start_hour'], data['start_min'], data['start_ampm'], )
data['end_datetime'] = "%s %s:%s %s" % (data['end_datetime'], data['end_hour'], data['end_min'], data['end_ampm'], )
Note that I'm always using the %s
modifier instead of the %d
modifier as DRF can handle incorrect numbers being passed into the fields, and it prevents an unhandled exception from occurring when a string is passed in.
Upvotes: 2