File size: 13,757 Bytes
3301039
 
 
dc2f1a6
3301039
3388cfb
3301039
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a9d57ac
 
54147e8
 
a9d57ac
3301039
 
54147e8
 
 
 
 
3301039
 
 
 
 
 
 
 
 
 
 
 
3388cfb
 
 
 
 
 
 
 
 
 
 
3301039
 
 
 
 
 
8e628bd
3301039
 
 
8e628bd
 
 
 
 
 
 
 
 
 
3301039
 
 
 
dc2f1a6
 
 
 
 
 
 
 
 
 
 
 
3301039
 
 
 
 
 
dc2f1a6
 
 
 
3301039
 
dc2f1a6
3301039
 
 
 
 
 
 
 
 
 
 
 
dc2f1a6
 
 
 
 
 
3301039
 
 
 
 
ae3f866
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3301039
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54147e8
3301039
 
 
 
54147e8
 
3301039
 
 
 
 
 
 
 
 
 
 
 
54147e8
3301039
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54147e8
3301039
 
 
 
54147e8
 
3301039
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3388cfb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
import pymysql
import json
import os
import time
from dotenv import load_dotenv
from werkzeug.security import generate_password_hash, check_password_hash

load_dotenv(override=True)

DB_HOST = os.environ.get("DB_HOST", "localhost")
DB_USER = os.environ.get("DB_USER", "root")
DB_PASS = os.environ.get("DB_PASS", "")
DB_NAME = os.environ.get("DB_NAME", "vorniity")

def get_db_connection():
    return pymysql.connect(
        host=DB_HOST,
        user=DB_USER,
        password=DB_PASS,
        database=DB_NAME,
        cursorclass=pymysql.cursors.DictCursor
    )

def init_db():
    """Initialize the MySQL database for caching results."""
    try:
        conn = get_db_connection()
        with conn.cursor() as cursor:
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS results_cache_v2 (
                    usn VARCHAR(20),
                    url VARCHAR(255),
                    data JSON,
                    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                    PRIMARY KEY (usn, url)
                )
            ''')
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS subject_credits (
                    subject_code VARCHAR(50) PRIMARY KEY,
                    credits INT NOT NULL
                )
            ''')
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS classes (
                    id INT AUTO_INCREMENT PRIMARY KEY,
                    name VARCHAR(255) NOT NULL,
                    start_usn VARCHAR(20) NULL,
                    end_usn VARCHAR(20) NULL,
                    lateral_start VARCHAR(20) NULL,
                    lateral_end VARCHAR(20) NULL,
                    usn_list MEDIUMTEXT NULL
                )
            ''')
            try:
                cursor.execute("ALTER TABLE classes ADD COLUMN lateral_start VARCHAR(20) NULL AFTER end_usn")
                cursor.execute("ALTER TABLE classes ADD COLUMN lateral_end VARCHAR(20) NULL AFTER lateral_start")
            except Exception:
                pass
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS scrape_history (
                    id VARCHAR(255) PRIMARY KEY,
                    start_usn VARCHAR(20),
                    end_usn VARCHAR(20),
                    total_usns INT,
                    completed INT,
                    time_taken FLOAT,
                    status VARCHAR(50),
                    timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
            ''')
            cursor.execute('''
                CREATE TABLE IF NOT EXISTS users (
                    id INT AUTO_INCREMENT PRIMARY KEY,
                    name VARCHAR(255) NOT NULL,
                    college VARCHAR(255) NOT NULL,
                    email VARCHAR(255) UNIQUE NOT NULL,
                    phone VARCHAR(50),
                    password_hash VARCHAR(255) NOT NULL,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
            ''')
        conn.commit()
        conn.close()
    except Exception as e:
        print(f"[DB ERROR] Failed to initialize DB: {e}")

def seed_credits_if_empty(hardcoded_map):
    """Ensure hardcoded subject credits exist in the DB without overwriting user changes."""
    try:
        conn = get_db_connection()
        with conn.cursor() as cursor:
            for code, credit in hardcoded_map.items():
                cursor.execute(
                    """
                    INSERT INTO subject_credits (subject_code, credits) 
                    VALUES (%s, %s)
                    ON DUPLICATE KEY UPDATE credits = IF(credits = 0, VALUES(credits), credits)
                    """,
                    (code, credit)
                )
            conn.commit()
        conn.close()
    except Exception as e:
        print(f"[DB ERROR] Failed to seed credits: {e}")

_CREDITS_CACHE = {}
_CREDITS_LAST_FETCH = 0
_CACHE_TTL = 300 # 5 minutes cache to prevent Hostinger max_connections_per_hour (500) errors

