2

So I have this Post model. I want to be able to retrieve all posts that were created in a month, year under a certain time zone.
My goal is to implement a feature where a user anywhere in the world let's say in PST can get all posts by another person from a certain month in their time zone. So let's say user A is in EST and user B is in PST (3 hours behind EST). User B wants to see all posts that user A created in October of 2021. Since the app will display posts in the time zone the user is currently in (we send date time in UTC then the front-end converts to local time) then the app should only send to user B all posts by user A that were created in October 2021 PST. So for example if user A (the user in EST) made a post at 11pm Oct 31 2021 EST(8pm Oct 31 2021 PST) and a post at 1am Nov 1st 2021 EST (10pm Oct 31st 2021 PST) then user B should on get both posts back, because the 2nd one was made in November in EST, but October in PST.

model.py

class Post(models.Model):
    uuid = models.UUIDField(primary_key=True)
    created = models.DateTimeField('Created at', auto_now_add=True)
    updated_at = models.DateTimeField('Last updated at', auto_now=True, blank=True, null=True)
    creator = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name="post_creator")
    body = models.CharField(max_length=POST_MAX_LEN)

So for example if a user creates 10 posts in November, 2 in December of 2021 in PST. Then I have a view that takes month, year and time_zone and let's say the url looks something like /post/<int:month>/<int:year>/<str:time_zone> and the user pings /post/11/2021/PST then it should return the 10 posts from November. How do I return all posts from a month and year in a time zone given time zone, month and year?

Note: The tricky edge case to take into consideration is if they post on the very last day of a month very late. Depending on the time zone something like 12/31/2021 in UTC could be 01/01/2022. Because Django stores datetime fields in UTC what would need to be done is converted created to the given time_zone then get posts from the specified month and year.

Setup:

  • Django 3.2.9
  • Postgresql

Attempted Solutions

  • The most obvious solution to me is to convert created to the specified time_zone then to do Post.objects.filter(created__in_range=<some range>)

Note
Main issue seems to be Pyzt, which takes in time in a very specific format "Amercian/Los_Angeles" w.e format this is. Rather than the abbreviated time zone format like "PST".

  • 1
    Take the month's first moment (midnight on the 1st) in UTC and the next month's first moment in UTC, adjust them with the timezone you want, do a `posted__range=(a, b)` query? – AKX Dec 21 '21 at 19:35
  • sorry I'm a bit lost –  Dec 21 '21 at 19:38
  • @AKX looks like there needs to be a conversion from UTC to PST first? –  Dec 21 '21 at 20:14
  • You will need to log hour and minuet in the time as well for the conversion to work. Otherwise how do yo know to transfer the day to a new month or not? – James_B Dec 21 '21 at 22:07
  • @James_B yes `created` is a date time field so it does include hour. –  Dec 21 '21 at 22:21
  • I think the first step is convert `created` to the specified `time_zone` then query. –  Dec 21 '21 at 22:22
  • @AKX you gave me a raw SQL query before that converts UTC to whatever timezone then I can filter by month after. The raw SQL query was for something else, but should be applicable here too. –  Dec 22 '21 at 01:49

4 Answers4

3

Take the month's first moment (midnight on the 1st) in UTC and the next month's first moment in UTC, adjust them with the timezone you want, do a posted__range=(a, b) query?

This might work (but date math is fiddly...).

This requires python-dateutil to make computing the end time robust.

import datetime
import pytz

from dateutil.relativedelta import relativedelta
from django.utils import timezone

year = 2021
month = 6
tz = pytz.timezone("America/Los_Angeles")

start = datetime.datetime(year, month, 1)
end = start + relativedelta(months=1)
start_as_tz_in_utc = tz.localize(start).astimezone(pytz.utc)
end_as_tz_in_utc = tz.localize(end).astimezone(pytz.utc)
print(start_as_tz_in_utc, end_as_tz_in_utc, sep="\n")

That prints out

2021-06-01 07:00:00+00:00
2021-07-01 07:00:00+00:00

which seems about right.

Then you might make a query such as

