Handling Bad Inbound ISO8601 DateTime Offset with Django Rest Framework - django

My mobile client is sending up inaccurate datetime offset information. For example:
2019-05-03T17:55:12-0700
The time is actually the correct UTC time however, the offset should read -0000.
I can not currently modify the client to correct the issue causing this. So I need to throw out the offset or change it to -0000.
In the above example, for this user who has their account timezone settings set to PST, it stores the date in validated_data as datetime.datetime(2019, 5, 4, 0, 55, 12, tzinfo=<UTC>)
If client-based time and offset information were synced up, this conversion by DRF would be correct, as it is seven hours off or PST + the current DST. (west coast us is currently -7:00 UTC)
The problem is that by the time I reach my ModelSerializer class, the validated_data already contains what DRF believes is now the correct UTC time.
Where is the appropriate place to mutate this field on the POST body so that by the time DRF attempts to create the DateTime it will build the correct timestamp?

The easiest way to handle this was to modify the DateTime object before it was saved in the model field. So in the create() get the original ISO8601 string from the POST body using self.context['request'].data['created']. Then use dateutil.parser to parse the string into a DateTime, and replace the timezone with a pytz.UTC.
import dateutil.parser
Class MySerializer(serializers.ModelSerializer):
...
def create(self, validated_data):
scan.created = dateutil.parser.parse(self.context['request'].data['created']).replace(tzinfo=pytz.UTC)
It is not a glorious fix, but will work until I can release an updated mobile client.

Related

How to create a new token with custom created datetime in Django Rest Framework?

