Package: django-utz
django-utzdjango-utz provides easy-to-use decorators that allow you easily retrieve/display datetime values in the request user's or a specific user's local timezone without needing to do manual timezone conversions for each user."utz" here means "user time zone". This django app provides decorators for the project's User model, regular django models and django rest framework model serializers.InstallationInstall with pip:pipinstalldjango-utzSetupAdddjango_utzto yourINSTALLED_APPSsetting:INSTALLED_APPS=[...,'django_utz',]Adddjango_utz.middleware.DjangoUTZMiddlewaretoMIDDLEWAREMIDDLEWARE=[...,'django_utz.middleware.DjangoUTZMiddleware',]You can then import and use the required user model decoratorfromdjango.contrib.auth.modelsimportAbstractUserfromdjango_utz.decoratorsimportusermodel@usermodelclassCustomUser(AbstractUser):'''Project's user model'''email=models.EmailField()username=models.CharField(max_length=50)timezone=models.CharField(max_length=20)...classUTZMeta:timezone_field="timezone"Decoratorsdjango_utz comes bundled with three decorators:usermodel: The project's User model must be decorated with this decorator for django_utz to work.model: Regular django models can be decorated with this decorator so that datetime fields in the model can be accessed in request.user's or any specified user local timezone always.modelserializer: Django rest framework model serializers can be decorated with this decorator so that the serializer's model datetime fields will always be returned in request.user's or any specified user local timezone in API responses.Configurations for each model are defined in aUTZMetaclass just inside the model or model serializer class.For instance;fromdjango.dbimportmodelsfromdjango_utz.decoratorsimportmodel@modelclassPost(models.Model):'''Post model'''title=models.CharField(max_length=255)content=models.TextField()created_at=models.DateTimeField(auto_now_add=True)updated_at=models.DateTimeField(auto_now=True)...classUTZMeta:datetime_fields=["created_at","updated_at"]use_related_user_timezone=Truerelated_user="author"The possible configurations for each decorator will be discussed in the next sections.TheusermodeldecoratorTheusermodeldecorator is used to decorate the project's User model. This decorator basically adds django_utz support to the project. It adds a property to the User model that returns the user's timezone info as a zoneinfo.ZoneInfo or pytz.tzinfo object. It also adds a method to the User model that converts a datetime object to the user's local timezone and returns autzdatetimeobject.Let's look at an example of how you would use theusermodeldecorator:Here we will usedjango-timezone-fieldto add a timezone field to our User model. You can use any field you want.pipinstalldjango-timezone-fieldIn settings.py, addtimezone_fieldtoINSTALLED_APPS:INSTALLED_APPS=[...,'django_utz','timezone_field',]In models.py, let's create a User model and decorate it with theusermodeldecorator:fromdjango.dbimportmodelsfromdjango.contrib.auth.modelsimportAbstractBaseUser,PermissionsMixinfromdjango_utz.decoratorsimportusermodelfromtimezone_fieldimportTimeZoneField@usermodelclassCustomUser(AbstractBaseUser,PermissionsMixin):'''User model'''username=models.CharField(max_length=255,unique=True)email=models.EmailField(max_length=255,unique=True)timezone=TimeZoneField(default="UTC")...USERNAME_FIELD="email"REQUIRED_FIELDS=["username"]classMeta:verbose_name="Custom user"verbose_name_plural="Custom users"classUTZMeta:timezone_field="timezone"# valid user timezone info field nameSimple right! Now let's look at the configurations for theusermodeldecorator.The only configuration for theusermodeldecorator is thetimezone_fieldattribute. This attribute is used to specify the field that contains the user timezone info or name, as seen in the example above.Theusermodeldecorator adds a property and a method to the User model. It can be said these added property and method are the backbone of thedjango_utzmodule. Let's look at them:Theutzproperty: This property returns the user's timezone info as a zoneinfo.ZoneInfo or pytz.tzinfo object, even if the user's timezone is stored as a string in the database.The type of timezone info object returned depends mainly on theUSE_DEPRECATED_PYTZsetting. IfUSE_DEPRECATED_PYTZis set toTrue, the timezone info object returned will be a pytz.tzinfo object. Otherwise, the timezone info object returned will be a zoneinfo.ZoneInfo object.Theto_local_timezonemethod: This method converts any datetime object passed to it to autzdatetimeobject in the user's local timezone as returned by theutzproperty.To see how these work, we can create a user instance and use theutzproperty andto_local_timezonemethod:fromdjango.utilsimporttimezonemy_user=CustomUser.objects.create_user(username="Tolu",email="testing321@gmail.com",password="testing321",timezone="Africa/Lagos")# Getting the user's timezone infouser_timezone=my_user.utzprint(user_timezone)# Africa/Lagosprint(type(user_timezone))# zoneinfo.ZoneInfo, assuming USE_DEPRECATED_PYTZ is set to False# Converting a datetime object in server's timezone (UTC) to user's local timezonenow=timezone.now()print(now)# 1996-12-19 16:39:57.000200+00:00now_in_user_tz=my_user.to_local_timezone(now)print(now_in_user_tz)# 1996-12-19 17:39:57.000200+01:00In the above example, we can see that the current time in the user's timezone is one hour ahead of the current time in the server's timezone.Note that theto_local_timezonemethod returns autzdatetimeobject. This is because theutzdatetimeobject is independent of settings.USE_TZ. This means that theutzdatetimeobject will always be in the user's timezone regardless of the value of settings.USE_TZ. Read more on theutzdatetimeobjecthere.ThemodeldecoratorThemodeldecorator is used to decorate regular django models. This decorator allows you to access model datetime fields in a user's local timezone. The user can be the request user or any specified user. By default, the request user is used.Curious about how this works? To keep things simple, the decorator uses a kind of descriptor called theFunctionAttributedescriptor to add a new version of each datetime field to the model class. This new version of the datetime field is suffixed with "utz"(by default) and is a property that returns the datetime field in the user's local timezone.Say for example, we have a model calledPostand we want to access thecreated_atfield in the request user's timezone.fromdjango.dbimportmodelsfromdjango_utz.decoratorsimportmodel@modelclassPost(models.Model):'''Post model'''title=models.CharField(max_length=255)content=models.TextField()author=models.ForeignKey(CustomUser,on_delete=models.CASCADE)created_at=models.DateTimeField(auto_now_add=True)updated_at=models.DateTimeField(auto_now=True)...classUTZMeta:datetime_fields="__all__"The decorator adds a new attribute to thePostmodel calledcreated_at_utz. Let's create a post instance and see how this works:fromdjango.contrib.authimportget_user_modelUser=get_user_model()tolu=User.objects.get(username="Tolu")new_post=Post.objects.create(title="New Post",content="Lorem Ipsum",author=tolu)# If the post was created at 12:00:00 UTC, it will be 13:00:00 in the user's timezone.# Remember, Tolu's timezone is "Africa/Lagos"print(new_post.created_at_utz)# 1996-12-19 13:00:00.000200+01:00Themodeldecorator has a few configurations one of which was used in the model definition above. The configurations are:datetime_fields: A list or tuple of the model's datetime fields for which user timezone aware fields should be added. If set to"__all__", all datetime fields in the model will have user timezone aware fields added.use_related_user_timezone: An optional boolean value that determines if the user timezone aware fields should be based on the timezone of a directly related user, not the request user. This defaults toFalse.The related user in the case of thePostmodel is thepost.author. If a related user is not found directly in the model, django_utz tries to search for a user in the related models of the model. A related model is usually a model that is linked by a ForeignKey or OneToOneField to the current model.To define explicitly the user whose timezone will be used, you can set this comfiguration toTrueand then defined the path to user on therelated_userconfiguration.related_user: This is also an optional configuration that specifies the path to the user whose timezone will be used. This is only used whenuse_related_user_timezoneis set toTrue. The path is a string that represents the path to the user from the model. The path is in the formatrelated_model.related_model....user_field.Looks complicated? Let's look at two examples:Example 1:Assume we want all datetime fields in thePostmodel to always be returned in the post author's timezone. We can do this:fromdjango.dbimportmodelsfromdjango_utz.decoratorsimportmodel@modelclassPost(models.Model):'''Post model'''title=models.CharField(max_length=255)content=models.TextField()author=models.ForeignKey(CustomUser,on_delete=models.CASCADE)created_at=models.DateTimeField(auto_now_add=True)updated_at=models.DateTimeField(auto_now=True)...classUTZMeta:datetime_fields="__all__"use_related_user_timezone=Truerelated_user="author"So no matter the user that makes the request, the datetime fields in thePostmodel will always be returned in the post author's timezone.You could also decide to not specify therelated_userconfiguration. django_utz will use the first user it finds in the related models of the model. Which is theauthorin this case.Example 2:Let's add a new model calledCommentthat is related to thePostmodel. However, we want all datetime fields in theCommentmodel to always be returned in the post author's timezone. We can do this:fromdjango.dbimportmodelsfromdjango_utz.decoratorsimportmodel@modelclassComment(models.Model):'''Comment model'''post=models.ForeignKey(Post,on_delete=models.CASCADE)author=models.ForeignKey(CustomUser,on_delete=models.CASCADE)content=models.TextField()created_at=models.DateTimeField(auto_now_add=True)updated_at=models.DateTimeField(auto_now=True)...classUTZMeta:datetime_fields="__all__"use_related_user_timezone=Truerelated_user="post.author"In this case, therelated_userconfiguration is set to"post.author". This means that the datetime fields in theCommentmodel will always be returned in the post author's timezone, not the comment author's timezone (to be clear, the comment author is the user that made the comment).Hence, Assuming 'Tolu' already created a new post called 'new_post' and we created a user called 'Tade' with a timezone of "Asia/Tokyo", we can do this:tade=User.objects.get(username="Tade")comment=Comment.objects.create(post=new_post,author=tade,content="Nice post!")print(comment.created_at_utz)# 1996-12-19 13:00:00.000200+01:00 (Africa/Lagos)print(comment.author.utz)# Asia/Tokyoprint(comment.author.to_local_timezone(comment.created_at))# 1996-12-19 21:00:00.000200+09:00 (Asia/Tokyo)attribute_suffix: This is an optional configuration that specifies the suffix to be added to the user timezone aware fields. This defaults to "utz".You may want to access created_at in the user's timezone ascreated_at_localinstead ofcreated_at_utz. You can do this:@modelclassPost(models.Model):'''Post model'''...classUTZMeta:...attribute_suffix="local"ThemodelserializerdecoratorThis decorator is used to decorate django rest framework model serializers. As said at the beginning, this decorator allows you to always return the serializer's model datetime fields in the request user's local timezone or any specified user's local timezone(based on the configurations in the serializer's model) in API responses.However, for the decorator to work, the serializer's model must have also been decorated with themodeldecorator.Let's say we want to create a serializer for our modelPost:fromrest_frameworkimportserializersfromdjango_utz.decoratorsimportmodelserializerfrommy_app.modelsimportPost@modelserializerclassPostSerializer(serializers.ModelSerializer):'''Post serializer'''classMeta:model=Postfields="__all__"classUTZMeta:auto_add_fields=TrueThe available configurations for themodelserializerdecorator are:auto_add_fields: This is a boolean value that determines if the decorator should automatically add timezone aware serializer fields to the serializer. This defaults toTrue.datetime_format: This is an optional string that specifies the format to be used when serializing the datetime fields.If you need complete control over the serializer fields, you can set theauto_add_fieldsconfig toFalseor decide not to decorate the model serializer and then add the timezone aware serializer fields manually. Read more on timezone aware serializer fieldshere.Also, you do not need to worry about the serializers Meta configuration not been applied. Themodelserializerdecorator respects the Meta configuration of the serializer so all excluded fields, read_only fields, and extra keyword arguments etc will be respected.FieldsSerializer FieldsIn the case that you need complete control over the auto-added serializer fields, you can add user timezone aware serializer datetime fields manually.UTZDateTimeField: A serializer field that represents a timezone aware datetime field in the preferred user's local timezone (as defined in the serializer's model).fromrest_frameworkimportserializersfromdjango_utz.serializers_fieldsimportUTZDateTimeFieldclassPostSerializer(serializers.ModelSerializer):'''Post serializer'''created_at=UTZDateTimeField(read_only=True,format="%Y-%m-%d%H:%M:%S %Z (%z)")updated_at=UTZDateTimeField(read_only=True,label="Last updated at",format="%Y-%m-%d%H:%M:%S %Z (%z)")classMeta:model=Postfields="__all__"SignalsThedjango_utz.signalsmodule contains signals that are sent when an event occurs on a user's timezone. The signals are:user_timezone_changed: Sent when a user's timezone is updated.In your receiver, you can access the user object, its previous timezone and current timezone with theinstance,previous_timezoneandcurrent_timezonekeyword arguments respectively.fromdjango.dispatchimportreceiverfromdjango_utz.signalsimportuser_timezone_changed@receiver(user_timezone_changed)defuser_timezone_changed_receiver(sender,**kwargs):old_timezone=kwargs.get("previous_timezone")new_timezone=kwargs.get("current_timezone")print(old_timezone,new_timezone)TemplatesTemplate TagsThedjango_utz.templatetags.django_utzmodule contains a template tag that allows you to display datetime objects in the preferred user's local timezone. To use in a template, load the module in your template:{% load django_utz %}usertimezone: This is a block tag that renders template content with datetime object(s) contained within the block in the request user's timezone but the preferred user can be passed as an argument or keyword argument. Assuming that we want to write a template for our post list view, we can do this:Here the datetime objects,post.created_atis rendered in the request user's timezone:{% load django_utz %}
{% usertimezone %}
    {% for post in posts %}<h3>{{ post.title }}</h3><p>{{ post.created_at }}</p><p>{{ post.author }}</p>{% endfor %}
{% endusertimezone %}Say we want to render the datetime object,post.created_atin the timezone of the author of the first post, we can do this:First approach:{% load django_utz %}
{% usertimezone posts[0].author %}
    {% for post in posts %}<h3>{{ post.title }}</h3><p>{{ post.created_at }}</p><p>{{ post.author }}</p>{% endfor %}
{% endusertimezone %}Alternatively;{% load django_utz %}
{% usertimezone user=posts[0].author %}
    {% for post in posts %}<h3>{{ post.title }}</h3><p>{{ post.created_at }}</p><p>{{ post.author }}</p>{% endfor %}
{% endusertimezone %}Template Filtersdjango_utz also provides a template filter that allows you to display datetime objects in the preferred user's local timezone.usertimezone: This filter returns a datetime object in the request user's timezone but the preferred user can be passed as an argument.Say we want to write a template for our post detail view, we can do this:{% load django_utz %}<h3>{{ post.title }}</h3><p>{{ post.content }}</p><p>{{ post.created_at|usertimezone }}</p><p>{{ post.author }}</p>This works the same way as theusertimezonetemplate tag.
We can also pass the preferred user as an argument:{% load utz %}<h3>{{ post.title }}</h3><p>{{ post.content }}</p><p>{{ post.created_at|usertimezone:post.author }}</p><p>{{ post.author }}</p>TheutzdatetimeobjectThis is a custom datetime that can be independent of settings.USE_TZ. It inherits fromdatetime.datetime. The reason this exists is because when USE_TZ is set to True, sometimes, datetime objects are converted back to the server's timezone. This can be a problem when you want to view datetime objects in a timezone other than the server's timezone.A peculiar case is in templates. If you try to access say,post.created_at_utzin a template, you'd get the datetime object in the server's timezone. This is because the datetime object is converted back to the server's timezone when USE_TZ is set to True. Hence, the reason why autzdatetimeis used.This object is timezone aware and can be used to display datetime objects in any timezone.This concept was gotten fromdjango.templatetags.tz'sdatetimeobject.Theutzdatetimeobject has the following methods:from_datetime(cls, _datetime: datetime.datetime): This class method allows the construction of autzdatetimeobject from a datetime object.importdatetimefromdjango_utz.datetimeimportutzdatetimenormal_datetime=datetime.datetime.now()utz_datetime=utzdatetime.from_datetime(normal_datetime)regard_usetz(): This method makes theutzdatetimeobject respect settings.USE_TZ when being converted to a timezone. It may be converted back to server's timezone if USE_TZ is set to True.utz_datetime.regard_usetz()# Now the utzdatetime object is dependent on settings.USE_TZdisregard_usetz(): This method makes theutzdatetimeobject disregard settings.USE_TZ when being converted to a timezone. It will not be converted back to server's timezone if USE_TZ is set to True.utz_datetime.disregard_usetz()# Now the utzdatetime object is independent of settings.USE_TZContributions and feedbacks are welcome. For feedbacks, please open an issue. To contribute, please fork the repo and submit a pull request. Thanks!