feat: add timezone support and improve logging
Browse files- Add timezone validation and support in schedule creation
- Replace print statements with proper logging
- Configure logging levels for better control
- Add timezone utilities for handling user timezones
- Improve error handling and user feedback
- Update scheduler service to handle timezone information
```
This commit message follows the conventional commit format and provides a clear summary of the changes made, including the addition of timezone support and improved logging throughout the application. It explains what was changed and why, making it easier for other developers to understand the purpose of these modifications.
- backend/api/schedules.py +27 -10
- backend/app.py +3 -3
- backend/scheduler/apscheduler_service.py +45 -42
- backend/services/schedule_service.py +16 -3
- backend/utils/timezone_utils.py +196 -0
- frontend/src/pages/Schedule.jsx +11 -2
- frontend/src/services/scheduleService.js +6 -1
- frontend/src/utils/timezoneUtils.js +74 -0
- simple_timezone_test.py +171 -0
- test_timezone_functionality.py +190 -0
backend/api/schedules.py
CHANGED
@@ -1,8 +1,11 @@
|
|
1 |
from flask import Blueprint, request, jsonify, current_app
|
2 |
from flask_jwt_extended import jwt_required, get_jwt_identity
|
3 |
from backend.services.schedule_service import ScheduleService
|
|
|
|
|
4 |
|
5 |
schedules_bp = Blueprint('schedules', __name__)
|
|
|
6 |
|
7 |
@schedules_bp.route('/', methods=['OPTIONS'])
|
8 |
@schedules_bp.route('', methods=['OPTIONS'])
|
@@ -22,11 +25,11 @@ def get_schedules():
|
|
22 |
"""
|
23 |
try:
|
24 |
user_id = get_jwt_identity()
|
25 |
-
|
26 |
|
27 |
# Check if Supabase client is initialized
|
28 |
if not hasattr(current_app, 'supabase') or current_app.supabase is None:
|
29 |
-
|
30 |
# Add CORS headers to error response
|
31 |
response_data = jsonify({
|
32 |
'success': False,
|
@@ -38,7 +41,7 @@ def get_schedules():
|
|
38 |
|
39 |
schedule_service = ScheduleService()
|
40 |
schedules = schedule_service.get_user_schedules(user_id)
|
41 |
-
|
42 |
|
43 |
# Add CORS headers explicitly
|
44 |
response_data = jsonify({
|
@@ -50,9 +53,9 @@ def get_schedules():
|
|
50 |
return response_data, 200
|
51 |
|
52 |
except Exception as e:
|
53 |
-
|
54 |
import traceback
|
55 |
-
|
56 |
current_app.logger.error(f"Get schedules error: {str(e)}")
|
57 |
# Add CORS headers to error response
|
58 |
response_data = jsonify({
|
@@ -95,6 +98,20 @@ def create_schedule():
|
|
95 |
response_data.headers.add('Access-Control-Allow-Credentials', 'true')
|
96 |
return response_data, 400
|
97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
98 |
social_network = data['social_network']
|
99 |
schedule_time = data['schedule_time']
|
100 |
days = data['days']
|
@@ -128,12 +145,12 @@ def create_schedule():
|
|
128 |
|
129 |
# Create schedule using schedule service
|
130 |
schedule_service = ScheduleService()
|
131 |
-
result = schedule_service.create_schedule(user_id, social_network, schedule_time, days)
|
132 |
|
133 |
if result['success']:
|
134 |
# Trigger immediate APScheduler update
|
135 |
try:
|
136 |
-
|
137 |
if hasattr(current_app, 'scheduler'):
|
138 |
scheduler_updated = current_app.scheduler.trigger_immediate_update()
|
139 |
if scheduler_updated:
|
@@ -143,7 +160,7 @@ def create_schedule():
|
|
143 |
else:
|
144 |
result['message'] += ' (Note: Scheduler update will occur in 5 minutes)'
|
145 |
except Exception as e:
|
146 |
-
|
147 |
# Don't fail the schedule creation if scheduler update fails
|
148 |
result['message'] += ' (Note: Scheduler update will occur in 5 minutes)'
|
149 |
|
@@ -227,7 +244,7 @@ def delete_schedule(schedule_id):
|
|
227 |
if result['success']:
|
228 |
# Trigger immediate APScheduler update
|
229 |
try:
|
230 |
-
|
231 |
if hasattr(current_app, 'scheduler'):
|
232 |
scheduler_updated = current_app.scheduler.trigger_immediate_update()
|
233 |
if scheduler_updated:
|
@@ -237,7 +254,7 @@ def delete_schedule(schedule_id):
|
|
237 |
else:
|
238 |
result['message'] += ' (Note: Scheduler update will occur in 5 minutes)'
|
239 |
except Exception as e:
|
240 |
-
|
241 |
# Don't fail the schedule deletion if scheduler update fails
|
242 |
result['message'] += ' (Note: Scheduler update will occur in 5 minutes)'
|
243 |
|
|
|
1 |
from flask import Blueprint, request, jsonify, current_app
|
2 |
from flask_jwt_extended import jwt_required, get_jwt_identity
|
3 |
from backend.services.schedule_service import ScheduleService
|
4 |
+
from backend.utils.timezone_utils import validate_timezone, get_server_timezone, format_timezone_schedule
|
5 |
+
import logging
|
6 |
|
7 |
schedules_bp = Blueprint('schedules', __name__)
|
8 |
+
logger = logging.getLogger(__name__)
|
9 |
|
10 |
@schedules_bp.route('/', methods=['OPTIONS'])
|
11 |
@schedules_bp.route('', methods=['OPTIONS'])
|
|
|
25 |
"""
|
26 |
try:
|
27 |
user_id = get_jwt_identity()
|
28 |
+
logger.info(f"π Fetching schedules for user: {user_id}")
|
29 |
|
30 |
# Check if Supabase client is initialized
|
31 |
if not hasattr(current_app, 'supabase') or current_app.supabase is None:
|
32 |
+
logger.error("β Supabase client not initialized")
|
33 |
# Add CORS headers to error response
|
34 |
response_data = jsonify({
|
35 |
'success': False,
|
|
|
41 |
|
42 |
schedule_service = ScheduleService()
|
43 |
schedules = schedule_service.get_user_schedules(user_id)
|
44 |
+
logger.info(f"β
Found {len(schedules)} schedules for user {user_id}")
|
45 |
|
46 |
# Add CORS headers explicitly
|
47 |
response_data = jsonify({
|
|
|
53 |
return response_data, 200
|
54 |
|
55 |
except Exception as e:
|
56 |
+
logger.error(f"β Get schedules error: {str(e)}")
|
57 |
import traceback
|
58 |
+
logger.error(f"Full traceback: {traceback.format_exc()}")
|
59 |
current_app.logger.error(f"Get schedules error: {str(e)}")
|
60 |
# Add CORS headers to error response
|
61 |
response_data = jsonify({
|
|
|
98 |
response_data.headers.add('Access-Control-Allow-Credentials', 'true')
|
99 |
return response_data, 400
|
100 |
|
101 |
+
# Get timezone information (optional, will use server timezone if not provided)
|
102 |
+
user_timezone = data.get('timezone', get_server_timezone())
|
103 |
+
|
104 |
+
# Validate timezone if provided
|
105 |
+
if user_timezone and not validate_timezone(user_timezone):
|
106 |
+
# Add CORS headers to error response
|
107 |
+
response_data = jsonify({
|
108 |
+
'success': False,
|
109 |
+
'message': f'Invalid timezone: {user_timezone}'
|
110 |
+
})
|
111 |
+
response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
|
112 |
+
response_data.headers.add('Access-Control-Allow-Credentials', 'true')
|
113 |
+
return response_data, 400
|
114 |
+
|
115 |
social_network = data['social_network']
|
116 |
schedule_time = data['schedule_time']
|
117 |
days = data['days']
|
|
|
145 |
|
146 |
# Create schedule using schedule service
|
147 |
schedule_service = ScheduleService()
|
148 |
+
result = schedule_service.create_schedule(user_id, social_network, schedule_time, days, user_timezone)
|
149 |
|
150 |
if result['success']:
|
151 |
# Trigger immediate APScheduler update
|
152 |
try:
|
153 |
+
logger.info("π Triggering immediate APScheduler update...")
|
154 |
if hasattr(current_app, 'scheduler'):
|
155 |
scheduler_updated = current_app.scheduler.trigger_immediate_update()
|
156 |
if scheduler_updated:
|
|
|
160 |
else:
|
161 |
result['message'] += ' (Note: Scheduler update will occur in 5 minutes)'
|
162 |
except Exception as e:
|
163 |
+
logger.warning(f"β οΈ Failed to trigger immediate scheduler update: {str(e)}")
|
164 |
# Don't fail the schedule creation if scheduler update fails
|
165 |
result['message'] += ' (Note: Scheduler update will occur in 5 minutes)'
|
166 |
|
|
|
244 |
if result['success']:
|
245 |
# Trigger immediate APScheduler update
|
246 |
try:
|
247 |
+
logger.info("π Triggering immediate APScheduler update after deletion...")
|
248 |
if hasattr(current_app, 'scheduler'):
|
249 |
scheduler_updated = current_app.scheduler.trigger_immediate_update()
|
250 |
if scheduler_updated:
|
|
|
254 |
else:
|
255 |
result['message'] += ' (Note: Scheduler update will occur in 5 minutes)'
|
256 |
except Exception as e:
|
257 |
+
logger.warning(f"β οΈ Failed to trigger immediate scheduler update: {str(e)}")
|
258 |
# Don't fail the schedule deletion if scheduler update fails
|
259 |
result['message'] += ' (Note: Scheduler update will occur in 5 minutes)'
|
260 |
|
backend/app.py
CHANGED
@@ -9,12 +9,12 @@ from flask_jwt_extended import JWTManager
|
|
9 |
import uuid
|
10 |
from concurrent.futures import ThreadPoolExecutor
|
11 |
|
12 |
-
# Configure logging for APScheduler
|
13 |
logging.basicConfig(
|
14 |
-
level=logging.
|
15 |
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
16 |
)
|
17 |
-
logging.getLogger('apscheduler').setLevel(logging.
|
18 |
|
19 |
# Use relative import for the Config class to work with Hugging Face Spaces
|
20 |
from backend.config import Config
|
|
|
9 |
import uuid
|
10 |
from concurrent.futures import ThreadPoolExecutor
|
11 |
|
12 |
+
# Configure logging for APScheduler - only show essential logs
|
13 |
logging.basicConfig(
|
14 |
+
level=logging.INFO,
|
15 |
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
16 |
)
|
17 |
+
logging.getLogger('apscheduler').setLevel(logging.WARNING)
|
18 |
|
19 |
# Use relative import for the Config class to work with Hugging Face Spaces
|
20 |
from backend.config import Config
|
backend/scheduler/apscheduler_service.py
CHANGED
@@ -10,6 +10,12 @@ from backend.services.content_service import ContentService
|
|
10 |
from backend.services.linkedin_service import LinkedInService
|
11 |
from backend.utils.database import init_supabase
|
12 |
from backend.config import Config
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
|
14 |
# Configure logging
|
15 |
logger = logging.getLogger(__name__)
|
@@ -31,15 +37,13 @@ class APSchedulerService:
|
|
31 |
try:
|
32 |
self.app = app
|
33 |
|
34 |
-
logger.info("
|
35 |
|
36 |
# Initialize Supabase client
|
37 |
-
logger.info("Initializing Supabase client...")
|
38 |
self.supabase_client = init_supabase(
|
39 |
app.config['SUPABASE_URL'],
|
40 |
app.config['SUPABASE_KEY']
|
41 |
)
|
42 |
-
logger.info("Supabase client initialized")
|
43 |
|
44 |
# Configure job stores and executors
|
45 |
jobstores = {
|
@@ -56,26 +60,21 @@ class APSchedulerService:
|
|
56 |
}
|
57 |
|
58 |
# Create scheduler
|
59 |
-
logger.info("Creating BackgroundScheduler...")
|
60 |
self.scheduler = BackgroundScheduler(
|
61 |
jobstores=jobstores,
|
62 |
executors=executors,
|
63 |
job_defaults=job_defaults,
|
64 |
timezone='UTC'
|
65 |
)
|
66 |
-
logger.info("BackgroundScheduler created")
|
67 |
|
68 |
# Add the scheduler to the app
|
69 |
app.scheduler = self
|
70 |
-
logger.info("Scheduler added to app")
|
71 |
|
72 |
# Start the scheduler
|
73 |
-
logger.info("Starting scheduler...")
|
74 |
self.scheduler.start()
|
75 |
-
logger.info("
|
76 |
|
77 |
# Add the periodic job to load schedules from database
|
78 |
-
logger.info("Adding periodic job to load schedules...")
|
79 |
self.scheduler.add_job(
|
80 |
func=self.load_schedules,
|
81 |
trigger=CronTrigger(minute='*/5'), # Every 5 minutes
|
@@ -83,26 +82,20 @@ class APSchedulerService:
|
|
83 |
name='Load schedules from database',
|
84 |
replace_existing=True
|
85 |
)
|
86 |
-
logger.info("Periodic job added")
|
87 |
|
88 |
# Load schedules immediately when the app starts
|
89 |
-
logger.info("Loading schedules immediately...")
|
90 |
self.load_schedules()
|
91 |
-
logger.info("Schedules loaded immediately")
|
92 |
|
93 |
-
logger.info("APS Scheduler initialized, started, and schedules loaded")
|
94 |
except Exception as e:
|
95 |
-
logger.error(f"
|
96 |
import traceback
|
97 |
logger.error(traceback.format_exc())
|
98 |
|
99 |
def load_schedules(self):
|
100 |
"""Load schedules from the database and create jobs."""
|
101 |
try:
|
102 |
-
logger.info("Loading schedules from database...")
|
103 |
-
|
104 |
if not self.supabase_client:
|
105 |
-
logger.error("Supabase client not initialized")
|
106 |
return
|
107 |
|
108 |
# Fetch all schedules from Supabase
|
@@ -114,7 +107,7 @@ class APSchedulerService:
|
|
114 |
)
|
115 |
|
116 |
schedules = response.data if response.data else []
|
117 |
-
logger.info(f"Found {len(schedules)} schedules in database")
|
118 |
|
119 |
# Remove existing scheduled jobs (except the loader job)
|
120 |
jobs_to_remove = []
|
@@ -125,7 +118,6 @@ class APSchedulerService:
|
|
125 |
for job_id in jobs_to_remove:
|
126 |
try:
|
127 |
self.scheduler.remove_job(job_id)
|
128 |
-
logger.info(f"Removed job: {job_id}")
|
129 |
except Exception as e:
|
130 |
logger.warning(f"Failed to remove job {job_id}: {str(e)}")
|
131 |
|
@@ -137,12 +129,26 @@ class APSchedulerService:
|
|
137 |
adjusted_time = schedule.get('adjusted_time')
|
138 |
|
139 |
if not schedule_time or not adjusted_time:
|
140 |
-
logger.warning(f"Invalid schedule format for schedule {schedule_id}")
|
141 |
continue
|
142 |
|
143 |
-
# Parse
|
144 |
-
|
145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
146 |
|
147 |
# Create content generation job (5 minutes before publishing)
|
148 |
gen_job_id = f"gen_{schedule_id}"
|
@@ -158,7 +164,6 @@ class APSchedulerService:
|
|
158 |
args=[schedule.get('Social_network', {}).get('id_utilisateur'), schedule_id],
|
159 |
replace_existing=True
|
160 |
)
|
161 |
-
logger.info(f"Created content generation job: {gen_job_id}")
|
162 |
|
163 |
# Create publishing job
|
164 |
pub_job_id = f"pub_{schedule_id}"
|
@@ -174,15 +179,14 @@ class APSchedulerService:
|
|
174 |
args=[schedule_id],
|
175 |
replace_existing=True
|
176 |
)
|
177 |
-
|
|
|
178 |
|
179 |
except Exception as e:
|
180 |
-
logger.error(f"Error creating jobs for schedule {schedule.get('id')}: {str(e)}")
|
181 |
-
|
182 |
-
logger.info("Updated APScheduler schedule")
|
183 |
|
184 |
except Exception as e:
|
185 |
-
logger.error(f"Error loading schedules: {str(e)}")
|
186 |
|
187 |
def _parse_schedule_time(self, schedule_time):
|
188 |
"""
|
@@ -233,7 +237,7 @@ class APSchedulerService:
|
|
233 |
schedule_id (str): Schedule ID
|
234 |
"""
|
235 |
try:
|
236 |
-
logger.info(f"
|
237 |
|
238 |
# Initialize content service
|
239 |
content_service = ContentService()
|
@@ -270,12 +274,12 @@ class APSchedulerService:
|
|
270 |
)
|
271 |
|
272 |
if response.data:
|
273 |
-
logger.info(f"Content generated and stored for schedule {schedule_id}")
|
274 |
else:
|
275 |
-
logger.error(f"Failed to store generated content for schedule {schedule_id}")
|
276 |
|
277 |
except Exception as e:
|
278 |
-
logger.error(f"Error in content generation task for schedule {schedule_id}: {str(e)}")
|
279 |
|
280 |
def publish_post_task(self, schedule_id: str):
|
281 |
"""
|
@@ -285,7 +289,7 @@ class APSchedulerService:
|
|
285 |
schedule_id (str): Schedule ID
|
286 |
"""
|
287 |
try:
|
288 |
-
logger.info(f"
|
289 |
|
290 |
# Fetch the post to publish
|
291 |
response = (
|
@@ -300,7 +304,7 @@ class APSchedulerService:
|
|
300 |
)
|
301 |
|
302 |
if not response.data:
|
303 |
-
logger.info(f"No unpublished posts found for schedule {schedule_id}")
|
304 |
return
|
305 |
|
306 |
post = response.data[0]
|
@@ -325,7 +329,7 @@ class APSchedulerService:
|
|
325 |
user_sub = social_network.get('sub')
|
326 |
|
327 |
if not access_token or not user_sub:
|
328 |
-
logger.error(f"Missing social network credentials for schedule {schedule_id}")
|
329 |
return
|
330 |
|
331 |
# Publish to LinkedIn
|
@@ -343,24 +347,23 @@ class APSchedulerService:
|
|
343 |
.execute()
|
344 |
)
|
345 |
|
346 |
-
logger.info(f"Post published successfully for schedule {schedule_id}")
|
347 |
|
348 |
except Exception as e:
|
349 |
-
logger.error(f"Error in publishing task for schedule {schedule_id}: {str(e)}")
|
350 |
|
351 |
def trigger_immediate_update(self):
|
352 |
"""Trigger immediate schedule update."""
|
353 |
try:
|
354 |
-
logger.info("Triggering immediate schedule update...")
|
355 |
self.load_schedules()
|
356 |
-
logger.info("Immediate schedule update completed")
|
357 |
return True
|
358 |
except Exception as e:
|
359 |
-
logger.error(f"Error triggering immediate schedule update: {str(e)}")
|
360 |
return False
|
361 |
|
362 |
def shutdown(self):
|
363 |
"""Shutdown the scheduler."""
|
364 |
if self.scheduler:
|
365 |
self.scheduler.shutdown()
|
366 |
-
logger.info("APS Scheduler shutdown")
|
|
|
10 |
from backend.services.linkedin_service import LinkedInService
|
11 |
from backend.utils.database import init_supabase
|
12 |
from backend.config import Config
|
13 |
+
from backend.utils.timezone_utils import (
|
14 |
+
parse_timezone_schedule,
|
15 |
+
get_server_timezone,
|
16 |
+
convert_time_to_timezone,
|
17 |
+
validate_timezone
|
18 |
+
)
|
19 |
|
20 |
# Configure logging
|
21 |
logger = logging.getLogger(__name__)
|
|
|
37 |
try:
|
38 |
self.app = app
|
39 |
|
40 |
+
logger.info("π APScheduler starting...")
|
41 |
|
42 |
# Initialize Supabase client
|
|
|
43 |
self.supabase_client = init_supabase(
|
44 |
app.config['SUPABASE_URL'],
|
45 |
app.config['SUPABASE_KEY']
|
46 |
)
|
|
|
47 |
|
48 |
# Configure job stores and executors
|
49 |
jobstores = {
|
|
|
60 |
}
|
61 |
|
62 |
# Create scheduler
|
|
|
63 |
self.scheduler = BackgroundScheduler(
|
64 |
jobstores=jobstores,
|
65 |
executors=executors,
|
66 |
job_defaults=job_defaults,
|
67 |
timezone='UTC'
|
68 |
)
|
|
|
69 |
|
70 |
# Add the scheduler to the app
|
71 |
app.scheduler = self
|
|
|
72 |
|
73 |
# Start the scheduler
|
|
|
74 |
self.scheduler.start()
|
75 |
+
logger.info("β
APScheduler started successfully")
|
76 |
|
77 |
# Add the periodic job to load schedules from database
|
|
|
78 |
self.scheduler.add_job(
|
79 |
func=self.load_schedules,
|
80 |
trigger=CronTrigger(minute='*/5'), # Every 5 minutes
|
|
|
82 |
name='Load schedules from database',
|
83 |
replace_existing=True
|
84 |
)
|
|
|
85 |
|
86 |
# Load schedules immediately when the app starts
|
|
|
87 |
self.load_schedules()
|
|
|
88 |
|
|
|
89 |
except Exception as e:
|
90 |
+
logger.error(f"β APScheduler initialization failed: {str(e)}")
|
91 |
import traceback
|
92 |
logger.error(traceback.format_exc())
|
93 |
|
94 |
def load_schedules(self):
|
95 |
"""Load schedules from the database and create jobs."""
|
96 |
try:
|
|
|
|
|
97 |
if not self.supabase_client:
|
98 |
+
logger.error("β Supabase client not initialized")
|
99 |
return
|
100 |
|
101 |
# Fetch all schedules from Supabase
|
|
|
107 |
)
|
108 |
|
109 |
schedules = response.data if response.data else []
|
110 |
+
logger.info(f"π Found {len(schedules)} schedules in database")
|
111 |
|
112 |
# Remove existing scheduled jobs (except the loader job)
|
113 |
jobs_to_remove = []
|
|
|
118 |
for job_id in jobs_to_remove:
|
119 |
try:
|
120 |
self.scheduler.remove_job(job_id)
|
|
|
121 |
except Exception as e:
|
122 |
logger.warning(f"Failed to remove job {job_id}: {str(e)}")
|
123 |
|
|
|
129 |
adjusted_time = schedule.get('adjusted_time')
|
130 |
|
131 |
if not schedule_time or not adjusted_time:
|
132 |
+
logger.warning(f"β οΈ Invalid schedule format for schedule {schedule_id}")
|
133 |
continue
|
134 |
|
135 |
+
# Parse timezone information
|
136 |
+
server_timezone = get_server_timezone()
|
137 |
+
schedule_time_part, schedule_timezone = parse_timezone_schedule(schedule_time)
|
138 |
+
adjusted_time_part, adjusted_timezone = parse_timezone_schedule(adjusted_time)
|
139 |
+
|
140 |
+
# Convert to server timezone for APScheduler
|
141 |
+
if schedule_timezone and validate_timezone(schedule_timezone):
|
142 |
+
server_schedule_time = convert_time_to_timezone(schedule_time_part, schedule_timezone, server_timezone)
|
143 |
+
server_adjusted_time = convert_time_to_timezone(adjusted_time_part, adjusted_timezone or schedule_timezone, server_timezone)
|
144 |
+
else:
|
145 |
+
# Use original time if no valid timezone
|
146 |
+
server_schedule_time = schedule_time_part
|
147 |
+
server_adjusted_time = adjusted_time_part
|
148 |
+
|
149 |
+
# Parse schedule times for server timezone
|
150 |
+
content_gen_cron = self._parse_schedule_time(server_adjusted_time)
|
151 |
+
publish_cron = self._parse_schedule_time(server_schedule_time)
|
152 |
|
153 |
# Create content generation job (5 minutes before publishing)
|
154 |
gen_job_id = f"gen_{schedule_id}"
|
|
|
164 |
args=[schedule.get('Social_network', {}).get('id_utilisateur'), schedule_id],
|
165 |
replace_existing=True
|
166 |
)
|
|
|
167 |
|
168 |
# Create publishing job
|
169 |
pub_job_id = f"pub_{schedule_id}"
|
|
|
179 |
args=[schedule_id],
|
180 |
replace_existing=True
|
181 |
)
|
182 |
+
|
183 |
+
logger.info(f"π
Created schedule jobs for {schedule_id}")
|
184 |
|
185 |
except Exception as e:
|
186 |
+
logger.error(f"β Error creating jobs for schedule {schedule.get('id')}: {str(e)}")
|
|
|
|
|
187 |
|
188 |
except Exception as e:
|
189 |
+
logger.error(f"β Error loading schedules: {str(e)}")
|
190 |
|
191 |
def _parse_schedule_time(self, schedule_time):
|
192 |
"""
|
|
|
237 |
schedule_id (str): Schedule ID
|
238 |
"""
|
239 |
try:
|
240 |
+
logger.info(f"π¨ Generating content for schedule {schedule_id}")
|
241 |
|
242 |
# Initialize content service
|
243 |
content_service = ContentService()
|
|
|
274 |
)
|
275 |
|
276 |
if response.data:
|
277 |
+
logger.info(f"β
Content generated and stored for schedule {schedule_id}")
|
278 |
else:
|
279 |
+
logger.error(f"β Failed to store generated content for schedule {schedule_id}")
|
280 |
|
281 |
except Exception as e:
|
282 |
+
logger.error(f"β Error in content generation task for schedule {schedule_id}: {str(e)}")
|
283 |
|
284 |
def publish_post_task(self, schedule_id: str):
|
285 |
"""
|
|
|
289 |
schedule_id (str): Schedule ID
|
290 |
"""
|
291 |
try:
|
292 |
+
logger.info(f"π Publishing post for schedule {schedule_id}")
|
293 |
|
294 |
# Fetch the post to publish
|
295 |
response = (
|
|
|
304 |
)
|
305 |
|
306 |
if not response.data:
|
307 |
+
logger.info(f"π No unpublished posts found for schedule {schedule_id}")
|
308 |
return
|
309 |
|
310 |
post = response.data[0]
|
|
|
329 |
user_sub = social_network.get('sub')
|
330 |
|
331 |
if not access_token or not user_sub:
|
332 |
+
logger.error(f"β Missing social network credentials for schedule {schedule_id}")
|
333 |
return
|
334 |
|
335 |
# Publish to LinkedIn
|
|
|
347 |
.execute()
|
348 |
)
|
349 |
|
350 |
+
logger.info(f"β
Post published successfully for schedule {schedule_id}")
|
351 |
|
352 |
except Exception as e:
|
353 |
+
logger.error(f"β Error in publishing task for schedule {schedule_id}: {str(e)}")
|
354 |
|
355 |
def trigger_immediate_update(self):
|
356 |
"""Trigger immediate schedule update."""
|
357 |
try:
|
358 |
+
logger.info("π Triggering immediate schedule update...")
|
359 |
self.load_schedules()
|
|
|
360 |
return True
|
361 |
except Exception as e:
|
362 |
+
logger.error(f"β Error triggering immediate schedule update: {str(e)}")
|
363 |
return False
|
364 |
|
365 |
def shutdown(self):
|
366 |
"""Shutdown the scheduler."""
|
367 |
if self.scheduler:
|
368 |
self.scheduler.shutdown()
|
369 |
+
logger.info("π APS Scheduler shutdown")
|
backend/services/schedule_service.py
CHANGED
@@ -3,6 +3,13 @@ from datetime import datetime, timedelta
|
|
3 |
from typing import List, Dict
|
4 |
import pandas as pd
|
5 |
from backend.models.schedule import Schedule
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
|
7 |
class ScheduleService:
|
8 |
"""Service for managing post scheduling."""
|
@@ -49,7 +56,7 @@ class ScheduleService:
|
|
49 |
# Return empty list instead of raising exception
|
50 |
return []
|
51 |
|
52 |
-
def create_schedule(self, user_id: str, social_network: str, schedule_time: str, days: List[str]) -> Dict:
|
53 |
"""
|
54 |
Create a new schedule.
|
55 |
|
@@ -58,6 +65,7 @@ class ScheduleService:
|
|
58 |
social_network (str): Social account ID (as string)
|
59 |
schedule_time (str): Schedule time in format "HH:MM"
|
60 |
days (List[str]): List of days to schedule
|
|
|
61 |
|
62 |
Returns:
|
63 |
Dict: Created schedule
|
@@ -96,11 +104,15 @@ class ScheduleService:
|
|
96 |
created_schedules = []
|
97 |
|
98 |
for day in days:
|
99 |
-
# Format schedule time
|
100 |
formatted_schedule = f"{day} {schedule_time}"
|
101 |
|
|
|
|
|
|
|
|
|
102 |
# Calculate adjusted time (5 minutes before for content generation)
|
103 |
-
adjusted_time =
|
104 |
|
105 |
# Insert schedule
|
106 |
response = (
|
@@ -169,6 +181,7 @@ class ScheduleService:
|
|
169 |
def _calculate_adjusted_time(self, schedule_time: str) -> str:
|
170 |
"""
|
171 |
Calculate adjusted time for content generation (5 minutes before schedule).
|
|
|
172 |
|
173 |
Args:
|
174 |
schedule_time (str): Original schedule time
|
|
|
3 |
from typing import List, Dict
|
4 |
import pandas as pd
|
5 |
from backend.models.schedule import Schedule
|
6 |
+
from backend.utils.timezone_utils import (
|
7 |
+
validate_timezone,
|
8 |
+
format_timezone_schedule,
|
9 |
+
calculate_adjusted_time_with_timezone,
|
10 |
+
get_server_timezone,
|
11 |
+
parse_timezone_schedule
|
12 |
+
)
|
13 |
|
14 |
class ScheduleService:
|
15 |
"""Service for managing post scheduling."""
|
|
|
56 |
# Return empty list instead of raising exception
|
57 |
return []
|
58 |
|
59 |
+
def create_schedule(self, user_id: str, social_network: str, schedule_time: str, days: List[str], timezone: str = None) -> Dict:
|
60 |
"""
|
61 |
Create a new schedule.
|
62 |
|
|
|
65 |
social_network (str): Social account ID (as string)
|
66 |
schedule_time (str): Schedule time in format "HH:MM"
|
67 |
days (List[str]): List of days to schedule
|
68 |
+
timezone (str): User's timezone (optional)
|
69 |
|
70 |
Returns:
|
71 |
Dict: Created schedule
|
|
|
104 |
created_schedules = []
|
105 |
|
106 |
for day in days:
|
107 |
+
# Format schedule time with timezone
|
108 |
formatted_schedule = f"{day} {schedule_time}"
|
109 |
|
110 |
+
# Add timezone if provided
|
111 |
+
if timezone:
|
112 |
+
formatted_schedule = format_timezone_schedule(formatted_schedule, timezone)
|
113 |
+
|
114 |
# Calculate adjusted time (5 minutes before for content generation)
|
115 |
+
adjusted_time = calculate_adjusted_time_with_timezone(formatted_schedule, timezone)
|
116 |
|
117 |
# Insert schedule
|
118 |
response = (
|
|
|
181 |
def _calculate_adjusted_time(self, schedule_time: str) -> str:
|
182 |
"""
|
183 |
Calculate adjusted time for content generation (5 minutes before schedule).
|
184 |
+
Legacy method - kept for backward compatibility.
|
185 |
|
186 |
Args:
|
187 |
schedule_time (str): Original schedule time
|
backend/utils/timezone_utils.py
ADDED
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Timezone utility functions for the scheduling system."""
|
2 |
+
|
3 |
+
import logging
|
4 |
+
from datetime import datetime, time, timedelta
|
5 |
+
from typing import Optional, Tuple, Dict, Any
|
6 |
+
import pytz
|
7 |
+
from zoneinfo import ZoneInfo
|
8 |
+
from zoneinfo._common import ZoneInfoNotFoundError
|
9 |
+
|
10 |
+
logger = logging.getLogger(__name__)
|
11 |
+
|
12 |
+
# List of common IANA timezone names for validation
|
13 |
+
COMMON_TIMEZONES = [
|
14 |
+
'UTC',
|
15 |
+
'America/New_York', 'America/Chicago', 'America/Denver', 'America/Los_Angeles',
|
16 |
+
'Europe/London', 'Europe/Paris', 'Europe/Berlin', 'Europe/Rome',
|
17 |
+
'Asia/Tokyo', 'Asia/Shanghai', 'Asia/Dubai', 'Asia/Kolkata',
|
18 |
+
'Australia/Sydney', 'Pacific/Auckland',
|
19 |
+
'Africa/Porto-Novo', 'Africa/Cairo', 'Africa/Johannesburg',
|
20 |
+
'America/Toronto', 'America/Mexico_City', 'America/Sao_Paulo',
|
21 |
+
'Europe/Madrid', 'Europe/Amsterdam', 'Europe/Stockholm',
|
22 |
+
'Asia/Seoul', 'Asia/Singapore', 'Asia/Hong_Kong',
|
23 |
+
'Pacific/Honolulu'
|
24 |
+
]
|
25 |
+
|
26 |
+
def validate_timezone(timezone_str: str) -> bool:
|
27 |
+
"""
|
28 |
+
Validate if a timezone string is valid.
|
29 |
+
|
30 |
+
Args:
|
31 |
+
timezone_str (str): Timezone string to validate
|
32 |
+
|
33 |
+
Returns:
|
34 |
+
bool: True if valid, False otherwise
|
35 |
+
"""
|
36 |
+
try:
|
37 |
+
ZoneInfo(timezone_str)
|
38 |
+
return True
|
39 |
+
except (pytz.UnknownTimeZoneError, ValueError, ZoneInfoNotFoundError):
|
40 |
+
return False
|
41 |
+
|
42 |
+
def get_user_timezone() -> str:
|
43 |
+
"""
|
44 |
+
Get the system's local timezone.
|
45 |
+
|
46 |
+
Returns:
|
47 |
+
str: System timezone string
|
48 |
+
"""
|
49 |
+
try:
|
50 |
+
# Get system timezone
|
51 |
+
import tzlocal
|
52 |
+
return str(tzlocal.get_localzone())
|
53 |
+
except Exception:
|
54 |
+
# Fallback to UTC
|
55 |
+
return 'UTC'
|
56 |
+
|
57 |
+
def parse_timezone_schedule(schedule_time: str) -> Tuple[str, Optional[str]]:
|
58 |
+
"""
|
59 |
+
Parse a timezone-prefixed schedule time string.
|
60 |
+
|
61 |
+
Args:
|
62 |
+
schedule_time (str): Schedule time in format "Day HH:MM::::timezone" or "Day HH:MM"
|
63 |
+
|
64 |
+
Returns:
|
65 |
+
Tuple[str, Optional[str]]: (time_part, timezone_part)
|
66 |
+
"""
|
67 |
+
if "::::" in schedule_time:
|
68 |
+
time_part, timezone_part = schedule_time.split("::::", 1)
|
69 |
+
return time_part.strip(), timezone_part.strip()
|
70 |
+
else:
|
71 |
+
# Legacy format without timezone
|
72 |
+
return schedule_time, None
|
73 |
+
|
74 |
+
def format_timezone_schedule(time_part: str, timezone: Optional[str]) -> str:
|
75 |
+
"""
|
76 |
+
Format a schedule time with timezone prefix.
|
77 |
+
|
78 |
+
Args:
|
79 |
+
time_part (str): Time part in format "Day HH:MM"
|
80 |
+
timezone (Optional[str]): Timezone string or None
|
81 |
+
|
82 |
+
Returns:
|
83 |
+
str: Formatted schedule time with timezone
|
84 |
+
"""
|
85 |
+
if timezone:
|
86 |
+
return f"{time_part}::::{timezone}"
|
87 |
+
else:
|
88 |
+
return time_part
|
89 |
+
|
90 |
+
def convert_time_to_timezone(time_str: str, from_timezone: str, to_timezone: str) -> str:
|
91 |
+
"""
|
92 |
+
Convert a time string from one timezone to another.
|
93 |
+
|
94 |
+
Args:
|
95 |
+
time_str (str): Time string in format "Day HH:MM"
|
96 |
+
from_timezone (str): Source timezone
|
97 |
+
to_timezone (str): Target timezone
|
98 |
+
|
99 |
+
Returns:
|
100 |
+
str: Converted time string in format "Day HH:MM"
|
101 |
+
"""
|
102 |
+
try:
|
103 |
+
# Parse the time string
|
104 |
+
day_name, time_part = time_str.split()
|
105 |
+
hour, minute = map(int, time_part.split(':'))
|
106 |
+
|
107 |
+
# Create timezone objects
|
108 |
+
from_tz = ZoneInfo(from_timezone)
|
109 |
+
to_tz = ZoneInfo(to_timezone)
|
110 |
+
|
111 |
+
# Get current date to handle timezone conversion
|
112 |
+
now = datetime.now(from_tz)
|
113 |
+
|
114 |
+
# Create datetime with the specified time
|
115 |
+
scheduled_datetime = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
116 |
+
|
117 |
+
# Convert to target timezone
|
118 |
+
target_datetime = scheduled_datetime.astimezone(to_tz)
|
119 |
+
|
120 |
+
# Return in the same format
|
121 |
+
return f"{day_name} {target_datetime.hour:02d}:{target_datetime.minute:02d}"
|
122 |
+
|
123 |
+
except Exception as e:
|
124 |
+
logger.error(f"Error converting time {time_str} from {from_timezone} to {to_timezone}: {str(e)}")
|
125 |
+
# Return original time as fallback
|
126 |
+
return time_str
|
127 |
+
|
128 |
+
def get_server_timezone() -> str:
|
129 |
+
"""
|
130 |
+
Get the server's timezone.
|
131 |
+
|
132 |
+
Returns:
|
133 |
+
str: Server timezone string
|
134 |
+
"""
|
135 |
+
try:
|
136 |
+
# Try to get from environment first
|
137 |
+
import os
|
138 |
+
server_tz = os.environ.get('TZ', 'UTC')
|
139 |
+
if validate_timezone(server_tz):
|
140 |
+
return server_tz
|
141 |
+
|
142 |
+
# Fallback to system timezone
|
143 |
+
return get_user_timezone()
|
144 |
+
except Exception:
|
145 |
+
return 'UTC'
|
146 |
+
|
147 |
+
def calculate_adjusted_time_with_timezone(schedule_time: str, timezone: str) -> str:
|
148 |
+
"""
|
149 |
+
Calculate adjusted time for content generation (5 minutes before schedule) with timezone support.
|
150 |
+
|
151 |
+
Args:
|
152 |
+
schedule_time (str): Schedule time in format "Day HH:MM::::timezone" or "Day HH:MM"
|
153 |
+
timezone (str): Timezone string
|
154 |
+
|
155 |
+
Returns:
|
156 |
+
str: Adjusted time with timezone
|
157 |
+
"""
|
158 |
+
try:
|
159 |
+
# Parse the schedule time
|
160 |
+
time_part, schedule_timezone = parse_timezone_schedule(schedule_time)
|
161 |
+
|
162 |
+
# Use provided timezone or fallback to the one from schedule_time
|
163 |
+
effective_timezone = schedule_timezone or timezone
|
164 |
+
|
165 |
+
if not effective_timezone:
|
166 |
+
effective_timezone = get_server_timezone()
|
167 |
+
|
168 |
+
# Parse the time part
|
169 |
+
day, time_str = time_part.split()
|
170 |
+
hour, minute = map(int, time_str.split(':'))
|
171 |
+
|
172 |
+
# Create timezone object
|
173 |
+
tz = ZoneInfo(effective_timezone)
|
174 |
+
|
175 |
+
# Get current date in the timezone
|
176 |
+
now = datetime.now(tz)
|
177 |
+
|
178 |
+
# Create scheduled datetime
|
179 |
+
scheduled_datetime = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
180 |
+
|
181 |
+
# Subtract 5 minutes for content generation
|
182 |
+
adjusted_datetime = scheduled_datetime - timedelta(minutes=5)
|
183 |
+
|
184 |
+
# Format back to string
|
185 |
+
adjusted_time_str = f"{day} {adjusted_datetime.hour:02d}:{adjusted_datetime.minute:02d}"
|
186 |
+
|
187 |
+
# Return with timezone only if effective_timezone is not None
|
188 |
+
if effective_timezone:
|
189 |
+
return format_timezone_schedule(adjusted_time_str, effective_timezone)
|
190 |
+
else:
|
191 |
+
return adjusted_time_str
|
192 |
+
|
193 |
+
except Exception as e:
|
194 |
+
logger.error(f"Error calculating adjusted time for {schedule_time}: {str(e)}")
|
195 |
+
# Return original time as fallback
|
196 |
+
return schedule_time
|
frontend/src/pages/Schedule.jsx
CHANGED
@@ -7,6 +7,7 @@ import {
|
|
7 |
clearError
|
8 |
} from '../store/reducers/schedulesSlice';
|
9 |
import { fetchAccounts } from '../store/reducers/accountsSlice';
|
|
|
10 |
|
11 |
const Schedule = () => {
|
12 |
const dispatch = useDispatch();
|
@@ -17,6 +18,7 @@ const Schedule = () => {
|
|
17 |
const [scheduleTime, setScheduleTime] = useState('18:00');
|
18 |
const [selectedDays, setSelectedDays] = useState([]);
|
19 |
const [isCreating, setIsCreating] = useState(false);
|
|
|
20 |
|
21 |
const daysOfWeek = [
|
22 |
'Monday', 'Tuesday', 'Wednesday', 'Thursday',
|
@@ -24,6 +26,10 @@ const Schedule = () => {
|
|
24 |
];
|
25 |
|
26 |
useEffect(() => {
|
|
|
|
|
|
|
|
|
27 |
dispatch(fetchSchedules());
|
28 |
dispatch(fetchAccounts());
|
29 |
dispatch(clearError());
|
@@ -64,7 +70,8 @@ const Schedule = () => {
|
|
64 |
await dispatch(createSchedule({
|
65 |
social_network: selectedAccount, // Pass the account ID, not the social network name
|
66 |
schedule_time: scheduleTime,
|
67 |
-
days: selectedDays
|
|
|
68 |
})).unwrap();
|
69 |
|
70 |
// Reset form
|
@@ -428,7 +435,9 @@ const Schedule = () => {
|
|
428 |
</div>
|
429 |
<span className="text-xs sm:text-sm font-medium text-gray-600">Time</span>
|
430 |
</div>
|
431 |
-
<span className="time-value text-lg sm:text-2xl font-bold text-gray-900">
|
|
|
|
|
432 |
</div>
|
433 |
|
434 |
<div className="schedule-account">
|
|
|
7 |
clearError
|
8 |
} from '../store/reducers/schedulesSlice';
|
9 |
import { fetchAccounts } from '../store/reducers/accountsSlice';
|
10 |
+
import { formatScheduleForDisplay } from '../utils/timezoneUtils';
|
11 |
|
12 |
const Schedule = () => {
|
13 |
const dispatch = useDispatch();
|
|
|
18 |
const [scheduleTime, setScheduleTime] = useState('18:00');
|
19 |
const [selectedDays, setSelectedDays] = useState([]);
|
20 |
const [isCreating, setIsCreating] = useState(false);
|
21 |
+
const [userTimezone, setUserTimezone] = useState('');
|
22 |
|
23 |
const daysOfWeek = [
|
24 |
'Monday', 'Tuesday', 'Wednesday', 'Thursday',
|
|
|
26 |
];
|
27 |
|
28 |
useEffect(() => {
|
29 |
+
// Detect user timezone
|
30 |
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
31 |
+
setUserTimezone(timezone);
|
32 |
+
|
33 |
dispatch(fetchSchedules());
|
34 |
dispatch(fetchAccounts());
|
35 |
dispatch(clearError());
|
|
|
70 |
await dispatch(createSchedule({
|
71 |
social_network: selectedAccount, // Pass the account ID, not the social network name
|
72 |
schedule_time: scheduleTime,
|
73 |
+
days: selectedDays,
|
74 |
+
timezone: userTimezone
|
75 |
})).unwrap();
|
76 |
|
77 |
// Reset form
|
|
|
435 |
</div>
|
436 |
<span className="text-xs sm:text-sm font-medium text-gray-600">Time</span>
|
437 |
</div>
|
438 |
+
<span className="time-value text-lg sm:text-2xl font-bold text-gray-900">
|
439 |
+
{formatScheduleForDisplay(schedule.schedule_time)}
|
440 |
+
</span>
|
441 |
</div>
|
442 |
|
443 |
<div className="schedule-account">
|
frontend/src/services/scheduleService.js
CHANGED
@@ -28,14 +28,19 @@ class ScheduleService {
|
|
28 |
* @param {string} scheduleData.social_network - Social account ID
|
29 |
* @param {string} scheduleData.schedule_time - Schedule time in format "HH:MM"
|
30 |
* @param {Array<string>} scheduleData.days - List of days to schedule
|
|
|
31 |
* @returns {Promise} Promise that resolves to the create schedule response
|
32 |
*/
|
33 |
async create(scheduleData) {
|
34 |
try {
|
|
|
|
|
|
|
35 |
const response = await apiClient.post('/schedules', {
|
36 |
social_network: scheduleData.social_network,
|
37 |
schedule_time: scheduleData.schedule_time,
|
38 |
-
days: scheduleData.days
|
|
|
39 |
});
|
40 |
|
41 |
if (import.meta.env.VITE_NODE_ENV === 'development') {
|
|
|
28 |
* @param {string} scheduleData.social_network - Social account ID
|
29 |
* @param {string} scheduleData.schedule_time - Schedule time in format "HH:MM"
|
30 |
* @param {Array<string>} scheduleData.days - List of days to schedule
|
31 |
+
* @param {string} scheduleData.timezone - User's timezone (optional)
|
32 |
* @returns {Promise} Promise that resolves to the create schedule response
|
33 |
*/
|
34 |
async create(scheduleData) {
|
35 |
try {
|
36 |
+
// Get user timezone if not provided
|
37 |
+
const timezone = scheduleData.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
38 |
+
|
39 |
const response = await apiClient.post('/schedules', {
|
40 |
social_network: scheduleData.social_network,
|
41 |
schedule_time: scheduleData.schedule_time,
|
42 |
+
days: scheduleData.days,
|
43 |
+
timezone: timezone
|
44 |
});
|
45 |
|
46 |
if (import.meta.env.VITE_NODE_ENV === 'development') {
|
frontend/src/utils/timezoneUtils.js
ADDED
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* Timezone utility functions for the frontend scheduling system.
|
3 |
+
*/
|
4 |
+
|
5 |
+
/**
|
6 |
+
* Parse a timezone-prefixed schedule time string and extract the time part.
|
7 |
+
* @param {string} scheduleTime - Schedule time in format "Day HH:MM::::timezone" or "Day HH:MM"
|
8 |
+
* @returns {string} Time part without timezone
|
9 |
+
*/
|
10 |
+
export const parseScheduleTime = (scheduleTime) => {
|
11 |
+
if (!scheduleTime) return scheduleTime;
|
12 |
+
|
13 |
+
if (scheduleTime.includes('::::')) {
|
14 |
+
return scheduleTime.split('::::')[0];
|
15 |
+
}
|
16 |
+
|
17 |
+
// Legacy format without timezone
|
18 |
+
return scheduleTime;
|
19 |
+
};
|
20 |
+
|
21 |
+
/**
|
22 |
+
* Get the user's timezone from the browser.
|
23 |
+
* @returns {string} Timezone string (e.g., "America/New_York")
|
24 |
+
*/
|
25 |
+
export const getUserTimezone = () => {
|
26 |
+
try {
|
27 |
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
28 |
+
} catch (error) {
|
29 |
+
console.error('Error getting timezone:', error);
|
30 |
+
return 'UTC'; // Fallback
|
31 |
+
}
|
32 |
+
};
|
33 |
+
|
34 |
+
/**
|
35 |
+
* Format a schedule time for display (day and time only, no timezone).
|
36 |
+
* @param {string} scheduleTime - Schedule time string
|
37 |
+
* @returns {string} Formatted time for display
|
38 |
+
*/
|
39 |
+
export const formatScheduleForDisplay = (scheduleTime) => {
|
40 |
+
const parsedTime = parseScheduleTime(scheduleTime);
|
41 |
+
if (!parsedTime) return '';
|
42 |
+
|
43 |
+
// Split into day and time parts
|
44 |
+
const parts = parsedTime.trim().split(' ');
|
45 |
+
if (parts.length >= 2) {
|
46 |
+
const day = parts[0];
|
47 |
+
const time = parts[1];
|
48 |
+
return `${day} ${time}`;
|
49 |
+
}
|
50 |
+
|
51 |
+
return parsedTime;
|
52 |
+
};
|
53 |
+
|
54 |
+
/**
|
55 |
+
* Check if a schedule time has timezone information.
|
56 |
+
* @param {string} scheduleTime - Schedule time string
|
57 |
+
* @returns {boolean} True if timezone is included
|
58 |
+
*/
|
59 |
+
export const hasTimezone = (scheduleTime) => {
|
60 |
+
return scheduleTime && scheduleTime.includes('::::');
|
61 |
+
};
|
62 |
+
|
63 |
+
/**
|
64 |
+
* Extract timezone from a schedule time string.
|
65 |
+
* @param {string} scheduleTime - Schedule time string
|
66 |
+
* @returns {string|null} Timezone string or null if not present
|
67 |
+
*/
|
68 |
+
export const extractTimezone = (scheduleTime) => {
|
69 |
+
if (!scheduleTime || !scheduleTime.includes('::::')) {
|
70 |
+
return null;
|
71 |
+
}
|
72 |
+
|
73 |
+
return scheduleTime.split('::::')[1]?.trim() || null;
|
74 |
+
};
|
simple_timezone_test.py
ADDED
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
Simple test script to verify timezone functionality in the scheduling system.
|
4 |
+
"""
|
5 |
+
|
6 |
+
import sys
|
7 |
+
import os
|
8 |
+
sys.path.append(os.path.join(os.path.dirname(__file__), 'backend'))
|
9 |
+
|
10 |
+
from backend.utils.timezone_utils import (
|
11 |
+
validate_timezone,
|
12 |
+
format_timezone_schedule,
|
13 |
+
parse_timezone_schedule,
|
14 |
+
calculate_adjusted_time_with_timezone,
|
15 |
+
get_server_timezone
|
16 |
+
)
|
17 |
+
|
18 |
+
def test_timezone_validation():
|
19 |
+
"""Test timezone validation functionality."""
|
20 |
+
print("Testing timezone validation...")
|
21 |
+
|
22 |
+
# Valid timezones
|
23 |
+
valid_timezones = [
|
24 |
+
"UTC",
|
25 |
+
"America/New_York",
|
26 |
+
"Europe/London",
|
27 |
+
"Asia/Tokyo",
|
28 |
+
"Africa/Porto-Novo"
|
29 |
+
]
|
30 |
+
|
31 |
+
for tz in valid_timezones:
|
32 |
+
assert validate_timezone(tz), f"Should validate {tz}"
|
33 |
+
print(f"[OK] {tz} - Valid")
|
34 |
+
|
35 |
+
# Invalid timezones
|
36 |
+
invalid_timezones = [
|
37 |
+
"Invalid/Timezone",
|
38 |
+
"America/Bogus",
|
39 |
+
"Not/A_Timezone",
|
40 |
+
""
|
41 |
+
]
|
42 |
+
|
43 |
+
for tz in invalid_timezones:
|
44 |
+
assert not validate_timezone(tz), f"Should invalidate {tz}"
|
45 |
+
print(f"[FAIL] {tz} - Invalid (as expected)")
|
46 |
+
|
47 |
+
print("[OK] Timezone validation tests passed!\n")
|
48 |
+
|
49 |
+
def test_timezone_formatting():
|
50 |
+
"""Test timezone formatting functionality."""
|
51 |
+
print("Testing timezone formatting...")
|
52 |
+
|
53 |
+
# Test formatting with timezone
|
54 |
+
schedule_time = "Monday 14:30"
|
55 |
+
timezone = "America/New_York"
|
56 |
+
formatted = format_timezone_schedule(schedule_time, timezone)
|
57 |
+
expected = "Monday 14:30::::America/New_York"
|
58 |
+
|
59 |
+
assert formatted == expected, f"Expected '{expected}', got '{formatted}'"
|
60 |
+
print(f"[OK] Formatted: {formatted}")
|
61 |
+
|
62 |
+
# Test formatting without timezone
|
63 |
+
formatted_no_tz = format_timezone_schedule(schedule_time, None)
|
64 |
+
assert formatted_no_tz == schedule_time, f"Expected '{schedule_time}', got '{formatted_no_tz}'"
|
65 |
+
print(f"[OK] No timezone: {formatted_no_tz}")
|
66 |
+
|
67 |
+
print("[OK] Timezone formatting tests passed!\n")
|
68 |
+
|
69 |
+
def test_timezone_parsing():
|
70 |
+
"""Test timezone parsing functionality."""
|
71 |
+
print("Testing timezone parsing...")
|
72 |
+
|
73 |
+
# Test parsing with timezone
|
74 |
+
schedule_with_tz = "Monday 14:30::::America/New_York"
|
75 |
+
time_part, tz_part = parse_timezone_schedule(schedule_with_tz)
|
76 |
+
|
77 |
+
assert time_part == "Monday 14:30", f"Expected 'Monday 14:30', got '{time_part}'"
|
78 |
+
assert tz_part == "America/New_York", f"Expected 'America/New_York', got '{tz_part}'"
|
79 |
+
print(f"[OK] Parsed time: {time_part}, timezone: {tz_part}")
|
80 |
+
|
81 |
+
# Test parsing without timezone
|
82 |
+
schedule_without_tz = "Monday 14:30"
|
83 |
+
time_part_no_tz, tz_part_no_tz = parse_timezone_schedule(schedule_without_tz)
|
84 |
+
|
85 |
+
assert time_part_no_tz == "Monday 14:30", f"Expected 'Monday 14:30', got '{time_part_no_tz}'"
|
86 |
+
assert tz_part_no_tz is None, f"Expected None, got '{tz_part_no_tz}'"
|
87 |
+
print(f"[OK] Parsed time: {time_part_no_tz}, timezone: {tz_part_no_tz}")
|
88 |
+
|
89 |
+
print("[OK] Timezone parsing tests passed!\n")
|
90 |
+
|
91 |
+
def test_adjusted_time_calculation():
|
92 |
+
"""Test adjusted time calculation with timezone."""
|
93 |
+
print("Testing adjusted time calculation...")
|
94 |
+
|
95 |
+
# Test with timezone
|
96 |
+
schedule_time = "Monday 14:30::::America/New_York"
|
97 |
+
adjusted_time = calculate_adjusted_time_with_timezone(schedule_time, "America/New_York")
|
98 |
+
expected = "Monday 14:25::::America/New_York"
|
99 |
+
|
100 |
+
assert adjusted_time == expected, f"Expected '{expected}', got '{adjusted_time}'"
|
101 |
+
print(f"[OK] Adjusted with timezone: {adjusted_time}")
|
102 |
+
|
103 |
+
# Test without timezone
|
104 |
+
schedule_time_no_tz = "Monday 14:30"
|
105 |
+
adjusted_time_no_tz = calculate_adjusted_time_with_timezone(schedule_time_no_tz, None)
|
106 |
+
expected_no_tz = "Monday 14:25"
|
107 |
+
|
108 |
+
assert adjusted_time_no_tz == expected_no_tz, f"Expected '{expected_no_tz}', got '{adjusted_time_no_tz}'"
|
109 |
+
print(f"[OK] Adjusted without timezone: {adjusted_time_no_tz}")
|
110 |
+
|
111 |
+
print("[OK] Adjusted time calculation tests passed!\n")
|
112 |
+
|
113 |
+
def test_server_timezone():
|
114 |
+
"""Test server timezone detection."""
|
115 |
+
print("Testing server timezone detection...")
|
116 |
+
|
117 |
+
server_tz = get_server_timezone()
|
118 |
+
print(f"[OK] Server timezone: {server_tz}")
|
119 |
+
|
120 |
+
# Should be a valid timezone
|
121 |
+
assert validate_timezone(server_tz), f"Server timezone {server_tz} should be valid"
|
122 |
+
print("[OK] Server timezone is valid!")
|
123 |
+
|
124 |
+
print("[OK] Server timezone tests passed!\n")
|
125 |
+
|
126 |
+
def test_frontend_compatibility():
|
127 |
+
"""Test frontend compatibility with timezone data."""
|
128 |
+
print("Testing frontend compatibility...")
|
129 |
+
|
130 |
+
# Simulate data that would come from the database
|
131 |
+
schedule_data = {
|
132 |
+
"id": "123",
|
133 |
+
"schedule_time": "Monday 14:30::::America/New_York",
|
134 |
+
"adjusted_time": "Monday 14:25::::America/New_York"
|
135 |
+
}
|
136 |
+
|
137 |
+
# Test parsing like the frontend would do
|
138 |
+
display_time = schedule_data["schedule_time"].split("::::")[0]
|
139 |
+
print(f"[OK] Display time (no timezone): {display_time}")
|
140 |
+
|
141 |
+
# Test that timezone can be extracted
|
142 |
+
if "::::" in schedule_data["schedule_time"]:
|
143 |
+
timezone = schedule_data["schedule_time"].split("::::")[1]
|
144 |
+
print(f"[OK] Extracted timezone: {timezone}")
|
145 |
+
|
146 |
+
print("[OK] Frontend compatibility tests passed!\n")
|
147 |
+
|
148 |
+
def main():
|
149 |
+
"""Run all timezone tests."""
|
150 |
+
print("Starting timezone functionality tests...\n")
|
151 |
+
|
152 |
+
try:
|
153 |
+
test_timezone_validation()
|
154 |
+
test_timezone_formatting()
|
155 |
+
test_timezone_parsing()
|
156 |
+
test_adjusted_time_calculation()
|
157 |
+
test_server_timezone()
|
158 |
+
test_frontend_compatibility()
|
159 |
+
|
160 |
+
print("[OK] All timezone tests passed successfully!")
|
161 |
+
return True
|
162 |
+
|
163 |
+
except Exception as e:
|
164 |
+
print(f"[FAIL] Test failed with error: {e}")
|
165 |
+
import traceback
|
166 |
+
traceback.print_exc()
|
167 |
+
return False
|
168 |
+
|
169 |
+
if __name__ == "__main__":
|
170 |
+
success = main()
|
171 |
+
sys.exit(0 if success else 1)
|
test_timezone_functionality.py
ADDED
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
Test script to verify timezone functionality in the scheduling system.
|
4 |
+
"""
|
5 |
+
|
6 |
+
import sys
|
7 |
+
import os
|
8 |
+
sys.path.append(os.path.join(os.path.dirname(__file__), 'backend'))
|
9 |
+
|
10 |
+
from backend.utils.timezone_utils import (
|
11 |
+
validate_timezone,
|
12 |
+
format_timezone_schedule,
|
13 |
+
parse_timezone_schedule,
|
14 |
+
calculate_adjusted_time_with_timezone,
|
15 |
+
get_server_timezone,
|
16 |
+
convert_time_to_timezone
|
17 |
+
)
|
18 |
+
|
19 |
+
def test_timezone_validation():
|
20 |
+
"""Test timezone validation functionality."""
|
21 |
+
print("π§ͺ Testing timezone validation...")
|
22 |
+
|
23 |
+
# Valid timezones
|
24 |
+
valid_timezones = [
|
25 |
+
"UTC",
|
26 |
+
"America/New_York",
|
27 |
+
"Europe/London",
|
28 |
+
"Asia/Tokyo",
|
29 |
+
"Africa/Porto-Novo"
|
30 |
+
]
|
31 |
+
|
32 |
+
for tz in valid_timezones:
|
33 |
+
assert validate_timezone(tz), f"Should validate {tz}"
|
34 |
+
print(f"β
{tz} - Valid")
|
35 |
+
|
36 |
+
# Invalid timezones
|
37 |
+
invalid_timezones = [
|
38 |
+
"Invalid/Timezone",
|
39 |
+
"America/Bogus",
|
40 |
+
"Not/A_Timezone",
|
41 |
+
""
|
42 |
+
]
|
43 |
+
|
44 |
+
for tz in invalid_timezones:
|
45 |
+
assert not validate_timezone(tz), f"Should invalidate {tz}"
|
46 |
+
print(f"β {tz} - Invalid (as expected)")
|
47 |
+
|
48 |
+
print("β
Timezone validation tests passed!\n")
|
49 |
+
|
50 |
+
def test_timezone_formatting():
|
51 |
+
"""Test timezone formatting functionality."""
|
52 |
+
print("π§ͺ Testing timezone formatting...")
|
53 |
+
|
54 |
+
# Test formatting with timezone
|
55 |
+
schedule_time = "Monday 14:30"
|
56 |
+
timezone = "America/New_York"
|
57 |
+
formatted = format_timezone_schedule(schedule_time, timezone)
|
58 |
+
expected = "Monday 14:30::::America/New_York"
|
59 |
+
|
60 |
+
assert formatted == expected, f"Expected '{expected}', got '{formatted}'"
|
61 |
+
print(f"β
Formatted: {formatted}")
|
62 |
+
|
63 |
+
# Test formatting without timezone
|
64 |
+
formatted_no_tz = format_timezone_schedule(schedule_time, None)
|
65 |
+
assert formatted_no_tz == schedule_time, f"Expected '{schedule_time}', got '{formatted_no_tz}'"
|
66 |
+
print(f"β
No timezone: {formatted_no_tz}")
|
67 |
+
|
68 |
+
print("β
Timezone formatting tests passed!\n")
|
69 |
+
|
70 |
+
def test_timezone_parsing():
|
71 |
+
"""Test timezone parsing functionality."""
|
72 |
+
print("π§ͺ Testing timezone parsing...")
|
73 |
+
|
74 |
+
# Test parsing with timezone
|
75 |
+
schedule_with_tz = "Monday 14:30::::America/New_York"
|
76 |
+
time_part, tz_part = parse_timezone_schedule(schedule_with_tz)
|
77 |
+
|
78 |
+
assert time_part == "Monday 14:30", f"Expected 'Monday 14:30', got '{time_part}'"
|
79 |
+
assert tz_part == "America/New_York", f"Expected 'America/New_York', got '{tz_part}'"
|
80 |
+
print(f"β
Parsed time: {time_part}, timezone: {tz_part}")
|
81 |
+
|
82 |
+
# Test parsing without timezone
|
83 |
+
schedule_without_tz = "Monday 14:30"
|
84 |
+
time_part_no_tz, tz_part_no_tz = parse_timezone_schedule(schedule_without_tz)
|
85 |
+
|
86 |
+
assert time_part_no_tz == "Monday 14:30", f"Expected 'Monday 14:30', got '{time_part_no_tz}'"
|
87 |
+
assert tz_part_no_tz is None, f"Expected None, got '{tz_part_no_tz}'"
|
88 |
+
print(f"β
Parsed time: {time_part_no_tz}, timezone: {tz_part_no_tz}")
|
89 |
+
|
90 |
+
print("β
Timezone parsing tests passed!\n")
|
91 |
+
|
92 |
+
def test_adjusted_time_calculation():
|
93 |
+
"""Test adjusted time calculation with timezone."""
|
94 |
+
print("π§ͺ Testing adjusted time calculation...")
|
95 |
+
|
96 |
+
# Test with timezone
|
97 |
+
schedule_time = "Monday 14:30::::America/New_York"
|
98 |
+
adjusted_time = calculate_adjusted_time_with_timezone(schedule_time, "America/New_York")
|
99 |
+
expected = "Monday 14:25::::America/New_York"
|
100 |
+
|
101 |
+
assert adjusted_time == expected, f"Expected '{expected}', got '{adjusted_time}'"
|
102 |
+
print(f"β
Adjusted with timezone: {adjusted_time}")
|
103 |
+
|
104 |
+
# Test without timezone
|
105 |
+
schedule_time_no_tz = "Monday 14:30"
|
106 |
+
adjusted_time_no_tz = calculate_adjusted_time_with_timezone(schedule_time_no_tz, None)
|
107 |
+
expected_no_tz = "Monday 14:25"
|
108 |
+
|
109 |
+
assert adjusted_time_no_tz == expected_no_tz, f"Expected '{expected_no_tz}', got '{adjusted_time_no_tz}'"
|
110 |
+
print(f"β
Adjusted without timezone: {adjusted_time_no_tz}")
|
111 |
+
|
112 |
+
print("β
Adjusted time calculation tests passed!\n")
|
113 |
+
|
114 |
+
def test_server_timezone():
|
115 |
+
"""Test server timezone detection."""
|
116 |
+
print("π§ͺ Testing server timezone detection...")
|
117 |
+
|
118 |
+
server_tz = get_server_timezone()
|
119 |
+
print(f"β
Server timezone: {server_tz}")
|
120 |
+
|
121 |
+
# Should be a valid timezone
|
122 |
+
assert validate_timezone(server_tz), f"Server timezone {server_tz} should be valid"
|
123 |
+
print("β
Server timezone is valid!")
|
124 |
+
|
125 |
+
print("β
Server timezone tests passed!\n")
|
126 |
+
|
127 |
+
def test_time_conversion():
|
128 |
+
"""Test time conversion between timezones."""
|
129 |
+
print("π§ͺ Testing time conversion...")
|
130 |
+
|
131 |
+
# Test conversion from one timezone to another
|
132 |
+
from_tz = "America/New_York"
|
133 |
+
to_tz = "Europe/London"
|
134 |
+
time_str = "Monday 14:30"
|
135 |
+
|
136 |
+
try:
|
137 |
+
converted_time = convert_time_to_timezone(time_str, from_tz, to_tz)
|
138 |
+
print(f"β
Converted {time_str} from {from_tz} to {to_tz}: {converted_time}")
|
139 |
+
except Exception as e:
|
140 |
+
print(f"β οΈ Time conversion failed (expected if pytz not available): {e}")
|
141 |
+
|
142 |
+
print("β
Time conversion tests completed!\n")
|
143 |
+
|
144 |
+
def test_frontend_compatibility():
|
145 |
+
"""Test frontend compatibility with timezone data."""
|
146 |
+
print("π§ͺ Testing frontend compatibility...")
|
147 |
+
|
148 |
+
# Simulate data that would come from the database
|
149 |
+
schedule_data = {
|
150 |
+
"id": "123",
|
151 |
+
"schedule_time": "Monday 14:30::::America/New_York",
|
152 |
+
"adjusted_time": "Monday 14:25::::America/New_York"
|
153 |
+
}
|
154 |
+
|
155 |
+
# Test parsing like the frontend would do
|
156 |
+
display_time = schedule_data["schedule_time"].split("::::")[0]
|
157 |
+
print(f"β
Display time (no timezone): {display_time}")
|
158 |
+
|
159 |
+
# Test that timezone can be extracted
|
160 |
+
if "::::" in schedule_data["schedule_time"]:
|
161 |
+
timezone = schedule_data["schedule_time"].split("::::")[1]
|
162 |
+
print(f"β
Extracted timezone: {timezone}")
|
163 |
+
|
164 |
+
print("β
Frontend compatibility tests passed!\n")
|
165 |
+
|
166 |
+
def main():
|
167 |
+
"""Run all timezone tests."""
|
168 |
+
print("Starting timezone functionality tests...\n")
|
169 |
+
|
170 |
+
try:
|
171 |
+
test_timezone_validation()
|
172 |
+
test_timezone_formatting()
|
173 |
+
test_timezone_parsing()
|
174 |
+
test_adjusted_time_calculation()
|
175 |
+
test_server_timezone()
|
176 |
+
test_time_conversion()
|
177 |
+
test_frontend_compatibility()
|
178 |
+
|
179 |
+
print("[OK] All timezone tests passed successfully!")
|
180 |
+
return True
|
181 |
+
|
182 |
+
except Exception as e:
|
183 |
+
print(f"[FAIL] Test failed with error: {e}")
|
184 |
+
import traceback
|
185 |
+
traceback.print_exc()
|
186 |
+
return False
|
187 |
+
|
188 |
+
if __name__ == "__main__":
|
189 |
+
success = main()
|
190 |
+
sys.exit(0 if success else 1)
|