posts = Post.objects.filter(created__range=(
  start_as_tz_in_utc,
  end_as_tz_in_utc,
))
AKX
  • 152,115
  • 15
  • 115
  • 172
  • `NotImplementedError: a tzinfo subclass must implement utcoffset()` is what I'm getting from `tz = datetime.tzinfo("PST")` –  Dec 23 '21 at 05:28
  • Which version of Python and Django are you working with? That code worked fine for me in the console. – AKX Dec 23 '21 at 06:43
  • Django is 3.2.9 and Python is 3.8. What did you run on? –  Dec 23 '21 at 09:05
  • Sorry, yeah, I screwed up there. Rewrote answer :) – AKX Dec 23 '21 at 09:18
  • is there a way to get the pytz time zone off say "PST"? Or is using something like "America/Los Angeles" better? –  Dec 23 '21 at 10:01
  • Pytz doesn't seem to know the name "PST", I think `US/Pacific` is another valid alias for that time zone... – AKX Dec 23 '21 at 10:03
  • hmm that's an issue because I'm passing it as part of a url in Django so the `/` in between is confusing the url. –  Dec 23 '21 at 10:25
  • perhaps converting to timezone and getting all posts from month X with a raw SQL query will work? –  Dec 23 '21 at 10:35
  • You can replace it with a `_` in your frontend and back to a `/` in the back, if that's a problem (or use query parameters or something else where `/` isn't a problem). – AKX Dec 23 '21 at 11:00
  • This question actually relates to the other question you were nice enough to answer. It just feels a little weird to me to use "PST" for that endpoint and then "USA/Pacific" for this endpont –  Dec 23 '21 at 11:07
  • Well – `if tz_name == "PST": tz_name = "US/Pacific"`. (Or a fancy mapping dictionary if you get more of these.) – AKX Dec 23 '21 at 11:20
  • Yea that could work. You know of any mapping dicts for that? –  Dec 23 '21 at 12:24
  • I'm being told in another StackOverflow question I posted that "PST" doesn't take into consideration daylights saving but "US/Pacific" does. Perhaps it might be better if the other question you answered used that instead of "PST". Is that possible with SQL? –  Dec 23 '21 at 13:38
  • if we can get this sorted I'll give you the bounty –  Dec 27 '21 at 18:43
  • 1
    PST is Pacific time without daylight saving. PDT is Pacific time with daylight saving. US/Pacific is a ruleset that knows about daylight saving and switches between the two. If you only care about the specific local time the user is in, have the client send the timezone offset instead of a timezone name. – AKX Dec 28 '21 at 07:20
  • do you happen to know how this can be obtained in say React Native? –  Dec 28 '21 at 08:41
  • would you mind adjusting your answer to this question https://stackoverflow.com/questions/70418245/django-return-a-list-of-months-that-a-query-of-post-objects-spans-to-front-end to account for timezone offset in the SQL query? –  Dec 28 '21 at 08:42
  • 1
    In plain JavaScript, `new Date().getTimezoneOffset()` returns `-120` since I'm 2 hours ahead of UTC. – AKX Dec 28 '21 at 08:51
  • is there still the problem of converting offset to a `pytz` understandable format for the range filter? Or does `pytz` have a way of handling that? –  Dec 28 '21 at 14:03
  • If you're just working with offsets, you don't need to even think about timezones. Just add or subtract minutes from UTC timestamps. – AKX Dec 28 '21 at 14:25
  • can you add the offset as a new answer as well as how to incorporate it into that previous question you helped me with? I'll test it out then when I get it working I'll give you the bounty. –  Dec 28 '21 at 15:35
  • https://stackoverflow.com/questions/70550687/django-return-months-and-years-that-posts-by-a-user-spans-given-time-offset-fr –  Jan 03 '22 at 10:51
  • actually I think I can get it in this specified format you wrote as your answer using MomentJS –  Jan 30 '22 at 05:05
0

I would try something like this:

Assuming you are passing a given month through as given_month and given year as given_year and given timezone as given_timezone

Model_Name.objects.filter(created.split('/',4)[2]=given_month,created.split('/',4)[3]=given_year, created.split('/',4)[4]=given_timezone)

This should get the value from the month and year and timzone section of your post. You might need to play around with what I gave you. Also it might be better to add a relationship from user to post so you can filter the user for posts with my given answer. In most cases this will be a lot more efficient, assuming there are more posts than users. This should get you on the correct track.