I am using Token Authentication in my Django Rest Framework project. I have noticed that while creating a token, if I specify the created datetime field in the create method, than the token is not created with that value, for example:
new_token = Token.objects.create(user=user, created=datetime(2021, 9, 7, 10, 10, 10, tzinfo=timezone.utc)
will not create the token with the specified datetime but will use the current UTC time. To make it work i have to perform another operation on this object and save the modified object again like this:
new_token = Token.objects.create(user=user)
new_token.created = datetime(2021, 9, 7, 10, 10, 10, tzinfo=timezone.utc)
new_token.save()
I think this will make two queries on the DB, first while creating and then while modifying the created datetime which is not very elegant. Can someone please tell me why Django is not setting the datetime in the first way? I'm very new to this framework so please pardon me if I'm overlooking something very simple here.
Thanks!
Okay, I figured out why this is the behavior of Token. If you see the code of Token model, the created field is defined as follows:
created = models.DateTimeField(_("Created"), auto_now_add=True)
The auto_now_add=True value specifies that this field will automatically be set to the current UTC time.
Now, what I wanted to do was to mock this created datetime in my unit tests to simulate some cases. I found out that you can just mock the django.utils.timezone.now return value to simulate any created datetime like this:
def my_unit_test(self):
with patch('django.utils.timezone.now') as fake_dt:
fake_dt.return_value = datetime(2021, 9, 7, 10, 10, 10, tzinfo=timezone.utc)
# token's created field will be set to the datetime above
token = Token.objects.create(user=test_user)

Get max and min formatted date values from queryset in Django

I have a one to many relation between session and camp. Now I have to get the max and min dates of all camps combined for a particular session.
I am able to do it like this:
sess = Session.objects.last()
max_min_dates = sess.camp.aggregate(Min('start_date'), Max('end_date'))
But if I try to send this from HttpResponse then I am getting this error:
TypeError: Object of type 'date' is not JSON serializable
So I need to send the formatted date values in that. How can I modify the above code to get the same?
The default encoder for json.dumps() does not support date encoding (ie. can't convert date into str). You can use django encoder instead, it supports a few more data types see the link for more info.
Django Ref
import json
from django.core.serializers.json import DjangoJSONEncoder
json_str = json.dumps(max_min_dates, cls=DjangoJSONEncoder)

DJANGO datetime wrong on template even after timezone setup

I have a DJANGO application, and I am completely lost about times.
I am located in Budapest, Hungary, my current time for these test is: 09:26
I have my timezone correctly set in settings.py
...
TIME_ZONE = 'Europe/Budapest'
USE_I18N = True
USE_L10N = True
USE_TZ = True
...
Lets say I store a datetime object in my SQLite database, the admin page will show the correct time:
If I query that data in a view the date is suddenly wrong.
2020-10-06 07:26:41.040463+00:00
I have read solutions that I need to activate timezone in my view, but it does not work:
tzname = pytz.timezone("Europe/Budapest")
timezone.activate(tzname)
for i in MyObject.objects.all():
print(i.date)
returns
2020-10-06 07:26:41.040463+00:00
I usually fill my templates with Ajax JS calls, so I was not able to try template filters like this:
{{ value|timezone:"Europe/Budapest" }}
How can I change the time so that my JsonResponse sends the correct time to my templates?
Consider carefully the first sentence in the timezone documentation:
When support for time zones is enabled, Django stores datetime information in UTC in the database, uses time-zone-aware datetime objects internally, and translates them to the end user’s time zone in templates and forms.
Note that translation to local time zones only happens in templates and forms—not database queries, views, or other functions. There are many good reasons for that, one of which is that converting to a different timezone can lose information. Let's say that the datetime you got back from the database was 2:30am on October 25, 2019, Budapest time. What moment in time does that represent? You can't know, because that time occurred twice due to daylight savings time.
So the behavior you're seeing is entirely correct. If you want to convert a datetime to the local time in code use localtime():
from django.utils.timezone import localtime
local = localtime(myobject.timestamp)
When you create your models, you could set the datetime to your local timezone.
from django.utils import timezone
date = models.DateTimeField(
default=timezone.localtime(timezone.now()),
blank=True
)
timezone.localtime(timezone.now()) will give you the time based on the TIME_ZONE given in the settings.
Moment JS:
https://momentjs.com/timezone/docs/#/using-timezones/converting-to-zone/

Django File-based session doesn't expire

I just realized that my session doesn't expire when I use file-based session engine. Looking at Django code for file-based session, Django doesn't store any expiration information for a session, thus it's never expire unless the session file gets deleted manually.
This looks like a bug to me, as the database-backed session works fine, and I believe regardless of what session back-end developer chooses, they all should behave similarly.
Switching to database-backed session is not an option for me, as I need to store user's session in files.
Can anyone shed some lights?
Is this really a bug?
If yes, how do you suggest me to work around it?
Thanks!
So it looks like you're right. At least in django 1.4, using django.contrib.sessions.backends.file totally ignores SESSION_COOKIE_AGE. I'm not sure whether that's really a bug, or just undocumented.
If you really need this functionality, you can create your own session engine based on the file backend in contrib, but extend it with expiry functionality.
Open django/contrib/sessions/backends/file.py and add the following imports:
import datetime
from django.utils import timezone
Then, add two lines to the load method, so that it appears as below:
def load(self):
session_data = {}
try:
session_file = open(self._key_to_file(), "rb")
if (timezone.now() - datetime.datetime.fromtimestamp(os.path.getmtime(self._key_to_file()))).total_seconds() > settings.SESSION_COOKIE_AGE:
raise IOError
try:
file_data = session_file.read()
# Don't fail if there is no data in the session file.
....
This will actually compare the last modified date on the session file to expire it.
Save this file in your project somewhere and use it as your SESSION_ENGINE instead of 'django.contrib.sessions.backends.file'
You'll also need to enable SESSION_SAVE_EVERY_REQUEST in your settings if you want the session to timeout based on inactivity.
An option would be to use tmpwatch in the directory where you store the sessions
I hit similar issue on Django 3.1. In my case, my program calls the function set_expiry(value) with an integer argument (int data type) before checking session expiry.
Accoring to Django documentation, the data type of argument value to set_expiry() can be int , datetime or timedelta. However for file-based session, expiry check inside load() doesn't work properly only if int argument is passed to set_expiry() beforehand, and such problem doesn't happen to datetime and timedelta argument of set_expiry().
The simple solution (workaround?) is to avoid int argument to set_expiry(value), you can do so by subclassing django.contrib.sessions.backends.file.SessionStore and overriding set_expiry(value) (code sample below), and change parameter SESSION_ENGINE accordingly in settings.py
from datetime import timedelta
from django.contrib.sessions.backends.file import SessionStore as FileSessionStore
class SessionStore(FileSessionStore):
def set_expiry(self, value):
""" force to convert to timedelta format """
if value and isinstance(value, int):
value = timedelta(seconds=value)
super().set_expiry(value=value)
Note:
It's also OK to pass timedelta or datetime to set_expiry(value) , but you will need to handle serialization issue on datetime object.

Google App Engine, parsedatetime and TimeZones

I'm working on a Google App Engine / Django app and I encountered the following problem:
In my html I have an input for time. The input is free text - the user types "in 1 hour" or "tomorrow at 11am". The text is then sent to the server in AJAX, which parses it using this python library: http://code.google.com/p/parsedatetime/. Once parsed, the server returns an epoch timestamp of the time.
Here is the problem - Google App Engine always runs on UTC. Therefore, lets say that the local time is now 11am and the UTC time is 2am. When I send "now" to the server it will return "2am", which is good because I want the date to be received in UTC time. When I send "in 1 hour" the server will return "3am" which is good, again. However, when I send "at noon" the server will return "12pm" because it thinks that I'm talking about noon UTC - but really I need it to return 3am, which is noon for the request sender.. I can pass on the TZ of the browser that sends the request, but that wont really help me - the parsedatetime library wont take a timezone argument (correct me if I'm wrong). Is there a walk around this? Maybe setting the environments TZ somehow?
Thanks!
What you could do is add the difference using a timedelta object (http://docs.python.org/library/datetime.html)
The offset
here's some (very rough) code to give you the idea:
import parsedatetime
import datetime
my_users_timezone = whatever #replace this with a string that will make sense in the offsets dictionary
utc_timezone_offsets_in_hours = {
'utc' : 0,
'pacific' : -8,
# etc
}
parsed_time = parsedatetime.whatever(input_string)
offset_hours = utc_utc_timezone_offsets_in_hours[my_users_timezone]
final_time = parsed_time + datetime.timedelta(hours=offset_hours)
return final_time
parsedatetime's parse routine expects a timetuple() as the sourceTime parameter and should carry over any timezone information you include in it as I don't recall writing any code that overrode it. If it doesn't then it's a bug and let me know.
You can use code like how the answer above suggested for now to add the TZ offset after the parse() routine returns what it has determined the datetime to be:
import parsedatetime as pdt
cal = pdt.Calendar()
start = datetime.datetime.now().timetuple()
parsed, flag = cal.parse('in 1 hr', start)
then you can take the timetuple value of parsed and use timedelta to add your offset hours