def get_all_credits(force_refresh=False):
    """Retrieve all subject credits as a dictionary with caching."""
    global _CREDITS_CACHE, _CREDITS_LAST_FETCH
    
    current_time = time.time()
    if not force_refresh and _CREDITS_CACHE and (current_time - _CREDITS_LAST_FETCH < _CACHE_TTL):
        return _CREDITS_CACHE

    try:
        conn = get_db_connection()
        with conn.cursor() as cursor:
            cursor.execute("SELECT subject_code, credits FROM subject_credits")
            rows = cursor.fetchall()
        conn.close()
        
        _CREDITS_CACHE = {row['subject_code']: row['credits'] for row in rows}
        _CREDITS_LAST_FETCH = current_time
        return _CREDITS_CACHE
    except Exception as e:
        print(f"[DB ERROR] Failed to fetch credits: {e}")
        return _CREDITS_CACHE if _CREDITS_CACHE else {}

def save_credit(subject_code, credits):
    """Save or update a subject credit."""
    try:
        conn = get_db_connection()
        with conn.cursor() as cursor:
            cursor.execute(
                "REPLACE INTO subject_credits (subject_code, credits) VALUES (%s, %s)",
                (subject_code.upper(), int(credits))
            )
        conn.commit()
        conn.close()
        
        # Instantly update cache
        global _CREDITS_CACHE
        if _CREDITS_CACHE is not None:
            _CREDITS_CACHE[subject_code.upper()] = int(credits)
            
        return True
    except Exception as e:
        print(f"[DB ERROR] Failed to save credit: {e}")
        return False

def save_bulk_credits(credits_list):
    """Save multiple subject credits efficiently."""
    try:
        conn = get_db_connection()
        with conn.cursor() as cursor:
            # credits_list is a list of dicts: [{'subject_code': 'ABC', 'credits': 3}, ...]
            values = [(item['subject_code'].upper(), int(item['credits'])) for item in credits_list]
            cursor.executemany(
                "REPLACE INTO subject_credits (subject_code, credits) VALUES (%s, %s)",
                values
            )
        conn.commit()
        conn.close()
        
        # Invalidate cache so next fetch gets everything
        global _CREDITS_CACHE
        _CREDITS_CACHE = None
        
        return True
    except Exception as e:
        print(f"[DB ERROR] Failed to save bulk credits: {e}")
        return False

def delete_credit(subject_code):
    """Delete a subject credit."""
    try:
        conn = get_db_connection()
        with conn.cursor() as cursor:
            cursor.execute("DELETE FROM subject_credits WHERE subject_code = %s", (subject_code.upper(),))
        conn.commit()
        conn.close()
        return True
    except Exception as e:
        print(f"[DB ERROR] Failed to delete credit: {e}")
        return False

# --- Classes Logic ---

def create_class(name, start_usn=None, end_usn=None, lateral_start=None, lateral_end=None, usn_list=None):
    try:
        conn = get_db_connection()
        with conn.cursor() as cursor:
            cursor.execute(
                "INSERT INTO classes (name, start_usn, end_usn, lateral_start, lateral_end, usn_list) VALUES (%s, %s, %s, %s, %s, %s)",
                (name, start_usn.upper() if start_usn else None, end_usn.upper() if end_usn else None, lateral_start.upper() if lateral_start else None, lateral_end.upper() if lateral_end else None, usn_list)
            )
        conn.commit()
        conn.close()
        return True
    except Exception as e:
        print(f"[DB ERROR] Failed to create class: {e}")
        return False

def get_all_classes():
    try:
        conn = get_db_connection()
        with conn.cursor() as cursor:
            cursor.execute("SELECT id, name, start_usn, end_usn, lateral_start, lateral_end, usn_list FROM classes ORDER BY id DESC")
            rows = cursor.fetchall()
        conn.close()
        return rows
    except Exception as e:
        print(f"[DB ERROR] Failed to fetch classes: {e}")
        return []

def delete_class(class_id):
    try:
        conn = get_db_connection()
        with conn.cursor() as cursor:
            cursor.execute("DELETE FROM classes WHERE id = %s", (class_id,))
        conn.commit()
        conn.close()
        return True
    except Exception as e:
        print(f"[DB ERROR] Failed to delete class: {e}")
        return False

def update_class(class_id, name, start_usn=None, end_usn=None, lateral_start=None, lateral_end=None, usn_list=None):
    try:
        conn = get_db_connection()
        with conn.cursor() as cursor:
            cursor.execute(
                "UPDATE classes SET name = %s, start_usn = %s, end_usn = %s, lateral_start = %s, lateral_end = %s, usn_list = %s WHERE id = %s",
                (name, start_usn.upper() if start_usn else None, end_usn.upper() if end_usn else None, lateral_start.upper() if lateral_start else None, lateral_end.upper() if lateral_end else None, usn_list, class_id)
            )
        conn.commit()
        conn.close()
        return True
    except Exception as e:
        print(f"[DB ERROR] Failed to update class: {e}")
        return False