James_B
  • 137
  • 7
  • If you run this. you will see how the split is working: ```ex = "/post/11/2021/PST" month = ex.split('/',4)[2] year = ex.split('/',4)[3] print (month, " ", year)``` – James_B Dec 21 '21 at 19:48
  • what other piece of code do you need? I felt the model was all that was needed to answer this question because it's essentially some sort of `Model_Name.objects.filter()` function. –  Dec 21 '21 at 19:48
  • don't you need to account for time zone? I mentioned why in the original post there's an edge case that needs consideration. –  Dec 21 '21 at 19:49
  • Are you querying from a user or from the post class? If it is from the user, what is the relationship (ie 1 to 1) between User class an Post? Explain how you will be using it (give an example ) and I can better answer your question. – James_B Dec 21 '21 at 19:53
  • If you want the timezone, use ```Model_Name.objects.filter(created.split('/',4)[2]=given_month,created.split('/',4)[3]=given_year, created.split('/',4)[4]=given_timezone)``` – James_B Dec 21 '21 at 19:57
  • so it's just a user sending a request to the server. Not actually a user model. Sorry for the ambiguity. So essentially the user's mobile app would send the time zone with their request. Then it would just query the Post table. –  Dec 21 '21 at 20:10
  • so the last comment you posted looks a bit off. `created` needs to be converted from the standard time zone Django stores in, which is UTC to PST then the rest of the query should be what I need. –  Dec 21 '21 at 20:12
  • Why even use a time zone then? No need to convert just manually set to PST. You are not keeping the minutes and hours in here so all you would be doing is changing 3 letters. to a different 3 letters. Am I miss understanding it? The time zone seems kind of irrelevant here from my understanding at least. – James_B Dec 21 '21 at 21:25
  • yes I think there is a misunderstand let me update the original question –  Dec 21 '21 at 21:44
0

Consider saving the record in UTC date time, do not attempt to save any time zone offset. just record the time the data arrived to the server in UTC, and let the browser or your app translate it to their configured timezone. It doesnt matter if your User is in China created a post and then moved to NY the next day and read the previously created post, the created_at datetime will be same for both locations because it is universal. So if the user B wants to recover the data the user A recorded since october it will as easy to querying created_at::date>="2021-10-01"

David I. Rock
  • 95
  • 2
  • 4
  • The issue is if user switches time zones. So for example lets say the user creates a post at 11pm PST in November 31st then goes to NYC and gets all post for December. That 11p PST Nov 31st post should not be returned with the November posts for the user, but with the December posts. Because NYC is 3 hours ahead of PST –  Dec 28 '21 at 19:04
0

Sorry, if it is not very on point to what you're expecting, but Django creates a PostgreSQL timestamp with a time zone field for the DateTimeField.

# \d posts_post
                       Table "public.posts_post"
   Column   |           Type           | Collation | Nullable | Default
------------+--------------------------+-----------+----------+---------
 uuid       | uuid                     |           | not null |
 created    | timestamp with time zone |           | not null |
 updated_at | timestamp with time zone |           |          |
 body       | character varying(500)   |           | not null |
Indexes:
    "posts_post_pkey" PRIMARY KEY, btree (uuid)

It means you can query them as timestamps with a custom field class from Timestamp fields in django.

from django.db import models
from datetime import datetime
from time import strftime

class UnixTimestampField(models.DateTimeField):
    """UnixTimestampField: creates a DateTimeField that is represented on the
    database as a TIMESTAMP field rather than the usual DATETIME field.
    """
    def __init__(self, null=False, blank=False, **kwargs):
        super(UnixTimestampField, self).__init__(**kwargs)
        # default for TIMESTAMP is NOT NULL unlike most fields, so we have to
        # cheat a little:
        self.blank, self.isnull = blank, null
        self.null = True # To prevent the framework from shoving in "not null".

    def db_type(self, connection):
        typ=['TIMESTAMP']
        # See above!
        if self.isnull:
            typ += ['NULL']
        if self.auto_created:
            typ += ['default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP']
        return ' '.join(typ)

    def to_python(self, value):
        if isinstance(value, int):
            return datetime.fromtimestamp(value)
        else:
            return models.DateTimeField.to_python(self, value)

    def get_db_prep_value(self, value, connection, prepared=False):
        if value==None:
            return None
        # Use '%Y%m%d%H%M%S' for MySQL < 4.1
        return strftime('%Y-%m-%d %H:%M:%S',value.timetuple())

In the query, you will pass the timestamp created from the required timezone DateTime. This approach is also should be faster since you don't need to perform 1 additional DateTime conversion.

class Post(models.Model):
    uuid = models.UUIDField(primary_key=True)
    created = UnixTimestampField(verbose_name='Created at', auto_now_add=True)
    updated_at = UnixTimestampField(verbose_name='Last updated at', auto_now=True, blank=True, null=True)


from datetime import datetime, timezone

Post.objects.filter(created__gte=datetime(year=2021, month=11, day=1, tzinfo=timezone.utc).timestamp(), created__lt=datetime(year=2021, month=12, day=1, tzinfo=timezone.utc).timestamp())
  • To your second part of the question, you can get the last day of the month with https://docs.python.org/3/library/calendar.html#calendar.monthrange module, and create timestamp from proper DateTime with a proper timestamp from it. – Ivan Markeev Jan 03 '22 at 13:03