# Migrate questions from file to SQLite if not already present def migrate_questions_to_sqlite(): import sqlite3 if not os.path.exists(QUESTIONS_FILE): return conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute('''CREATE TABLE IF NOT EXISTS questions ( id INTEGER PRIMARY KEY, category TEXT, question TEXT, answer1 TEXT, answer2 TEXT, answer3 TEXT, correct_index INTEGER )''') # Only import if table is empty c.execute('SELECT COUNT(*) FROM questions') if c.fetchone()[0] == 0: with open(QUESTIONS_FILE) as f: for line in f: parts = [p.strip() for p in line.strip().split('|')] if len(parts) == 7: # category|question|a1|a2|a3|correct|explanation category, question = parts[0], parts[1] answers = parts[2:5] correct_index = int(parts[5]) explanation = parts[6] elif len(parts) == 6: # question|a1|a2|a3|correct|explanation (no category) category = "General" question = parts[0] answers = parts[1:4] correct_index = int(parts[4]) explanation = parts[5] else: continue while len(answers) < 3: answers.append("") # Add explanation as a new column if not exists try: c.execute('ALTER TABLE questions ADD COLUMN explanation TEXT') except Exception: pass c.execute('INSERT INTO questions (category, question, answer1, answer2, answer3, correct_index, explanation) VALUES (?, ?, ?, ?, ?, ?, ?)', (category, question, answers[0], answers[1], answers[2], correct_index, explanation)) conn.commit() conn.close() # Data migration from text files to SQLite def migrate_txt_to_sqlite(): migrated = False # Migrate XP, level, total_time xp = 0 level = 1 total_time = 0 if os.path.exists(XP_FILE): try: with open(XP_FILE) as f: xp = int(f.read().strip() or 0) except Exception: pass migrated = True if os.path.exists(LEVEL_FILE): try: with open(LEVEL_FILE) as f: level = int(f.read().strip() or 1) except Exception: pass migrated = True if os.path.exists(TOTAL_TIME_FILE): try: with open(TOTAL_TIME_FILE) as f: total_time = int(f.read().strip() or 0) except Exception: pass migrated = True # Save to SQLite if any file existed if migrated: stats = UserStatsSqlite(DB_PATH, SUBSCRIPTION_END_DATE) stats.save_stats(xp, level, total_time) # Migrate session log if os.path.exists(SESSION_LOG): stats = UserStatsSqlite(DB_PATH, SUBSCRIPTION_END_DATE) try: with open(SESSION_LOG) as f: for line in f: line = line.strip() if line: # Try to parse: Session start/end: YYYY-MM-DD HH:MM:SS import re m = re.match(r'Session (\w+): (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})', line) if m: status, timestamp = m.groups() stats.log_session(status, duration=None) except Exception: pass migrated = True # Remove text files if migrated: for f in [XP_FILE, LEVEL_FILE, TOTAL_TIME_FILE, SESSION_LOG]: try: os.remove(f) except Exception: pass import os import time import psutil import tkinter as tk from tkinter import messagebox from datetime import datetime, timedelta import webbrowser import random import sqlite3 TIMER_MINUTES = 30 # Default quiz timer in minutes # Constants def show_score_page(score_by_category, total_score): # Minimal implementation: show a message box with the score summary import tkinter as tk from tkinter import messagebox summary = f"Total Score: {total_score}\n" for cat, (correct, total) in score_by_category.items(): percent = (correct / total * 100) if total else 0 summary += f"{cat}: {correct}/{total} ({percent:.0f}%)\n" messagebox.showinfo("Quiz Complete", summary) SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) ASSETS_DIR = os.path.join(SCRIPT_DIR, 'assets') XP_FILE = os.path.join(ASSETS_DIR, 'xp.txt') LEVEL_FILE = os.path.join(ASSETS_DIR, 'level.txt') SESSION_LOG = os.path.join(ASSETS_DIR, 'session.log') TOTAL_TIME_FILE = os.path.join(ASSETS_DIR, 'total_time.txt') QUESTIONS_FILE = os.path.join(ASSETS_DIR, 'questions.txt') QUESTIONS_SHUFFLED_FILE = os.path.join(ASSETS_DIR, 'questions_shuffled.txt') DB_PATH = os.path.join(SCRIPT_DIR, 'rhel_learning.db') import subprocess DAYS_LEFT_FROM_FIXED_DATE = 284 FIXED_START_DATE_STR = '2025-08-15' FIXED_START_DATE = datetime.strptime(FIXED_START_DATE_STR, '%Y-%m-%d') # Get current system date using 'date' command for reliability try: current_date_str = subprocess.check_output(['date', '+%Y-%m-%d']).decode().strip() CURRENT_DATE = datetime.strptime(current_date_str, '%Y-%m-%d') except Exception: CURRENT_DATE = datetime.now() SUBSCRIPTION_END_DATE = FIXED_START_DATE + timedelta(days=DAYS_LEFT_FROM_FIXED_DATE) def format_time(seconds): hours, remainder = divmod(seconds, 3600) minutes, seconds = divmod(remainder, 60) return f"{hours}h {minutes}m {seconds}s" # SQLite-based stats and session tracking class UserStatsSqlite: def __init__(self, db_path, subscription_end_date): self.db_path = db_path self.subscription_end_date = subscription_end_date self._ensure_tables() self._ensure_user() def _connect(self): return sqlite3.connect(self.db_path) def _ensure_tables(self): with self._connect() as conn: c = conn.cursor() c.execute('''CREATE TABLE IF NOT EXISTS user_stats ( id INTEGER PRIMARY KEY, xp INTEGER NOT NULL, level INTEGER NOT NULL, total_time INTEGER NOT NULL )''') c.execute('''CREATE TABLE IF NOT EXISTS session_log ( id INTEGER PRIMARY KEY, status TEXT, timestamp TEXT, duration INTEGER )''') conn.commit() def _ensure_user(self): with self._connect() as conn: c = conn.cursor() c.execute('SELECT COUNT(*) FROM user_stats') if c.fetchone()[0] == 0: c.execute('INSERT INTO user_stats (xp, level, total_time) VALUES (?, ?, ?)', (0, 1, 0)) conn.commit() def get_stats(self): with self._connect() as conn: c = conn.cursor() c.execute('SELECT xp, level, total_time FROM user_stats LIMIT 1') return c.fetchone() def save_stats(self, xp, level, total_time): with self._connect() as conn: c = conn.cursor() c.execute('UPDATE user_stats SET xp=?, level=?, total_time=? WHERE id=1', (xp, level, total_time)) conn.commit() def add_xp(self, amount): xp, level, total_time = self.get_stats() xp += amount leveled_up = False if xp >= level * 100: xp -= level * 100 level += 1 leveled_up = True self.save_stats(xp, level, total_time) return xp, level, leveled_up def add_time(self, seconds): xp, level, total_time = self.get_stats() total_time += seconds self.save_stats(xp, level, total_time) def days_left(self): from datetime import datetime today = datetime.now() return (self.subscription_end_date - today).days def log_session(self, status, duration=None): import time with self._connect() as conn: c = conn.cursor() c.execute('INSERT INTO session_log (status, timestamp, duration) VALUES (?, ?, ?)', (status, time.strftime('%Y-%m-%d %H:%M:%S'), duration)) conn.commit() def get_total_time(self): _, _, total_time = self.get_stats() return total_time def get_xp_level(self): xp, level, _ = self.get_stats() return xp, level import os import time import psutil import tkinter as tk from tkinter import messagebox # Utility Functions def initialize_assets(): """Ensure all necessary files and directories exist.""" os.makedirs(ASSETS_DIR, exist_ok=True) for file_path, default_content in [ (XP_FILE, '0'), (LEVEL_FILE, '1'), (SESSION_LOG, ''), (TOTAL_TIME_FILE, '0'), ]: if not os.path.exists(file_path): with open(file_path, 'w') as f: f.write(default_content) if not os.path.exists(QUESTIONS_FILE): raise FileNotFoundError(f"Questions file not found: {QUESTIONS_FILE}") def log_session(status): """Log session start or end with a timestamp.""" with open(SESSION_LOG, 'a') as f: f.write(f"Session {status}: {time.strftime('%Y-%m-%d %H:%M:%S')}\n") def display_summary(): """Display a summary of total time, XP, and level.""" stats = UserStatsSqlite(DB_PATH, SUBSCRIPTION_END_DATE) print(f"Welcome to the RHEL Learning Script!") total_time = stats.get_total_time() xp, level = stats.get_xp_level() print(f"Total session time: {format_time(total_time)}") print(f"Current Level: {level}") print(f"Current XP: {xp}") print(f"Days left: {stats.days_left()}") print() return total_time, xp, level def kill_steam(): """Kill Steam only before the session starts.""" for proc in psutil.process_iter(['name']): if proc.info['name'] and "steam" in proc.info['name'].lower(): print("Stopping Steam...") proc.kill() def update_xp(correct): """Update XP and level based on whether the question was answered correctly.""" stats = UserStatsSqlite(DB_PATH, SUBSCRIPTION_END_DATE) if correct: xp, level, leveled_up = stats.add_xp(10) else: xp, level = stats.get_xp_level() xp = max(0, xp - 5) stats.save_stats(xp, level, stats.get_total_time()) leveled_up = False if correct and leveled_up: messagebox.showinfo("LEVEL UP!", f"LEVEL UP! You are now Level {level}!") with open(SESSION_LOG, 'a') as f: f.write(f"XP and level updated: {xp} XP, Level {level}\n") return xp, level def show_timer(minutes, questions_file, session_log, total_time, xp, level, root): # Initialize timer variables BEFORE any inner function definitions seconds_left = minutes * 60 elapsed_time = 0 def update_timer(): nonlocal seconds_left, elapsed_time if seconds_left > 0: seconds_left -= 1 elapsed_time += 1 hours, remainder = divmod(seconds_left, 3600) minutes_left, seconds = divmod(remainder, 60) # Only update timer_label if it still exists if timer_label.winfo_exists(): timer_label.config(text=f"Time left: {hours:02}:{minutes_left:02}:{seconds:02}") root.after(1000, update_timer) else: end_session() def update_rol_timer(): today = datetime.now() days_left = (SUBSCRIPTION_END_DATE - today).days if rol_label.winfo_exists(): if days_left > 0: rol_label.config(text=f"ROL Subscription: {days_left} days left") root.after(60000, update_rol_timer) # Update every minute else: rol_label.config(text="ROL Subscription expired!") messagebox.showwarning("Subscription Alert", "Your ROL Subscription has expired!") def end_session(): with open(session_log, 'a') as f: f.write(f"Session duration: {elapsed_time // 60} minutes and {elapsed_time % 60} seconds\n") with open(TOTAL_TIME_FILE, 'w') as f: f.write(str(total_time + elapsed_time)) root.destroy() # Function body starts here conn = sqlite3.connect(DB_PATH) c = conn.cursor() # Try to fetch explanation if present try: c.execute('SELECT id, category, question, answer1, answer2, answer3, correct_index, explanation FROM questions') rows = c.fetchall() has_explanation = True except Exception: c.execute('SELECT id, category, question, answer1, answer2, answer3, correct_index FROM questions') rows = c.fetchall() has_explanation = False conn.close() if not rows: messagebox.showerror("Error", "No questions found in database. Please populate your questions file.") root.destroy() return # Convert to lines format for compatibility lines = [] for row in rows: # Compose as: Category|Question|A|B|C|CorrectIndex|Explanation (if present) if has_explanation: lines.append(f"{row[1]}|{row[2]}|{row[3]}|{row[4]}|{row[5]}|{row[6]}|{row[7]}") else: lines.append(f"{row[1]}|{row[2]}|{row[3]}|{row[4]}|{row[5]}|{row[6]}") # Track per-category score score_by_category = {} total_correct = 0 total_questions = 0 max_score = 300 # Shuffle and use each question only once per session random.shuffle(lines) questions_to_ask = lines.copy() # Create a frame for question/answer widgets ONCE question_frame = tk.Frame(root) question_frame.pack(pady=10) # Add a label to show per-category progress progress_label = tk.Label(root, text="", font=("Helvetica", 10), fg="blue") progress_label.pack(pady=2) def update_progress_label(): # Always show progress for each category, even if empty progress_items = [] all_cats = set(score_by_category.keys()) for q in lines: cat = q.split('|')[0] if q.split('|')[0] else "General" all_cats.add(cat) for cat in sorted(all_cats): correct, total = score_by_category.get(cat, [0, 0]) progress_items.append(f"{cat[:3].upper()}:{correct}/{total}") # Break into multiple lines if too many categories max_per_line = 7 lines_out = [" | ".join(progress_items[i:i+max_per_line]) for i in range(0, len(progress_items), max_per_line)] progress_label.config(text="\n".join(lines_out)) def ask_next_question(): nonlocal total_questions, total_correct if not questions_to_ask: # Show score page at end total_score = int((total_correct / total_questions) * max_score) if total_questions else 0 root.destroy() show_score_page(score_by_category, total_score) return qline = questions_to_ask.pop(0) parts = qline.split('|') if len(parts) < 5: ask_next_question() return # Assume format: Category|Question|A|B|C|CorrectIndex|Explanation (optional) category = parts[0] if parts[0] else "General" question_text = parts[1] answers = parts[2:-1] if len(parts) < 7 else parts[2:-2] correct_idx = int(parts[-2]) - 1 if len(parts) >= 7 else int(parts[-1]) - 1 explanation = parts[-1] if len(parts) >= 7 else None # Build a list of (text, is_correct) and shuffle opts = [] for idx, ans in enumerate(answers): if ans.strip(): opts.append({'text': ans, 'correct': (idx == correct_idx)}) random.shuffle(opts) # Clear previous question/answer widgets only for widget in question_frame.winfo_children(): widget.destroy() # Show only the question text (not category) question_label = tk.Label(question_frame, text=question_text, font=("Helvetica", 14)) question_label.pack(pady=10) answer_var = tk.StringVar(value="") radio_buttons = [] for opt in opts: rb = tk.Radiobutton(question_frame, text=opt['text'], variable=answer_var, value=opt['text'], font=("Helvetica", 12)) rb.pack(anchor='w') radio_buttons.append(rb) def check_answer(): nonlocal total_questions, total_correct sel = answer_var.get() correct = any(o['text'] == sel and o['correct'] for o in opts) # Find the correct answer correct_answer = None for o in opts: if o['correct']: correct_answer = o['text'] break # Use explanation if present, else fallback if explanation: detail = explanation else: detail = f"The correct answer is: {correct_answer}" if correct: messagebox.showinfo("Correct!", f"You chose the right answer.\n{detail}") new_xp, new_level = update_xp(True) score_by_category.setdefault(category, [0, 0])[0] += 1 total_correct += 1 score_by_category.setdefault(category, [0, 0])[1] += 1 total_questions += 1 for b in radio_buttons: b.config(state=tk.DISABLED) xp_label.config(text=f"XP: {new_xp}") level_label.config(text=f"Level: {new_level}") update_progress_label() root.after(500, ask_next_question) else: new_xp, new_level = update_xp(False) score_by_category.setdefault(category, [0, 0])[1] += 1 total_questions += 1 xp_label.config(text=f"XP: {new_xp}") level_label.config(text=f"Level: {new_level}") update_progress_label() # Show explanation, then immediately go to next question after dialog closes def after_incorrect(): ask_next_question() messagebox.showinfo("Incorrect", f"Sorry, that's not correct.\n{detail}") root.after(10, after_incorrect) submit_btn = tk.Button(question_frame, text="Submit", command=check_answer) submit_btn.pack(pady=5) stats = UserStatsSqlite(DB_PATH, SUBSCRIPTION_END_DATE) timer_label = tk.Label(root, text=f"Time left: {minutes:02}:00:00", font=("Helvetica", 16)) timer_label.pack(pady=20) rol_label = tk.Label(root, text=f"ROL Subscription: {stats.days_left()} days left", font=("Helvetica", 14), fg="red") rol_label.pack(pady=5) xp_val, level_val = stats.get_xp_level() xp_label = tk.Label(root, text=f"XP: {xp_val}", font=("Helvetica", 14)) xp_label.pack(pady=5) level_label = tk.Label(root, text=f"Level: {level_val}", font=("Helvetica", 14)) level_label.pack(pady=5) total_time_label = tk.Label(root, text=f"Total session time: {format_time(stats.get_total_time())}", font=("Helvetica", 14)) total_time_label.pack(pady=5) def update_total_time_label(): nonlocal elapsed_time # Show total time as (previous total + elapsed this session) current_total = stats.get_total_time() + elapsed_time total_time_label.config(text=f"Total session time: {format_time(current_total)}") root.after(1000, update_total_time_label) # Start the first question and live-updating total time update_progress_label() update_total_time_label() ask_next_question() def periodic_kill_steam(): kill_steam() root.after(10000, periodic_kill_steam) root.after(1000, update_timer) root.after(1000, update_rol_timer) root.after(10000, periodic_kill_steam) ## Removed open_firefox, use open_learning_links instead def shuffle_questions(input_file, output_file): """Shuffle questions and answers, updating the correct answer index.""" with open(input_file) as f: lines = [l.strip() for l in f if l.strip()] with open(output_file, 'w') as f: for line in lines: parts = line.split('|') # Handle both with and without category if len(parts) == 6: # category|question|a1|a2|a3|correct category, question = parts[0], parts[1] answers = parts[2:5] correct = int(parts[5]) elif len(parts) == 5: # question|a1|a2|a3|correct (no category) category = "General" question = parts[0] answers = parts[1:4] correct = int(parts[4]) else: continue # skip malformed zipped = list(zip(answers, range(1, len(answers)+1))) random.shuffle(zipped) new_answers, old_indices = zip(*zipped) new_correct = old_indices.index(correct) + 1 f.write(f"{category}|{question}|{'|'.join(new_answers)}|{new_correct}\n") def open_learning_links(): urls = [ "https://rol.redhat.com", "https://docs.google.com/spreadsheets/d/1ZQpE1fQoc-lyElFmvoQLW-2tOclMsMENYKiehu3jrxc/edit?gid=0#gid=0" ] for url in urls: webbrowser.open_new_tab(url) def main(): migrate_txt_to_sqlite() migrate_questions_to_sqlite() initialize_assets() log_session("start") total_time, xp, level = display_summary() kill_steam() # open_firefox removed; open_learning_links is used instead # Always open learning links on startup open_learning_links() root = tk.Tk() root.title("RHEL Learning Timer") def on_closing(): log_session("end") root.destroy() root.protocol("WM_DELETE_WINDOW", on_closing) try: show_timer(TIMER_MINUTES, QUESTIONS_SHUFFLED_FILE, SESSION_LOG, total_time, xp, level, root) root.mainloop() except Exception as e: messagebox.showerror("Error", f"An error occurred: {e}") log_session("end") if __name__ == "__main__": shuffle_questions(QUESTIONS_FILE, QUESTIONS_SHUFFLED_FILE) main()