Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WEB-2884] chore: Update timezone list, add new endpoint, and update timezone dropdowns #6231

Merged
merged 2 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apiserver/plane/app/urls/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from .views import urlpatterns as view_urls
from .webhook import urlpatterns as webhook_urls
from .workspace import urlpatterns as workspace_urls
from .timezone import urlpatterns as timezone_urls

urlpatterns = [
*analytic_urls,
Expand All @@ -38,4 +39,5 @@
*workspace_urls,
*api_urls,
*webhook_urls,
*timezone_urls,
]
8 changes: 8 additions & 0 deletions apiserver/plane/app/urls/timezone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.urls import path

from plane.app.views import TimezoneEndpoint

urlpatterns = [
# timezone endpoint
path("timezones/", TimezoneEndpoint.as_view(), name="timezone-list")
]
2 changes: 2 additions & 0 deletions apiserver/plane/app/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,5 @@

from .notification.base import MarkAllReadNotificationViewSet
from .user.base import AccountEndpoint, ProfileEndpoint, UserSessionEndpoint

from .timezone.base import TimezoneEndpoint
247 changes: 247 additions & 0 deletions apiserver/plane/app/views/timezone/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
# Python imports
import pytz
from datetime import datetime

# Django imports
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page

# Third party imports
from rest_framework import status
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from rest_framework.views import APIView

# Module imports
from plane.authentication.rate_limit import AuthenticationThrottle


class TimezoneEndpoint(APIView):
permission_classes = [AllowAny]

throttle_classes = [AuthenticationThrottle]

