Zelyanoth commited on
Commit
7ed8537
Β·
1 Parent(s): 98b8066

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 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
- print(f"[DEBUG] get_schedules called for user_id: {user_id}")
26
 
27
  # Check if Supabase client is initialized
28
  if not hasattr(current_app, 'supabase') or current_app.supabase is None:
29
- print("[ERROR] Supabase client not initialized")
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
- print(f"[DEBUG] Found {len(schedules)} schedules for user {user_id}")
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
- print(f"[ERROR] Get schedules error: {str(e)}")
54
  import traceback
55
- print(f"[ERROR] Full traceback: {traceback.format_exc()}")
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
- print("[INFO] Triggering immediate APScheduler update...")
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
- print(f"[WARNING] Failed to trigger immediate scheduler update: {str(e)}")
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
- print("[INFO] Triggering immediate APScheduler update after deletion...")
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
- print(f"[WARNING] Failed to trigger immediate scheduler update: {str(e)}")
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.DEBUG,
15
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
16
  )
17
- logging.getLogger('apscheduler').setLevel(logging.DEBUG)
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("Initializing APScheduler...")
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("Scheduler started")
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"Error initializing APScheduler: {str(e)}")
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 schedule times
144
- content_gen_cron = self._parse_schedule_time(adjusted_time)
145
- publish_cron = self._parse_schedule_time(schedule_time)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- logger.info(f"Created publishing job: {pub_job_id}")
 
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"Starting content generation for schedule {schedule_id}")
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"Starting post publishing for schedule {schedule_id}")
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 = self._calculate_adjusted_time(formatted_schedule)
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">{schedule.schedule_time}</span>
 
 
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)