# --- Scrape History Logic ---

def save_scrape_history(job_id, start_usn, end_usn, total_usns, completed, time_taken, status):
    try:
        conn = get_db_connection()
        with conn.cursor() as cursor:
            cursor.execute(
                "REPLACE INTO scrape_history (id, start_usn, end_usn, total_usns, completed, time_taken, status) VALUES (%s, %s, %s, %s, %s, %s, %s)",
                (job_id, start_usn, end_usn, total_usns, completed, time_taken, status)
            )
        conn.commit()
        conn.close()
        return True
    except Exception as e:
        print(f"[DB ERROR] Failed to save scrape history: {e}")
        return False

def get_scrape_history():
    try:
        conn = get_db_connection()
        with conn.cursor() as cursor:
            cursor.execute("SELECT id, start_usn, end_usn, total_usns, completed, time_taken, status, timestamp FROM scrape_history ORDER BY timestamp DESC")
            rows = cursor.fetchall()
        conn.close()
        
        # Format timestamp to ISO string
        for row in rows:
            if row['timestamp']:
                row['timestamp'] = row['timestamp'].isoformat()
        return rows
    except Exception as e:
        print(f"[DB ERROR] Failed to fetch scrape history: {e}")
        return []

def delete_student(usn):
    try:
        conn = get_db_connection()
        with conn.cursor() as cursor:
            cursor.execute("DELETE FROM results_cache_v2 WHERE usn = %s", (usn.upper(),))
        conn.commit()
        conn.close()
        return True
    except Exception as e:
        print(f"[DB ERROR] Failed to delete student: {e}")
        return False

def get_cached_result(usn, url):
    """Retrieve a cached result for a given USN and URL if it exists."""
    try:
        conn = get_db_connection()
        with conn.cursor() as cursor:
            cursor.execute("SELECT data FROM results_cache_v2 WHERE usn = %s AND url = %s", (usn.upper(), url))
            row = cursor.fetchone()
        conn.close()
        
        if row:
            if isinstance(row['data'], str):
                return json.loads(row['data'])
            return row['data']
    except Exception as e:
        print(f"[CACHE ERROR] Failed to read cache for {usn}: {e}")
    return None

def save_cached_result(usn, url, result_dict):
    """Save a successfully parsed result to the cache."""
    try:
        conn = get_db_connection()
        with conn.cursor() as cursor:
            # REPLACE INTO handles updating the record if it already exists
            cursor.execute(
                "REPLACE INTO results_cache_v2 (usn, url, data) VALUES (%s, %s, %s)",
                (usn.upper(), url, json.dumps(result_dict))
            )
        conn.commit()
        conn.close()
    except Exception as e:
        print(f"[CACHE ERROR] Failed to save cache for {usn}: {e}")

def clear_database():
    """Wipe all student scraped data, results, and history."""
    try:
        conn = get_db_connection()
        with conn.cursor() as cursor:
            cursor.execute("SET FOREIGN_KEY_CHECKS = 0;")
            cursor.execute("TRUNCATE TABLE results_cache_v2;")
            cursor.execute("TRUNCATE TABLE scrape_history;")
            cursor.execute("SET FOREIGN_KEY_CHECKS = 1;")
        conn.commit()
        conn.close()
        return True, "Database cleared successfully"
    except Exception as e:
        print(f"[DB ERROR] Failed to clear database: {e}")
        return False, str(e)

# --- Auth Logic ---
def create_user(name, college, email, phone, password):
    try:
        conn = get_db_connection()
        password_hash = generate_password_hash(password)
        with conn.cursor() as cursor:
            cursor.execute(
                "INSERT INTO users (name, college, email, phone, password_hash) VALUES (%s, %s, %s, %s, %s)",
                (name, college, email, phone, password_hash)
            )
        conn.commit()
        conn.close()
        return True, "User created successfully"
    except pymysql.err.IntegrityError:
        return False, "Email already exists"
    except Exception as e:
        print(f"[DB ERROR] Failed to create user: {e}")
        return False, str(e)

def get_user_by_email(email):
    try:
        conn = get_db_connection()
        with conn.cursor() as cursor:
            cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
            user = cursor.fetchone()
        conn.close()
        return user
    except Exception as e:
        print(f"[DB ERROR] Failed to get user: {e}")
        return None