SHAFI commited on
Commit
5e81c7b
·
1 Parent(s): 586568d

newsletter bug erro '500 NOT FOUND' Fixed

Browse files
app/routes/admin.py CHANGED
@@ -445,7 +445,7 @@ async def send_newsletter_now(preference: str = "Weekly"):
445
  @router.get("/subscribers/analytics")
446
  async def get_subscriber_analytics():
447
  """
448
- Get subscriber distribution by preference
449
 
450
  Shows how many subscribers have chosen each newsletter timing.
451
  Useful for understanding user preferences and planning content strategy.
@@ -454,17 +454,17 @@ async def get_subscriber_analytics():
454
  Total active subscribers and breakdown by preference
455
  """
456
  try:
457
- from app.services.firebase_service import get_firebase_service
458
 
459
- firebase = get_firebase_service()
460
 
461
- if not firebase.initialized:
462
  raise HTTPException(
463
  status_code=503,
464
- detail="Firebase service not available"
465
  )
466
 
467
- all_subscribers = firebase.get_all_subscribers()
468
 
469
  # Calculate preference distribution
470
  preference_counts = {
@@ -479,18 +479,26 @@ async def get_subscriber_analytics():
479
  total_count = len(all_subscribers)
480
 
481
  for sub in all_subscribers:
482
- if sub.get('subscribed', True):
483
  active_count += 1
484
- pref = sub.get('preference', 'Weekly')
485
- if pref in preference_counts:
486
- preference_counts[pref] += 1
 
 
 
 
 
 
 
 
487
 
488
  return {
489
  "total_subscribers": total_count,
490
  "active_subscribers": active_count,
491
- "unsubscribed": total_count - active_count,
492
  "distribution_by_preference": preference_counts,
493
- " percentage_distribution": {
494
  pref: round((count / active_count * 100), 2) if active_count > 0 else 0
495
  for pref, count in preference_counts.items()
496
  }
 
445
  @router.get("/subscribers/analytics")
446
  async def get_subscriber_analytics():
447
  """
448
+ Get subscriber distribution by preference from Appwrite
449
 
450
  Shows how many subscribers have chosen each newsletter timing.
451
  Useful for understanding user preferences and planning content strategy.
 
454
  Total active subscribers and breakdown by preference
455
  """
456
  try:
457
+ from app.services.appwrite_db import get_appwrite_db
458
 
459
+ appwrite_db = get_appwrite_db()
460
 
461
+ if not appwrite_db.initialized:
462
  raise HTTPException(
463
  status_code=503,
464
+ detail="Appwrite database not available"
465
  )
466
 
467
+ all_subscribers = await appwrite_db.get_all_subscribers()
468
 
469
  # Calculate preference distribution
470
  preference_counts = {
 
479
  total_count = len(all_subscribers)
480
 
481
  for sub in all_subscribers:
482
+ if sub.get('isActive', True):
483
  active_count += 1
484
+ # Count each preference subscription
485
+ if sub.get('sub_morning', False):
486
+ preference_counts['Morning'] += 1
487
+ if sub.get('sub_afternoon', False):
488
+ preference_counts['Afternoon'] += 1
489
+ if sub.get('sub_evening', False):
490
+ preference_counts['Evening'] += 1
491
+ if sub.get('sub_weekly', False):
492
+ preference_counts['Weekly'] += 1
493
+ if sub.get('sub_monthly', False):
494
+ preference_counts['Monthly'] += 1
495
 
496
  return {
497
  "total_subscribers": total_count,
498
  "active_subscribers": active_count,
499
+ "inactive": total_count - active_count,
500
  "distribution_by_preference": preference_counts,
501
+ "percentage_distribution": {
502
  pref: round((count / active_count * 100), 2) if active_count > 0 else 0
503
  for pref, count in preference_counts.items()
504
  }
app/routes/subscription.py CHANGED
@@ -238,17 +238,17 @@ async def unsubscribe_post(request: UnsubscribeRequest):
238
 
239
  @router.get("/subscribers/count")
240
  async def get_subscriber_count():
241
- """Get total number of active subscribers"""
242
  try:
243
- firebase = get_firebase_service()
244
- subscribers = firebase.get_all_subscribers()
245
 
246
- active_count = sum(1 for s in subscribers if s.get('subscribed', True))
247
 
248
  return {
249
  "total": len(subscribers),
250
  "active": active_count,
251
- "unsubscribed": len(subscribers) - active_count
252
  }
253
 
254
  except Exception as e:
@@ -265,19 +265,19 @@ async def send_newsletter(
265
  category: str = "ai"
266
  ):
267
  """
268
- Send newsletter to all subscribers
269
 
270
  - Fetches latest news from specified category
271
  - Sends to all active subscribers
272
  - Returns send statistics
273
  """
274
  try:
275
- firebase = get_firebase_service()
276
  brevo = get_brevo_service()
277
 
278
- # Get all active subscribers
279
- subscribers = firebase.get_all_subscribers()
280
- active_subscribers = [s for s in subscribers if s.get('subscribed', True)]
281
 
282
  if not active_subscribers:
283
  return {
 
238
 
239
  @router.get("/subscribers/count")
240
  async def get_subscriber_count():
241
+ """Get total number of active subscribers from Appwrite"""
242
  try:
243
+ appwrite_db = get_appwrite_db()
244
+ subscribers = await appwrite_db.get_all_subscribers()
245
 
246
+ active_count = sum(1 for s in subscribers if s.get('isActive', True))
247
 
248
  return {
249
  "total": len(subscribers),
250
  "active": active_count,
251
+ "inactive": len(subscribers) - active_count
252
  }
253
 
254
  except Exception as e:
 
265
  category: str = "ai"
266
  ):
267
  """
268
+ Send newsletter to all subscribers (LEGACY ENDPOINT - Use scheduled newsletters instead)
269
 
270
  - Fetches latest news from specified category
271
  - Sends to all active subscribers
272
  - Returns send statistics
273
  """
274
  try:
275
+ appwrite_db = get_appwrite_db()
276
  brevo = get_brevo_service()
277
 
278
+ # Get all active subscribers from Appwrite
279
+ subscribers = await appwrite_db.get_all_subscribers()
280
+ active_subscribers = [s for s in subscribers if s.get('isActive', True)]
281
 
282
  if not active_subscribers:
283
  return {
app/services/newsletter_service.py CHANGED
@@ -281,13 +281,9 @@ async def send_scheduled_newsletter(preference: str) -> Dict[str, int]:
281
  return result
282
 
283
 
284
- def get_subscribers_by_preference(preference: str) -> List[Dict]:
285
- """
286
- Helper function to get subscribers for a specific preference.
287
- Used by admin endpoints and testing.
288
- """
289
- firebase = get_firebase_service()
290
- return firebase.get_subscribers_by_preference(preference)
291
 
292
 
293
  async def preview_newsletter_content(preference: str) -> Dict:
 
281
  return result
282
 
283
 
284
+
285
+ # Note: Subscriber queries now use Appwrite directly via appwrite_db.get_subscribers_by_preference()
286
+ # See newsletter_service.py line 211
 
 
 
 
287
 
288
 
289
  async def preview_newsletter_content(preference: str) -> Dict:
app/services/scheduler.py CHANGED
@@ -560,7 +560,8 @@ async def trigger_newsletter_now(preference: str):
560
  """Manually trigger newsletter"""
561
  from app.services.newsletter_service import send_scheduled_newsletter
562
  logger.info(f"🔧 [MANUAL TRIGGER] Running {preference} newsletter job NOW...")
563
- await send_scheduled_newsletter(preference)
 
564
 
565
 
566
 
 
560
  """Manually trigger newsletter"""
561
  from app.services.newsletter_service import send_scheduled_newsletter
562
  logger.info(f"🔧 [MANUAL TRIGGER] Running {preference} newsletter job NOW...")
563
+ result = await send_scheduled_newsletter(preference)
564
+ return result
565
 
566
 
567
 
tools/test_audio_api.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test script for audio generation API
3
+ Tests the complete flow from request to response
4
+ """
5
+ import requests
6
+ import json
7
+
8
+ # Test configuration
9
+ API_URL = "http://127.0.0.1:8000/api/audio/generate"
10
+
11
+ # Sample article data
12
+ test_payload = {
13
+ "article_url": "https://www.rawstory.com%2FTrump-ic e-2675183222%2F&title=Analyst%20issues%20dire%20warning%20about%20Trump%27s%20%27horrifying%27%20directive%20to%20ICE%20to%20detain%20immigration%20judges",
14
+ "title": "Test Article for Audio Generation",
15
+ "image_url": "https://example.com/image.jpg",
16
+ "category": "ai"
17
+ }
18
+
19
+ print("=" * 60)
20
+ print("🧪 Testing Audio Generation API")
21
+ print("=" * 60)
22
+ print(f"\n📍 Endpoint: {API_URL}")
23
+ print(f"\n📦 Payload:")
24
+ print(json.dumps(test_payload, indent=2))
25
+ print("\n" + "=" * 60)
26
+
27
+ try:
28
+ print("\n🚀 Sending POST request...")
29
+ response = requests.post(
30
+ API_URL,
31
+ json=test_payload,
32
+ headers={"Content-Type": "application/json"},
33
+ timeout=60 # Audio generation can take time
34
+ )
35
+
36
+ print(f"\n📊 Response Status: {response.status_code}")
37
+ print(f"\n📄 Response Body:")
38
+
39
+ try:
40
+ response_data = response.json()
41
+ print(json.dumps(response_data, indent=2))
42
+
43
+ if response.status_code == 200:
44
+ if response_data.get('success'):
45
+ print("\n✅ SUCCESS! Audio generated successfully")
46
+ print(f"🔊 Audio URL: {response_data.get('audio_url')}")
47
+ else:
48
+ print("\n❌ FAILED: success=False in response")
49
+ print(f"💬 Message: {response_data.get('message', 'No message')}")
50
+ else:
51
+ print(f"\n❌ HTTP ERROR: {response.status_code}")
52
+ print(f"💬 Detail: {response_data.get('detail', 'No detail')}")
53
+
54
+ except json.JSONDecodeError:
55
+ print("⚠️ Response is not valid JSON:")
56
+ print(response.text)
57
+
58
+ except requests.exceptions.ConnectionError:
59
+ print("\n❌ CONNECTION ERROR: Cannot connect to backend")
60
+ print("Make sure the backend is running on http://127.0.0.1:8000")
61
+
62
+ except requests.exceptions.Timeout:
63
+ print("\n❌ TIMEOUT: Request took longer than 60 seconds")
64
+
65
+ except Exception as e:
66
+ print(f"\n❌ UNEXPECTED ERROR: {type(e).__name__}: {e}")
67
+
68
+ print("\n" + "=" * 60)
tools/test_audio_quick.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Quick test script for audio API after fixes
3
+ """
4
+ import requests
5
+ import json
6
+
7
+ print("=" * 60)
8
+ print("🎵 AUDIO API QUICK TEST")
9
+ print("=" * 60)
10
+
11
+ # Test 1: Health Check
12
+ print("\n1️⃣ Testing backend health...")
13
+ try:
14
+ health = requests.get("http://127.0.0.1:8000/health", timeout=5)
15
+ print(f" ✅ Backend is running (Status: {health.status_code})")
16
+ except Exception as e:
17
+ print(f" ❌ Backend not responding: {e}")
18
+ exit(1)
19
+
20
+ # Test 2: Audio Endpoint
21
+ print("\n2️⃣ Testing /api/audio/generate endpoint...")
22
+ payload = {
23
+ "article_url": "https://www.example.com/test-article",
24
+ "title": "Test Article Title",
25
+ "category": "ai",
26
+ "image_url": "https://www.example.com/image.jpg"
27
+ }
28
+
29
+ print(f"\n📦 Payload:")
30
+ print(json.dumps(payload, indent=2))
31
+
32
+ try:
33
+ print("\n⏳ Sending request (this may take 10-30 seconds)...")
34
+ response = requests.post(
35
+ "http://127.0.0.1:8000/api/audio/generate",
36
+ json=payload,
37
+ timeout=60
38
+ )
39
+
40
+ print(f"\n📊 Status Code: {response.status_code}")
41
+
42
+ if response.status_code == 200:
43
+ data = response.json()
44
+ if data.get('success'):
45
+ print("✅ SUCCESS! Audio generated")
46
+ print(f"🔊 Audio URL: {data.get('audio_url')}")
47
+ else:
48
+ print("⚠️ Request succeeded but success=False")
49
+ print(f"Message: {data.get('message', 'No message')}")
50
+ else:
51
+ print(f"❌ HTTP {response.status_code}")
52
+ try:
53
+ error_data = response.json()
54
+ print(f"Detail: {error_data.get('detail', 'No detail')}")
55
+ except:
56
+ print(f"Response: {response.text[:200]}")
57
+
58
+ except requests.exceptions.Timeout:
59
+ print("❌ Request timed out (>60s)")
60
+ print("Possible causes:")
61
+ print(" - GROQ_API_KEY not set in .env")
62
+ print(" - Appwrite bucket 'audio-summaries' doesn't exist")
63
+ print(" - Article content scraping failed")
64
+
65
+ except Exception as e:
66
+ print(f"❌ Error: {type(e).__name__}: {e}")
67
+
68
+ print("\n" + "=" * 60)
69
+ print("\n💡 If test failed, check:")
70
+ print(" 1. GROQ_API_KEY in .env file")
71
+ print(" 2. 'audio-summaries' bucket exists in Appwrite")
72
+ print(" 3. Backend terminal logs for detailed error")
73
+ print("=" * 60)
tools/test_cloud_schema.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ from datetime import datetime
4
+
5
+ # Add parent directory to path
6
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
7
+
8
+ from app.models import Article
9
+ from app.services.appwrite_db import AppwriteDatabase
10
+ from app.config import settings
11
+
12
+ # MOCK SETTINGS FOR TEST
13
+ # We need to simulate that we are saving to the CLOUD collection
14
+ settings.APPWRITE_CLOUD_COLLECTION_ID = "mock_cloud_collection_id"
15
+ settings.APPWRITE_DATABASE_ID = "mock_db_id"
16
+
17
+ def test_cloud_schema_mapping():
18
+ print("🧪 Testing Cloud Collection Schema Mapping...")
19
+
20
+ # 1. Create a dummy AppwriteDatabase instance
21
+ # We will mock the tablesDB.create_row method to intercept the data
22
+ db = AppwriteDatabase()
23
+
24
+ # Mock the internal tablesDB.create_row to just print the data
25
+ class MockDB:
26
+ def create_row(self, database_id, collection_id, document_id, data):
27
+ print(f"\n📝 INTERCEPTED WRITE to {collection_id}:")
28
+ print(f" IDs: {document_id}")
29
+ print(f" Keys: {list(data.keys())}")
30
+
31
+ # CHECK 1: 'image' should exist, 'image_url' should NOT
32
+ if 'image' in data and 'image_url' not in data:
33
+ print(" ✅ PASS: 'image' used instead of 'image_url'")
34
+ else:
35
+ print(f" ❌ FAIL: Keys are wrong! {list(data.keys())}")
36
+
37
+ # CHECK 2: 'publishedAt' should exist, 'published_at' should NOT
38
+ if 'publishedAt' in data and 'published_at' not in data:
39
+ print(" ✅ PASS: 'publishedAt' used instead of 'published_at'")
40
+ else:
41
+ print(f" ❌ FAIL: Date keys are wrong! {list(data.keys())}")
42
+
43
+ return {'$id': document_id}
44
+
45
+ db.tablesDB = MockDB()
46
+ db.initialized = True # Force init for test
47
+
48
+ # 2. Create a test article (Cloud Category)
49
+ # This category 'cloud-aws' triggers the get_collection_id -> cloud collection logic
50
+ article = {
51
+ 'title': 'Test Cloud Article',
52
+ 'url': 'https://aws.amazon.com/test',
53
+ 'image_url': 'https://aws.amazon.com/image.jpg', # New schema key
54
+ 'published_at': datetime.now(), # New schema key
55
+ 'source': 'AWS Blog',
56
+ 'category': 'cloud-aws' # CRITICAL: This routes to Cloud Collection
57
+ }
58
+
59
+ print(f"\n📋 Input Article Keys: {list(article.keys())}")
60
+
61
+ # 3. Import asyncio and run save_articles
62
+ import asyncio
63
+
64
+ # Patch get_collection_id to return our mock ID
65
+ original_get_collection_id = db.get_collection_id
66
+ db.get_collection_id = lambda cat: settings.APPWRITE_CLOUD_COLLECTION_ID
67
+
68
+ try:
69
+ asyncio.run(db.save_articles([article]))
70
+ except Exception as e:
71
+ print(f"Error: {e}")
72
+ finally:
73
+ # Restore (good practice, though script ends)
74
+ db.get_collection_id = original_get_collection_id
75
+
76
+ if __name__ == "__main__":
77
+ test_cloud_schema_mapping()
tools/test_validation_fix.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ from datetime import datetime
4
+
5
+ # Add parent directory to path
6
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
7
+
8
+ from app.models import Article
9
+ from app.utils.data_validation import is_valid_article, sanitize_article
10
+
11
+ def test_validation():
12
+ print("🧪 Testing Data Validation Fix...")
13
+
14
+ # 1. Create a Pydantic Article (snake_case fields)
15
+ article = Article(
16
+ title="Test Article Title For Validation",
17
+ url="https://example.com/test-article",
18
+ published_at=datetime.now(),
19
+ image_url="https://example.com/image.jpg",
20
+ source="Test Source",
21
+ category="ai"
22
+ )
23
+
24
+ print(f"\n📋 Model Data: {article.model_dump()}")
25
+
26
+ # 2. Test is_valid_article
27
+ is_valid = is_valid_article(article)
28
+ print(f"\n✅ is_valid_article: {is_valid}")
29
+
30
+ if is_valid:
31
+ print(" -> PASSED: Pydantic model validated successfully!")
32
+ else:
33
+ print(" -> FAILED: Pydantic model rejected!")
34
+
35
+ # 3. Test sanitize_article
36
+ sanitized = sanitize_article(article)
37
+ print(f"\n🧹 Sanitized Data Keys: {list(sanitized.keys())}")
38
+ print(f" publishedAt: {sanitized.get('publishedAt')}")
39
+ print(f" published_at: {sanitized.get('published_at')}")
40
+ print(f" image: {sanitized.get('image')}")
41
+ print(f" image_url: {sanitized.get('image_url')}")
42
+
43
+ if sanitized.get('publishedAt') and sanitized.get('image_url'):
44
+ print(" -> PASSED: Sanitization preserved critical fields!")
45
+ else:
46
+ print(" -> FAILED: Sanitization missing fields!")
47
+
48
+ if __name__ == "__main__":
49
+ test_validation()