@method_decorator(cache_page(60 * 60 * 24))
def get(self, request):
timezone_mapping = {
"-1100": [
("Midway Island", "Pacific/Midway"),
("American Samoa", "Pacific/Pago_Pago"),
],
"-1000": [
("Hawaii", "Pacific/Honolulu"),
("Aleutian Islands", "America/Adak"),
],
"-0930": [("Marquesas Islands", "Pacific/Marquesas")],
"-0900": [
("Alaska", "America/Anchorage"),
("Gambier Islands", "Pacific/Gambier"),
],
"-0800": [
("Pacific Time (US and Canada)", "America/Los_Angeles"),
("Baja California", "America/Tijuana"),
],
"-0700": [
("Mountain Time (US and Canada)", "America/Denver"),
("Arizona", "America/Phoenix"),
("Chihuahua, Mazatlan", "America/Chihuahua"),
],
"-0600": [
("Central Time (US and Canada)", "America/Chicago"),
("Saskatchewan", "America/Regina"),
("Guadalajara, Mexico City, Monterrey", "America/Mexico_City"),
("Tegucigalpa, Honduras", "America/Tegucigalpa"),
("Costa Rica", "America/Costa_Rica"),
],
"-0500": [
("Eastern Time (US and Canada)", "America/New_York"),
("Lima", "America/Lima"),
("Bogota", "America/Bogota"),
("Quito", "America/Guayaquil"),
("Chetumal", "America/Cancun"),
],
"-0430": [("Caracas (Old Venezuela Time)", "America/Caracas")],
"-0400": [
("Atlantic Time (Canada)", "America/Halifax"),
("Caracas", "America/Caracas"),
("Santiago", "America/Santiago"),
("La Paz", "America/La_Paz"),
("Manaus", "America/Manaus"),
("Georgetown", "America/Guyana"),
("Bermuda", "Atlantic/Bermuda"),
],
"-0330": [("Newfoundland Time (Canada)", "America/St_Johns")],
"-0300": [
("Buenos Aires", "America/Argentina/Buenos_Aires"),
("Brasilia", "America/Sao_Paulo"),
("Greenland", "America/Godthab"),
("Montevideo", "America/Montevideo"),
("Falkland Islands", "Atlantic/Stanley"),
],
"-0200": [
(
"South Georgia and the South Sandwich Islands",
"Atlantic/South_Georgia",
)
],
"-0100": [
("Azores", "Atlantic/Azores"),
("Cape Verde Islands", "Atlantic/Cape_Verde"),
],
"+0000": [
("Dublin", "Europe/Dublin"),
("Reykjavik", "Atlantic/Reykjavik"),
("Lisbon", "Europe/Lisbon"),
("Monrovia", "Africa/Monrovia"),
("Casablanca", "Africa/Casablanca"),
],
"+0100": [
("Central European Time (Berlin, Rome, Paris)", "Europe/Paris"),
("West Central Africa", "Africa/Lagos"),
("Algiers", "Africa/Algiers"),
("Lagos", "Africa/Lagos"),
("Tunis", "Africa/Tunis"),
],
"+0200": [
("Eastern European Time (Cairo, Helsinki, Kyiv)", "Europe/Kiev"),
("Athens", "Europe/Athens"),
("Jerusalem", "Asia/Jerusalem"),
("Johannesburg", "Africa/Johannesburg"),
("Harare, Pretoria", "Africa/Harare"),
],
"+0300": [
("Moscow Time", "Europe/Moscow"),
("Baghdad", "Asia/Baghdad"),
("Nairobi", "Africa/Nairobi"),
("Kuwait, Riyadh", "Asia/Riyadh"),
],
"+0330": [("Tehran", "Asia/Tehran")],
"+0400": [
("Abu Dhabi", "Asia/Dubai"),
("Baku", "Asia/Baku"),
("Yerevan", "Asia/Yerevan"),
("Astrakhan", "Europe/Astrakhan"),
("Tbilisi", "Asia/Tbilisi"),
("Mauritius", "Indian/Mauritius"),
],
"+0500": [
("Islamabad", "Asia/Karachi"),
("Karachi", "Asia/Karachi"),
("Tashkent", "Asia/Tashkent"),
("Yekaterinburg", "Asia/Yekaterinburg"),
("Maldives", "Indian/Maldives"),
("Chagos", "Indian/Chagos"),
],
"+0530": [
("Chennai", "Asia/Kolkata"),
("Kolkata", "Asia/Kolkata"),
("Mumbai", "Asia/Kolkata"),
("New Delhi", "Asia/Kolkata"),
("Sri Jayawardenepura", "Asia/Colombo"),
],
"+0545": [("Kathmandu", "Asia/Kathmandu")],
"+0600": [
("Dhaka", "Asia/Dhaka"),
("Almaty", "Asia/Almaty"),
("Bishkek", "Asia/Bishkek"),
("Thimphu", "Asia/Thimphu"),
],
"+0630": [
("Yangon (Rangoon)", "Asia/Yangon"),
("Cocos Islands", "Indian/Cocos"),
],
"+0700": [
("Bangkok", "Asia/Bangkok"),
("Hanoi", "Asia/Ho_Chi_Minh"),
("Jakarta", "Asia/Jakarta"),
("Novosibirsk", "Asia/Novosibirsk"),
("Krasnoyarsk", "Asia/Krasnoyarsk"),
],
"+0800": [
("Beijing", "Asia/Shanghai"),
("Singapore", "Asia/Singapore"),
("Perth", "Australia/Perth"),
("Hong Kong", "Asia/Hong_Kong"),
("Ulaanbaatar", "Asia/Ulaanbaatar"),
("Palau", "Pacific/Palau"),
],
"+0845": [("Eucla", "Australia/Eucla")],
"+0900": [
("Tokyo", "Asia/Tokyo"),
("Seoul", "Asia/Seoul"),
("Yakutsk", "Asia/Yakutsk"),
],
"+0930": [
("Adelaide", "Australia/Adelaide"),
("Darwin", "Australia/Darwin"),
],
"+1000": [
("Sydney", "Australia/Sydney"),
("Brisbane", "Australia/Brisbane"),
("Guam", "Pacific/Guam"),
("Vladivostok", "Asia/Vladivostok"),
("Tahiti", "Pacific/Tahiti"),
],
"+1030": [("Lord Howe Island", "Australia/Lord_Howe")],
"+1100": [
("Solomon Islands", "Pacific/Guadalcanal"),
("Magadan", "Asia/Magadan"),
("Norfolk Island", "Pacific/Norfolk"),
("Bougainville Island", "Pacific/Bougainville"),
("Chokurdakh", "Asia/Srednekolymsk"),
],
"+1200": [
("Auckland", "Pacific/Auckland"),
("Wellington", "Pacific/Auckland"),
("Fiji Islands", "Pacific/Fiji"),
("Anadyr", "Asia/Anadyr"),
],
"+1245": [("Chatham Islands", "Pacific/Chatham")],
"+1300": [("Nuku'alofa", "Pacific/Tongatapu"), ("Samoa", "Pacific/Apia")],
"+1400": [("Kiritimati Island", "Pacific/Kiritimati")],
}
Comment on lines +26 to +202
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider moving timezone mapping to a configuration file.

The large timezone mapping dictionary should be moved to a separate configuration file for better maintainability and readability.

