trpt4him
trpt4him

Reputation: 1906

Python/Django - datetime dropping the time portion when updating serializer data

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

Answers (1)

Kevin Brown-Silva
Kevin Brown-Silva

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

  1. Validate the date fields in a separate serializer (or just manually) and construct the correct date input on your own.
  2. Combine all of the date fields before passing them into the serializer, such that they match one of the Django datetime input formats.
  3. Directly modify 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

Related Questions