add API support for user timezone and working hours

This commit is contained in:
Vadim Stepanov 2022-07-05 13:20:26 +01:00
parent 3d92e6967d
commit 3387c4be0f
No known key found for this signature in database
GPG key ID: 6C3A5A9D5F62C114
4 changed files with 105 additions and 6 deletions

View file

@ -1,3 +1,6 @@
import time
import pytz
from django.conf import settings
from rest_framework import serializers
@ -9,6 +12,7 @@ from apps.base.utils import live_settings
from apps.oss_installation.utils import cloud_user_identity_status
from apps.twilioapp.utils import check_phone_number_is_valid
from apps.user_management.models import User
from apps.user_management.models.user import default_working_hours
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField
from common.api_helpers.mixins import EagerLoadingMixin
from common.constants.role import Role
@ -29,6 +33,7 @@ class UserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin):
organization = FastOrganizationSerializer(read_only=True)
current_team = TeamPrimaryKeyRelatedField(allow_null=True, required=False)
timezone = serializers.CharField(allow_null=True, required=False)
avatar = serializers.URLField(source="avatar_url", read_only=True)
permissions = serializers.SerializerMethodField()
@ -47,6 +52,8 @@ class UserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin):
"username",
"role",
"avatar",
"timezone",
"working_hours",
"unverified_phone_number",
"verified_phone_number",
"slack_user_identity",
@ -63,6 +70,49 @@ class UserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin):
"verified_phone_number",
]
def validate_timezone(self, tz):
if tz is None:
return tz
try:
pytz.timezone(tz)
except pytz.UnknownTimeZoneError:
raise serializers.ValidationError("not a valid timezone")
return tz
def validate_working_hours(self, working_hours):
if not isinstance(working_hours, dict):
raise serializers.ValidationError("must be dict")
# check that all days are present
if sorted(working_hours.keys()) != sorted(default_working_hours().keys()):
raise serializers.ValidationError("missing some days")
for day in working_hours:
periods = working_hours[day]
for period in periods:
if not isinstance(period, dict):
raise serializers.ValidationError("period must be dict")
if sorted(period.keys()) != sorted(["start", "end"]):
raise serializers.ValidationError("'start' and 'end' fields must be present")
if not isinstance(period["start"], str) or not isinstance(period["end"], str):
raise serializers.ValidationError("'start' and 'end' fields must be str")
try:
start = time.strptime(period["start"], "%H:%M:%S")
end = time.strptime(period["end"], "%H:%M:%S")
except ValueError:
raise serializers.ValidationError("'start' and 'end' fields must be in '%H:%M:%S' format")
if start >= end:
raise serializers.ValidationError("'start' must be less than 'end'")
return working_hours
def validate_unverified_phone_number(self, value):
if value:
if check_phone_number_is_valid(value):

View file

@ -1,6 +1,7 @@
import logging
from urllib.parse import urljoin
import pytz
from django.apps import apps
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
@ -123,7 +124,7 @@ class UserView(
"mobile_app_verification_token",
"mobile_app_auth_token",
),
AnyRole: ("retrieve",),
AnyRole: ("retrieve", "timezone_options"),
}
action_object_permissions = {
@ -236,6 +237,10 @@ class UserView(
serializer = UserSerializer(self.get_queryset().get(pk=self.request.user.pk))
return Response(serializer.data)
@action(detail=False, methods=["get"])
def timezone_options(self, request):
return Response(pytz.common_timezones)
@action(detail=True, methods=["get"])
def get_verification_code(self, request, pk):
user = self.get_object()

View file

@ -0,0 +1,24 @@
# Generated by Django 3.2.13 on 2022-07-05 12:14
import apps.user_management.models.user
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user_management', '0001_squashed_initial'),
]
operations = [
migrations.AddField(
model_name='user',
name='_timezone',
field=models.CharField(default=None, max_length=50, null=True),
),
migrations.AddField(
model_name='user',
name='working_hours',
field=models.JSONField(default=apps.user_management.models.user.default_working_hours, null=True),
),
]

View file

@ -30,6 +30,16 @@ def generate_public_primary_key_for_user():
return new_public_primary_key
def default_working_hours():
weekdays = ["monday", "tuesday", "wednesday", "thursday", "friday"]
weekends = ["saturday", "sunday"]
working_hours = {day: [{"start": "09:00:00", "end": "17:00:00"}] for day in weekdays}
working_hours |= {day: [] for day in weekends}
return working_hours
class UserManager(models.Manager):
@staticmethod
def sync_for_team(team, api_members: list[dict]):
@ -128,6 +138,10 @@ class User(models.Model):
role = models.PositiveSmallIntegerField(choices=Role.choices())
avatar_url = models.URLField()
# don't use "_timezone" directly, use the "timezone" property since it can be populated via slack user identity
_timezone = models.CharField(max_length=50, null=True, default=None)
working_hours = models.JSONField(null=True, default=default_working_hours)
notification = models.ManyToManyField("alerts.AlertGroup", through="alerts.UserHasNotification")
unverified_phone_number = models.CharField(max_length=20, null=True, default=None)
@ -222,11 +236,17 @@ class User(models.Model):
@property
def timezone(self):
slack_user_identity = self.slack_user_identity
if slack_user_identity:
return slack_user_identity.timezone
else:
return None
if self._timezone:
return self._timezone
if self.slack_user_identity:
return self.slack_user_identity.timezone
return None
@timezone.setter
def timezone(self, value):
self._timezone = value
def short(self):
return {"username": self.username, "pk": self.public_primary_key, "avatar": self.avatar_url}