Create a new file timezone_config.py and move the mapping there:

# timezone_config.py
TIMEZONE_MAPPING = {
    "-1100": [
        ("Midway Island", "Pacific/Midway"),
        # ... rest of the mapping
    ],
}


timezone_list = []
now = datetime.now()

# Process timezone mapping
for offset, locations in timezone_mapping.items():
sign = "-" if offset.startswith("-") else "+"
hours = offset[1:3]
minutes = offset[3:] if len(offset) > 3 else "00"

for friendly_name, tz_identifier in locations:
try:
tz = pytz.timezone(tz_identifier)
current_offset = now.astimezone(tz).strftime("%z")

# converting and formatting UTC offset to GMT offset
current_utc_offset = now.astimezone(tz).utcoffset()
total_seconds = int(current_utc_offset.total_seconds())
hours_offset = total_seconds // 3600
minutes_offset = abs(total_seconds % 3600) // 60
gmt_offset = (
f"GMT{'+' if hours_offset >= 0 else '-'}"
f"{abs(hours_offset):02}:{minutes_offset:02}"
)

timezone_value = {
"offset": int(current_offset),
"utc_offset": f"UTC{sign}{hours}:{minutes}",
"gmt_offset": gmt_offset,
"value": tz_identifier,
"label": f"{friendly_name}",
}

timezone_list.append(timezone_value)
except pytz.exceptions.UnknownTimeZoneError:
continue

# Sort by offset and then by label
timezone_list.sort(key=lambda x: (x["offset"], x["label"]))

# Remove offset from final output
for tz in timezone_list:
del tz["offset"]

return Response({"timezones": timezone_list}, status=status.HTTP_200_OK)
1 change: 1 addition & 0 deletions packages/types/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ export * from "./favorite";
export * from "./file";
export * from "./workspace-draft-issues/base";
export * from "./command-palette";
export * from "./timezone";
8 changes: 8 additions & 0 deletions packages/types/src/timezone.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export type TTimezoneObject = {
utc_offset: string;
gmt_offset: string;
label: string;
value: string;
};

export type TTimezones = { timezones: TTimezoneObject[] };
35 changes: 6 additions & 29 deletions web/app/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ import {
import { DeactivateAccountModal } from "@/components/account";
import { LogoSpinner } from "@/components/common";
import { ImagePickerPopover, UserImageUploadModal, PageHead } from "@/components/core";
import { TimezoneSelect } from "@/components/global";
import { ProfileSettingContentWrapper } from "@/components/profile";
// constants
import { TIME_ZONES, TTimezone } from "@/constants/timezones";
import { USER_ROLES } from "@/constants/workspace";
// helpers
import { getFileURL } from "@/helpers/file.helper";
Expand Down Expand Up @@ -120,22 +120,6 @@ const ProfileSettingsPage = observer(() => {
});
};

const getTimeZoneLabel = (timezone: TTimezone | undefined) => {
if (!timezone) return undefined;
return (
<div className="flex gap-1.5">
<span className="text-custom-text-400">{timezone.gmtOffset}</span>
<span className="text-custom-text-200">{timezone.name}</span>
</div>
);
};

const timeZoneOptions = TIME_ZONES.map((timeZone) => ({
value: timeZone.value,
query: timeZone.name + " " + timeZone.gmtOffset + " " + timeZone.value,
content: getTimeZoneLabel(timeZone),
}));

if (!currentUser)
return (
<div className="grid h-full w-full place-items-center px-4 sm:px-0">
Expand Down Expand Up @@ -379,19 +363,12 @@ const ProfileSettingsPage = observer(() => {
control={control}
rules={{ required: "Please select a timezone" }}
render={({ field: { value, onChange } }) => (
<CustomSearchSelect
<TimezoneSelect
value={value}
label={
value
? (getTimeZoneLabel(TIME_ZONES.find((t) => t.value === value)) ?? value)
: "Select a timezone"
}
options={timeZoneOptions}
onChange={onChange}
buttonClassName={errors.user_timezone ? "border-red-500" : ""}
className="rounded-md border-[0.5px] !border-custom-border-200"
optionsClassName="w-72"
input
onChange={(value: string) => {
onChange(value);
}}
error={Boolean(errors.user_timezone)}
/>
)}
/>
Expand Down
2 changes: 2 additions & 0 deletions web/core/components/global/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from "./product-updates";

export * from "./timezone-select";
Loading
Loading