amauricunha commited on
Commit
0efa332
·
verified ·
1 Parent(s): eed86c5

Delete database.py

Browse files
Files changed (1) hide show
  1. database.py +0 -785
database.py DELETED
@@ -1,785 +0,0 @@
1
- # database.py
2
- import sqlite3
3
- import os
4
- import hashlib
5
- import secrets
6
- import smtplib
7
- from email.mime.text import MIMEText
8
- from email.mime.multipart import MIMEMultipart
9
- from datetime import datetime, timedelta
10
- from functools import wraps
11
- from flask import g, session, jsonify, request
12
- import logging
13
-
14
- # Configure logging
15
- logging.basicConfig(level=logging.INFO)
16
- logger = logging.getLogger(__name__)
17
-
18
- # Database configuration
19
- DATABASE_PATH = 'users.db'
20
-
21
- def get_db():
22
- """Get database connection"""
23
- if 'db' not in g:
24
- g.db = sqlite3.connect(DATABASE_PATH)
25
- g.db.row_factory = sqlite3.Row
26
- return g.db
27
-
28
- def close_db(e=None):
29
- """Close database connection"""
30
- db = g.pop('db', None)
31
- if db is not None:
32
- db.close()
33
-
34
- def get_db_connection():
35
- """Get a new database connection (for use outside Flask context)"""
36
- conn = sqlite3.connect(DATABASE_PATH)
37
- conn.row_factory = sqlite3.Row
38
- return conn
39
-
40
- def init_db():
41
- """Initialize database with required tables"""
42
- try:
43
- db = sqlite3.connect(DATABASE_PATH)
44
- db.executescript('''
45
- -- Users table
46
- CREATE TABLE IF NOT EXISTS users (
47
- id INTEGER PRIMARY KEY AUTOINCREMENT,
48
- email TEXT UNIQUE NOT NULL,
49
- password_hash TEXT NOT NULL,
50
- salt TEXT NOT NULL,
51
- is_confirmed BOOLEAN DEFAULT FALSE,
52
- confirmation_token TEXT,
53
- reset_token TEXT,
54
- reset_token_expires TIMESTAMP,
55
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
56
- last_login TIMESTAMP,
57
- is_active BOOLEAN DEFAULT TRUE
58
- );
59
-
60
- -- User flashcards table
61
- CREATE TABLE IF NOT EXISTS user_flashcards (
62
- id INTEGER PRIMARY KEY AUTOINCREMENT,
63
- user_id INTEGER NOT NULL,
64
- term TEXT NOT NULL,
65
- translation TEXT,
66
- context_sentence TEXT,
67
- gapped_sentence TEXT,
68
- definition TEXT,
69
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
70
- study_count INTEGER DEFAULT 0,
71
- last_studied TIMESTAMP,
72
- difficulty_level INTEGER DEFAULT 1,
73
- FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
74
- );
75
-
76
- -- User study sessions table
77
- CREATE TABLE IF NOT EXISTS study_sessions (
78
- id INTEGER PRIMARY KEY AUTOINCREMENT,
79
- user_id INTEGER NOT NULL,
80
- session_type TEXT NOT NULL, -- 'flashcard', 'conversation', 'activity'
81
- duration_minutes INTEGER,
82
- cards_studied INTEGER DEFAULT 0,
83
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
84
- FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
85
- );
86
-
87
- -- User settings table
88
- CREATE TABLE IF NOT EXISTS user_settings (
89
- id INTEGER PRIMARY KEY AUTOINCREMENT,
90
- user_id INTEGER UNIQUE NOT NULL,
91
- preferred_model TEXT DEFAULT 'gemini:gemini-2.5-flash-latest',
92
- context_focus TEXT DEFAULT 'General/Social',
93
- voice_accent TEXT DEFAULT 'co.uk',
94
- daily_goal INTEGER DEFAULT 10,
95
- notification_enabled BOOLEAN DEFAULT TRUE,
96
- -- New settings for advanced features
97
- english_level TEXT DEFAULT 'B1', -- A1, A2, B1, B2, C1, C2
98
- study_goals TEXT, -- JSON: objectives like "business english", "technical vocabulary"
99
- preferred_content_types TEXT DEFAULT 'articles,videos', -- comma separated
100
- content_difficulty TEXT DEFAULT 'adaptive', -- 'easy', 'medium', 'hard', 'adaptive'
101
- study_schedule TEXT, -- JSON: preferred days/times
102
- auto_recommendations BOOLEAN DEFAULT TRUE,
103
- content_sources TEXT DEFAULT 'news,tech,business', -- preferred content sources
104
- FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
105
- );
106
-
107
- -- User articles/content table
108
- CREATE TABLE IF NOT EXISTS user_articles (
109
- id INTEGER PRIMARY KEY AUTOINCREMENT,
110
- user_id INTEGER NOT NULL,
111
- title TEXT NOT NULL,
112
- content TEXT NOT NULL,
113
- source_url TEXT,
114
- source_type TEXT DEFAULT 'manual', -- 'manual', 'web_search', 'recommended'
115
- category TEXT, -- user interest category
116
- difficulty_level TEXT, -- estimated difficulty
117
- word_count INTEGER,
118
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
119
- last_accessed TIMESTAMP,
120
- is_favorite BOOLEAN DEFAULT FALSE,
121
- study_progress REAL DEFAULT 0.0, -- 0.0 to 1.0
122
- FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
123
- );
124
-
125
- -- User interests/preferences table
126
- CREATE TABLE IF NOT EXISTS user_interests (
127
- id INTEGER PRIMARY KEY AUTOINCREMENT,
128
- user_id INTEGER NOT NULL,
129
- interest_category TEXT NOT NULL,
130
- weight REAL DEFAULT 1.0, -- importance weight
131
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
132
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
133
- FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
134
- UNIQUE(user_id, interest_category)
135
- );
136
-
137
- -- Content recommendations table
138
- CREATE TABLE IF NOT EXISTS content_recommendations (
139
- id INTEGER PRIMARY KEY AUTOINCREMENT,
140
- user_id INTEGER NOT NULL,
141
- article_id INTEGER,
142
- recommendation_reason TEXT,
143
- relevance_score REAL,
144
- recommended_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
145
- viewed BOOLEAN DEFAULT FALSE,
146
- accepted BOOLEAN DEFAULT FALSE,
147
- FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
148
- FOREIGN KEY (article_id) REFERENCES user_articles (id) ON DELETE CASCADE
149
- );
150
-
151
- -- Study plans table
152
- CREATE TABLE IF NOT EXISTS study_plans (
153
- id INTEGER PRIMARY KEY AUTOINCREMENT,
154
- user_id INTEGER NOT NULL,
155
- plan_name TEXT NOT NULL,
156
- target_level TEXT, -- A1, A2, B1, B2, C1, C2
157
- current_level TEXT,
158
- objectives TEXT, -- JSON string with objectives
159
- weekly_hours INTEGER DEFAULT 5,
160
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
161
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
162
- is_active BOOLEAN DEFAULT TRUE,
163
- completion_percentage REAL DEFAULT 0.0,
164
- FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
165
- );
166
-
167
- -- Study plan activities table
168
- CREATE TABLE IF NOT EXISTS study_plan_activities (
169
- id INTEGER PRIMARY KEY AUTOINCREMENT,
170
- plan_id INTEGER NOT NULL,
171
- activity_type TEXT NOT NULL, -- 'reading', 'flashcards', 'conversation', 'writing'
172
- content_reference TEXT, -- reference to article, flashcard set, etc.
173
- scheduled_date DATE,
174
- estimated_duration INTEGER, -- minutes
175
- actual_duration INTEGER,
176
- completed BOOLEAN DEFAULT FALSE,
177
- completed_at TIMESTAMP,
178
- difficulty_rating INTEGER, -- 1-5 user rating
179
- notes TEXT,
180
- FOREIGN KEY (plan_id) REFERENCES study_plans (id) ON DELETE CASCADE
181
- );
182
-
183
- -- User analytics table
184
- CREATE TABLE IF NOT EXISTS user_analytics (
185
- id INTEGER PRIMARY KEY AUTOINCREMENT,
186
- user_id INTEGER NOT NULL,
187
- metric_name TEXT NOT NULL,
188
- metric_value REAL NOT NULL,
189
- metric_date DATE NOT NULL,
190
- context_data TEXT, -- JSON with additional context
191
- FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
192
- );
193
-
194
- -- Token usage tracking table for admin
195
- CREATE TABLE IF NOT EXISTS token_usage (
196
- id INTEGER PRIMARY KEY AUTOINCREMENT,
197
- user_id INTEGER,
198
- api_provider TEXT NOT NULL, -- 'groq', 'gemini', etc.
199
- input_tokens INTEGER DEFAULT 0,
200
- output_tokens INTEGER DEFAULT 0,
201
- operation_type TEXT, -- 'conversation', 'content_analysis', 'recommendation', etc.
202
- tokens_used INTEGER GENERATED ALWAYS AS (input_tokens + output_tokens) STORED,
203
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
204
- FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
205
- );
206
-
207
- -- Create indexes for better performance
208
- CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
209
- CREATE INDEX IF NOT EXISTS idx_users_confirmation_token ON users(confirmation_token);
210
- CREATE INDEX IF NOT EXISTS idx_users_reset_token ON users(reset_token);
211
- CREATE INDEX IF NOT EXISTS idx_flashcards_user_id ON user_flashcards(user_id);
212
- CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON study_sessions(user_id);
213
- CREATE INDEX IF NOT EXISTS idx_settings_user_id ON user_settings(user_id);
214
- CREATE INDEX IF NOT EXISTS idx_articles_user_id ON user_articles(user_id);
215
- CREATE INDEX IF NOT EXISTS idx_articles_category ON user_articles(category);
216
- CREATE INDEX IF NOT EXISTS idx_interests_user_id ON user_interests(user_id);
217
- CREATE INDEX IF NOT EXISTS idx_recommendations_user_id ON content_recommendations(user_id);
218
- CREATE INDEX IF NOT EXISTS idx_study_plans_user_id ON study_plans(user_id);
219
- CREATE INDEX IF NOT EXISTS idx_plan_activities_plan_id ON study_plan_activities(plan_id);
220
- CREATE INDEX IF NOT EXISTS idx_analytics_user_date ON user_analytics(user_id, metric_date);
221
- CREATE INDEX IF NOT EXISTS idx_token_usage_user_id ON token_usage(user_id);
222
- CREATE INDEX IF NOT EXISTS idx_token_usage_provider ON token_usage(api_provider);
223
- CREATE INDEX IF NOT EXISTS idx_token_usage_date ON token_usage(created_at);
224
- ''')
225
- db.commit()
226
- db.close()
227
- logger.info("Database initialized successfully")
228
- return True
229
- except Exception as e:
230
- logger.error(f"Error initializing database: {e}")
231
- return False
232
-
233
- def hash_password(password, salt=None):
234
- """Hash password with salt"""
235
- if salt is None:
236
- salt = secrets.token_hex(32)
237
-
238
- password_hash = hashlib.pbkdf2_hmac(
239
- 'sha256',
240
- password.encode('utf-8'),
241
- salt.encode('utf-8'),
242
- 100000 # iterations
243
- )
244
- return password_hash.hex(), salt
245
-
246
- def verify_password(password, password_hash, salt):
247
- """Verify password against hash"""
248
- new_hash, _ = hash_password(password, salt)
249
- return new_hash == password_hash
250
-
251
- def generate_token():
252
- """Generate secure random token"""
253
- return secrets.token_urlsafe(32)
254
-
255
- def create_user(email, password):
256
- """Create new user account"""
257
- try:
258
- db = get_db()
259
-
260
- # Check if user already exists
261
- existing_user = db.execute(
262
- 'SELECT id FROM users WHERE email = ?', (email,)
263
- ).fetchone()
264
-
265
- if existing_user:
266
- return {'success': False, 'message': 'Email already registered'}
267
-
268
- # Hash password
269
- password_hash, salt = hash_password(password)
270
- confirmation_token = generate_token()
271
-
272
- # For HF Spaces demo mode, auto-confirm emails
273
- is_hf_spaces = os.environ.get('SPACE_ID') is not None
274
- has_smtp_username = os.environ.get('SMTP_USERNAME') is not None
275
- has_smtp_password = os.environ.get('SMTP_PASSWORD') is not None
276
- has_smtp = has_smtp_username and has_smtp_password
277
-
278
- # Sempre auto-confirmar no HF Spaces ou quando SMTP não está configurado
279
- auto_confirm = is_hf_spaces or not has_smtp
280
-
281
- # Debug logging
282
- logger.info(f"Registration debug - SPACE_ID: {os.environ.get('SPACE_ID')}")
283
- logger.info(f"HF_Spaces: {is_hf_spaces}, SMTP_USER: {has_smtp_username}, SMTP_PASS: {has_smtp_password}")
284
- logger.info(f"SMTP configured: {has_smtp}, Auto-confirm: {auto_confirm}")
285
-
286
- # Insert user
287
- cursor = db.execute(
288
- '''INSERT INTO users (email, password_hash, salt, confirmation_token, is_confirmed)
289
- VALUES (?, ?, ?, ?, ?)''',
290
- (email, password_hash, salt, confirmation_token, auto_confirm)
291
- )
292
- user_id = cursor.lastrowid
293
-
294
- # Create default user settings
295
- db.execute(
296
- '''INSERT INTO user_settings (user_id) VALUES (?)''',
297
- (user_id,)
298
- )
299
-
300
- db.commit()
301
-
302
- if auto_confirm:
303
- logger.info(f"User created and auto-confirmed: {email} (HF Spaces demo mode)")
304
- message = 'Account created and ready to use! (Demo mode - no email confirmation needed)'
305
- else:
306
- logger.info(f"User created: {email}")
307
- message = 'User created successfully. Please check your email for confirmation.'
308
-
309
- return {
310
- 'success': True,
311
- 'user_id': user_id,
312
- 'confirmation_token': confirmation_token,
313
- 'message': message,
314
- 'auto_confirmed': auto_confirm
315
- }
316
-
317
- except Exception as e:
318
- logger.error(f"Error creating user: {e}")
319
- return {'success': False, 'message': 'Internal server error'}
320
-
321
- def authenticate_user(email, password):
322
- """Authenticate user login"""
323
- try:
324
- db = get_db()
325
- user = db.execute(
326
- '''SELECT id, email, password_hash, salt, is_confirmed, is_active
327
- FROM users WHERE email = ?''', (email,)
328
- ).fetchone()
329
-
330
- if not user:
331
- return {'success': False, 'message': 'Invalid email or password'}
332
-
333
- if not user['is_active']:
334
- return {'success': False, 'message': 'Account is deactivated'}
335
-
336
- if not verify_password(password, user['password_hash'], user['salt']):
337
- return {'success': False, 'message': 'Invalid email or password'}
338
-
339
- if not user['is_confirmed']:
340
- return {'success': False, 'message': 'Please confirm your email before logging in'}
341
-
342
- # Update last login
343
- db.execute(
344
- 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?',
345
- (user['id'],)
346
- )
347
- db.commit()
348
-
349
- return {
350
- 'success': True,
351
- 'user_id': user['id'],
352
- 'email': user['email'],
353
- 'message': 'Login successful'
354
- }
355
-
356
- except Exception as e:
357
- logger.error(f"Error authenticating user: {e}")
358
- return {'success': False, 'message': 'Internal server error'}
359
-
360
- def confirm_email(token):
361
- """Confirm user email with token"""
362
- try:
363
- db = get_db()
364
- user = db.execute(
365
- 'SELECT id, email FROM users WHERE confirmation_token = ? AND is_confirmed = FALSE',
366
- (token,)
367
- ).fetchone()
368
-
369
- if not user:
370
- return {'success': False, 'message': 'Invalid or expired confirmation token'}
371
-
372
- db.execute(
373
- '''UPDATE users SET is_confirmed = TRUE, confirmation_token = NULL
374
- WHERE id = ?''',
375
- (user['id'],)
376
- )
377
- db.commit()
378
-
379
- logger.info(f"Email confirmed for user: {user['email']}")
380
- return {'success': True, 'message': 'Email confirmed successfully'}
381
-
382
- except Exception as e:
383
- logger.error(f"Error confirming email: {e}")
384
- return {'success': False, 'message': 'Internal server error'}
385
-
386
- def get_user_settings(user_id):
387
- """Get user settings"""
388
- try:
389
- db = get_db()
390
- settings = db.execute(
391
- '''SELECT preferred_model, context_focus, voice_accent, daily_goal, notification_enabled
392
- FROM user_settings WHERE user_id = ?''',
393
- (user_id,)
394
- ).fetchone()
395
-
396
- if settings:
397
- return dict(settings)
398
- return None
399
-
400
- except Exception as e:
401
- logger.error(f"Error getting user settings: {e}")
402
- return None
403
-
404
- def update_user_settings(user_id, settings):
405
- """Update user settings"""
406
- try:
407
- db = get_db()
408
- db.execute(
409
- '''UPDATE user_settings
410
- SET preferred_model = ?, context_focus = ?, voice_accent = ?,
411
- daily_goal = ?, notification_enabled = ?
412
- WHERE user_id = ?''',
413
- (settings.get('preferred_model'), settings.get('context_focus'),
414
- settings.get('voice_accent'), settings.get('daily_goal'),
415
- settings.get('notification_enabled'), user_id)
416
- )
417
- db.commit()
418
- return True
419
-
420
- except Exception as e:
421
- logger.error(f"Error updating user settings: {e}")
422
- return False
423
-
424
- def save_user_flashcard(user_id, flashcard_data):
425
- """Save flashcard to user's collection"""
426
- try:
427
- db = get_db()
428
- db.execute(
429
- '''INSERT INTO user_flashcards
430
- (user_id, term, translation, context_sentence, gapped_sentence, definition)
431
- VALUES (?, ?, ?, ?, ?, ?)''',
432
- (user_id, flashcard_data.get('term'), flashcard_data.get('translation'),
433
- flashcard_data.get('context_sentence'), flashcard_data.get('gapped_sentence'),
434
- flashcard_data.get('definition'))
435
- )
436
- db.commit()
437
- return True
438
-
439
- except Exception as e:
440
- logger.error(f"Error saving flashcard: {e}")
441
- return False
442
-
443
- def get_user_flashcards(user_id, limit=50):
444
- """Get user's flashcards"""
445
- try:
446
- db = get_db()
447
- flashcards = db.execute(
448
- '''SELECT * FROM user_flashcards
449
- WHERE user_id = ?
450
- ORDER BY created_at DESC
451
- LIMIT ?''',
452
- (user_id, limit)
453
- ).fetchall()
454
-
455
- return [dict(card) for card in flashcards]
456
-
457
- except Exception as e:
458
- logger.error(f"Error getting user flashcards: {e}")
459
- return []
460
-
461
- def record_study_session(user_id, session_type, duration_minutes=None, cards_studied=0):
462
- """Record a study session"""
463
- try:
464
- db = get_db()
465
- db.execute(
466
- '''INSERT INTO study_sessions (user_id, session_type, duration_minutes, cards_studied)
467
- VALUES (?, ?, ?, ?)''',
468
- (user_id, session_type, duration_minutes, cards_studied)
469
- )
470
- db.commit()
471
- return True
472
-
473
- except Exception as e:
474
- logger.error(f"Error recording study session: {e}")
475
- return False
476
-
477
- # Authentication decorators
478
- def login_required(f):
479
- """Decorator to require login"""
480
- @wraps(f)
481
- def decorated_function(*args, **kwargs):
482
- if 'user_id' not in session:
483
- return jsonify({'error': 'Authentication required'}), 401
484
- return f(*args, **kwargs)
485
- return decorated_function
486
-
487
- def get_current_user():
488
- """Get current logged in user"""
489
- if 'user_id' in session:
490
- try:
491
- db = get_db()
492
- user = db.execute(
493
- 'SELECT id, email, is_confirmed FROM users WHERE id = ? AND is_active = TRUE',
494
- (session['user_id'],)
495
- ).fetchone()
496
- return dict(user) if user else None
497
- except Exception as e:
498
- logger.error(f"Error getting current user: {e}")
499
- return None
500
- return None
501
-
502
- # Email functionality (for Hugging Face Spaces)
503
- def send_confirmation_email(email, token):
504
- """Send confirmation email (simplified for HF Spaces with timeout)"""
505
- try:
506
- # For Hugging Face Spaces, we'll use environment variables for SMTP
507
- smtp_server = os.environ.get('SMTP_SERVER', 'smtp.gmail.com')
508
- smtp_port = int(os.environ.get('SMTP_PORT', '587'))
509
- smtp_username = os.environ.get('SMTP_USERNAME')
510
- smtp_password = os.environ.get('SMTP_PASSWORD')
511
-
512
- if not all([smtp_username, smtp_password]):
513
- logger.warning("SMTP credentials not configured - skipping email")
514
- return False
515
-
516
- # Create confirmation URL (will be updated with actual domain)
517
- base_url = os.environ.get('BASE_URL', 'http://localhost:7860')
518
- confirm_url = f"{base_url}/confirm-email?token={token}"
519
-
520
- # Create email
521
- msg = MIMEMultipart()
522
- msg['From'] = smtp_username
523
- msg['To'] = email
524
- msg['Subject'] = "Confirm your English Helper account"
525
-
526
- body = f"""
527
- <html>
528
- <body>
529
- <h2>Welcome to Dynamic English Study Studio!</h2>
530
- <p>Thank you for creating an account. Please click the link below to confirm your email address:</p>
531
- <p><a href="{confirm_url}" style="background-color: #4f46e5; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Confirm Email</a></p>
532
- <p>If the button doesn't work, copy and paste this link into your browser:</p>
533
- <p>{confirm_url}</p>
534
- <p>This link will expire in 24 hours.</p>
535
- <p>If you didn't create this account, please ignore this email.</p>
536
- </body>
537
- </html>
538
- """
539
-
540
- msg.attach(MIMEText(body, 'html'))
541
-
542
- # Send email with timeout
543
- import socket
544
-
545
- # Set socket timeout to prevent hanging
546
- socket.setdefaulttimeout(10)
547
-
548
- server = smtplib.SMTP(smtp_server, smtp_port)
549
- server.starttls()
550
- server.login(smtp_username, smtp_password)
551
- text = msg.as_string()
552
- server.sendmail(smtp_username, email, text)
553
- server.quit()
554
-
555
- # Reset socket timeout
556
- socket.setdefaulttimeout(None)
557
-
558
- logger.info(f"Confirmation email sent to {email}")
559
- return True
560
-
561
- except Exception as e:
562
- logger.error(f"Error sending confirmation email: {e}")
563
- # Reset socket timeout on error
564
- try:
565
- import socket
566
- socket.setdefaulttimeout(None)
567
- except:
568
- pass
569
- return False
570
-
571
- # --- CONTENT CURATION FUNCTIONS ---
572
-
573
- def save_user_article(user_id, title, content, source_url=None, source_type='manual', category=None):
574
- """Save article/content for user"""
575
- try:
576
- db = get_db()
577
- word_count = len(content.split()) if content else 0
578
-
579
- cursor = db.execute(
580
- '''INSERT INTO user_articles
581
- (user_id, title, content, source_url, source_type, category, word_count)
582
- VALUES (?, ?, ?, ?, ?, ?, ?)''',
583
- (user_id, title, content, source_url, source_type, category, word_count)
584
- )
585
- article_id = cursor.lastrowid
586
- db.commit()
587
-
588
- logger.info(f"Article saved for user {user_id}: {title}")
589
- return {'success': True, 'article_id': article_id}
590
-
591
- except Exception as e:
592
- logger.error(f"Error saving article: {e}")
593
- return {'success': False, 'message': 'Failed to save article'}
594
-
595
- def get_user_articles(user_id, category=None, limit=50):
596
- """Get user's saved articles"""
597
- try:
598
- db = get_db()
599
-
600
- if category:
601
- articles = db.execute(
602
- '''SELECT * FROM user_articles
603
- WHERE user_id = ? AND category = ?
604
- ORDER BY created_at DESC LIMIT ?''',
605
- (user_id, category, limit)
606
- ).fetchall()
607
- else:
608
- articles = db.execute(
609
- '''SELECT * FROM user_articles
610
- WHERE user_id = ?
611
- ORDER BY created_at DESC LIMIT ?''',
612
- (user_id, limit)
613
- ).fetchall()
614
-
615
- return [dict(article) for article in articles]
616
-
617
- except Exception as e:
618
- logger.error(f"Error getting user articles: {e}")
619
- return []
620
-
621
- def update_user_interests(user_id, interests):
622
- """Update user's interests/categories"""
623
- try:
624
- db = get_db()
625
-
626
- # Clear existing interests
627
- db.execute('DELETE FROM user_interests WHERE user_id = ?', (user_id,))
628
-
629
- # Add new interests
630
- for interest, weight in interests.items():
631
- db.execute(
632
- '''INSERT INTO user_interests (user_id, interest_category, weight)
633
- VALUES (?, ?, ?)''',
634
- (user_id, interest, weight)
635
- )
636
-
637
- db.commit()
638
- return True
639
-
640
- except Exception as e:
641
- logger.error(f"Error updating user interests: {e}")
642
- return False
643
-
644
- def get_user_interests(user_id):
645
- """Get user's interests"""
646
- try:
647
- db = get_db()
648
- interests = db.execute(
649
- 'SELECT interest_category, weight FROM user_interests WHERE user_id = ?',
650
- (user_id,)
651
- ).fetchall()
652
-
653
- return {interest['interest_category']: interest['weight'] for interest in interests}
654
-
655
- except Exception as e:
656
- logger.error(f"Error getting user interests: {e}")
657
- return {}
658
-
659
- def create_study_plan(user_id, plan_name, target_level, current_level, objectives, weekly_hours=5):
660
- """Create new study plan"""
661
- try:
662
- db = get_db()
663
-
664
- cursor = db.execute(
665
- '''INSERT INTO study_plans
666
- (user_id, plan_name, target_level, current_level, objectives, weekly_hours)
667
- VALUES (?, ?, ?, ?, ?, ?)''',
668
- (user_id, plan_name, target_level, current_level, objectives, weekly_hours)
669
- )
670
- plan_id = cursor.lastrowid
671
- db.commit()
672
-
673
- logger.info(f"Study plan created for user {user_id}: {plan_name}")
674
- return {'success': True, 'plan_id': plan_id}
675
-
676
- except Exception as e:
677
- logger.error(f"Error creating study plan: {e}")
678
- return {'success': False, 'message': 'Failed to create study plan'}
679
-
680
- def get_user_study_plans(user_id):
681
- """Get user's study plans"""
682
- try:
683
- db = get_db()
684
- plans = db.execute(
685
- '''SELECT * FROM study_plans
686
- WHERE user_id = ?
687
- ORDER BY created_at DESC''',
688
- (user_id,)
689
- ).fetchall()
690
-
691
- return [dict(plan) for plan in plans]
692
-
693
- except Exception as e:
694
- logger.error(f"Error getting study plans: {e}")
695
- return []
696
-
697
- def add_study_activity(plan_id, activity_type, content_reference, scheduled_date, estimated_duration):
698
- """Add activity to study plan"""
699
- try:
700
- db = get_db()
701
-
702
- db.execute(
703
- '''INSERT INTO study_plan_activities
704
- (plan_id, activity_type, content_reference, scheduled_date, estimated_duration)
705
- VALUES (?, ?, ?, ?, ?)''',
706
- (plan_id, activity_type, content_reference, scheduled_date, estimated_duration)
707
- )
708
- db.commit()
709
- return True
710
-
711
- except Exception as e:
712
- logger.error(f"Error adding study activity: {e}")
713
- return False
714
-
715
- def get_study_activities(plan_id, date_range=None):
716
- """Get activities for study plan"""
717
- try:
718
- db = get_db()
719
-
720
- if date_range:
721
- start_date, end_date = date_range
722
- activities = db.execute(
723
- '''SELECT * FROM study_plan_activities
724
- WHERE plan_id = ? AND scheduled_date BETWEEN ? AND ?
725
- ORDER BY scheduled_date''',
726
- (plan_id, start_date, end_date)
727
- ).fetchall()
728
- else:
729
- activities = db.execute(
730
- '''SELECT * FROM study_plan_activities
731
- WHERE plan_id = ?
732
- ORDER BY scheduled_date''',
733
- (plan_id,)
734
- ).fetchall()
735
-
736
- return [dict(activity) for activity in activities]
737
-
738
- except Exception as e:
739
- logger.error(f"Error getting study activities: {e}")
740
- return []
741
-
742
- def record_analytics_metric(user_id, metric_name, metric_value, context_data=None):
743
- """Record analytics metric"""
744
- try:
745
- db = get_db()
746
-
747
- db.execute(
748
- '''INSERT INTO user_analytics (user_id, metric_name, metric_value, metric_date, context_data)
749
- VALUES (?, ?, ?, DATE('now'), ?)''',
750
- (user_id, metric_name, metric_value, context_data)
751
- )
752
- db.commit()
753
- return True
754
-
755
- except Exception as e:
756
- logger.error(f"Error recording analytics: {e}")
757
- return False
758
-
759
- def get_user_analytics(user_id, metric_name=None, days=30):
760
- """Get user analytics data"""
761
- try:
762
- db = get_db()
763
-
764
- if metric_name:
765
- analytics = db.execute(
766
- '''SELECT * FROM user_analytics
767
- WHERE user_id = ? AND metric_name = ?
768
- AND metric_date >= DATE('now', '-{} days')
769
- ORDER BY metric_date DESC'''.format(days),
770
- (user_id, metric_name)
771
- ).fetchall()
772
- else:
773
- analytics = db.execute(
774
- '''SELECT * FROM user_analytics
775
- WHERE user_id = ?
776
- AND metric_date >= DATE('now', '-{} days')
777
- ORDER BY metric_date DESC'''.format(days),
778
- (user_id,)
779
- ).fetchall()
780
-
781
- return [dict(metric) for metric in analytics]
782
-
783
- except Exception as e:
784
- logger.error(f"Error getting analytics: {e}")
785
- return []