From c68baecb522ed59dfa1f8b9be22e0243bbc0606e Mon Sep 17 00:00:00 2001 From: alexpolo1 Date: Fri, 15 Aug 2025 10:20:02 +0200 Subject: [PATCH] Implement migration script from text files to SQLite database - Added `migrate_to_sqlite.py` to handle migration of user stats and session logs from text files to an SQLite database. - The script reads XP, level, and total time from respective text files and saves them into the `user_stats` table in the SQLite database. - Session logs are parsed and logged into the `session_log` table. - Existing text files are deleted after successful migration. - Created a new SQLite database file `rhel_learning.db` with tables for questions, user stats, and session logs. --- __pycache__/main.cpython-312.pyc | Bin 0 -> 25090 bytes assets/level.txt | 2 +- assets/questions.txt | 21 +- assets/questions_shuffled.txt | 58 ++-- assets/session.log | 459 +------------------------- assets/total_time.txt | 2 +- assets/xp.txt | 2 +- main.py | 540 +++++++++++++++++++++++-------- migrate_to_sqlite.py | 63 ++++ rhel_learning.db | Bin 0 -> 24576 bytes src/main.py | 139 -------- 11 files changed, 534 insertions(+), 752 deletions(-) create mode 100644 __pycache__/main.cpython-312.pyc create mode 100644 migrate_to_sqlite.py create mode 100644 rhel_learning.db delete mode 100644 src/main.py diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a49e4a47c086efcaa8caee6c00d03f2b5c24184e GIT binary patch literal 25090 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 D81nTd literal 0 HcmV?d00001 diff --git a/assets/level.txt b/assets/level.txt index e440e5c..56a6051 100644 --- a/assets/level.txt +++ b/assets/level.txt @@ -1 +1 @@ -3 \ No newline at end of file +1 \ No newline at end of file diff --git a/assets/questions.txt b/assets/questions.txt index 144859f..014b194 100644 --- a/assets/questions.txt +++ b/assets/questions.txt @@ -17,4 +17,23 @@ How do you display the current runlevel?|runlevel|who -r|systemctl get-default|1 How do you set a service to start at boot?|systemctl enable|systemctl start|systemctl boot|1 How do you transfer a file securely to a remote host?|scp file user@host:/path|ftp file user@host:/path|rsync file user@host:/path|1 How do you display the UUID of a filesystem?|blkid|lsblk|uuidgen|1 -How do you reload the systemd daemon?|systemctl daemon-reload|systemctl reload|systemctl restart|1 \ No newline at end of file +How do you reload the systemd daemon?|systemctl daemon-reload|systemctl reload|systemctl restart|1 +Networking|How do you set the hostname to mars.domain250.example.com?|hostnamectl set-hostname mars.domain250.example.com|echo "mars.domain250.example.com" > /etc/hostname|nmcli con mod hostname mars.domain250.example.com|1 +Networking|Which command sets the IP address, gateway, and DNS for enp0s3?|nmcli connection modify enp0s3 autoconnect yes ipv4.method manual ipv4.addresses 172.25.250.100 ipv4.gateway 172.25.250.254 ipv4.dns 172.25.250.254|ifconfig enp0s3 172.25.250.100 netmask 255.255.255.0|ip addr add 172.25.250.100/24 dev enp0s3|1 +SELinux|How do you allow httpd to serve content on port 82?|semanage port -a -t http_port_t -p tcp 82|firewall-cmd --add-port=82/tcp --permanent|setenforce 0|1 +User Management|Which command creates a user 'natasha' and adds to group 'sysmgrs'?|useradd natasha -G sysmgrs|adduser natasha sysmgrs|useradd -g sysmgrs natasha|1 +User Management|How do you create a user 'sarah' with no interactive shell?|useradd sarah --shell /sbin/nologin|useradd sarah -G nologin|adduser sarah --no-login|1 +User Management|How do you set the password for user 'natasha' to 'password'?|echo "password" | passwd --stdin natasha|passwd natasha password|usermod -p password natasha|1 +Scheduling|What is the correct crontab entry to log "EX200 in progress" every 2 min?|*/2 * * * * logger "EX200 in progress"|0 2 * * * logger "EX200 in progress"|2 * * * * logger "EX200 in progress"|1 +Permissions|How do you give user 'natasha' rw access to /var/tmp/fstab using ACL?|setfacl -m u:natasha:rw- /var/tmp/fstab|chmod 600 /var/tmp/fstab|chown natasha /var/tmp/fstab|1 +Permissions|How do you deny user 'harry' all access to /var/tmp/fstab using ACL?|setfacl -m u:harry:- /var/tmp/fstab|chmod 000 /var/tmp/fstab|chown nobody /var/tmp/fstab|1 +Permissions|How do you set group ownership of /home/managers to sysmgrs and setgid?|chown root:sysmgrs /home/managers && chmod g+s /home/managers|chmod 770 /home/managers|setfacl -m g:sysmgrs:rwx /home/managers|1 +NTP|How do you configure chrony to use materials.example.com as NTP server?|Edit /etc/chrony.conf and add 'server materials.example.com iburst'|systemctl enable ntpd|ntpdate materials.example.com|1 +Autofs|How do you configure autofs to mount remoteuser1's home from materials.example.com?|Add '/rhome /etc/auto.rhome' to /etc/auto.master and 'remoteuser1 -rw materials.example.com:/rhome/remoteuser1' to /etc/auto.rhome|mount -t nfs materials.example.com:/rhome/remoteuser1 /rhome/remoteuser1|autofs --mount /rhome/remoteuser1|1 +User Management|How do you create user 'manalo' with UID 3533?|useradd -u 3533 manalo|adduser manalo --uid=3533|useradd manalo -U 3533|1 +Archiving|How do you create a gzip archive of /usr/local as /root/backup.tar.gz?|tar -czvf /root/backup.tar.gz /usr/local|gzip /usr/local > /root/backup.tar.gz|tar -cvf /root/backup.tar.gz /usr/local|1 +Swap|How do you add a 512M swap partition and make it persistent?|Create partition, mkswap /dev/vdb3, add UUID to /etc/fstab|swapon /dev/vdb3|echo "/dev/vdb3 swap swap defaults 0 0" >> /etc/fstab|1 +LVM|How do you create a logical volume 'qa' of 60 PE in group 'qagroup' and format ext3?|lvcreate -n qa -l 60 qagroup && mkfs.ext3 /dev/qagroup/qa|lvcreate -n qa -L 60M qagroup && mkfs.ext3 /dev/qagroup/qa|lvcreate -n qa -l 60 qagroup && mkfs.ext4 /dev/qagroup/qa|1 +Permissions|How do you give natasha rw and harry no access to /var/tmp/fstab using ACL?|setfacl -m u:natasha:rw-,u:harry:- /var/tmp/fstab|chmod 600 /var/tmp/fstab|setfacl -m u:natasha:rw- /var/tmp/fstab|1 +Scripting|How do you find all files under /usr less than 10M with sgid and save to /root/myfile?|find /usr -size -10M -perm -2000 > /root/myfile|find /usr -size +10M -perm -2000 > /root/myfile|find /usr -size -10M -perm -4000 > /root/myfile|1 +Scripting|How do you create a script to find SUID files between 30k and 50k in /usr?|find /usr -size +30k -size -50k -perm /u=s > /root/newfiles|find /usr -size +30k -size -50k -perm /g=s > /root/newfiles|find /usr -size +30k -size -50k -perm /o=s > /root/newfiles|1 \ No newline at end of file diff --git a/assets/questions_shuffled.txt b/assets/questions_shuffled.txt index 4f796bb..e6e8080 100644 --- a/assets/questions_shuffled.txt +++ b/assets/questions_shuffled.txt @@ -1,20 +1,38 @@ -Which command creates a compressed tar archive?|gzip dir|tar czf archive.tar.gz dir|bzip2 dir|2 -How do you append output to a file in Bash?|>>|>|&>|1 -Which command finds all files named "passwd" under /etc?|find /etc -name passwd|ls /etc/passwd|grep passwd /etc|1 -How do you switch to the root user?|sudo root|sudo su -|su -|3 -Which command shows the SELinux context of a file?|ls --context|ls -Z|ls -l|2 -How do you create a new user?|usermod|adduser|useradd|3 -Which command lists all running containers?|docker ps|podman ps|podman list|2 -How do you mount an NFS share?|mount -t ext4 /dev/sda1 /mnt|mount -t nfs server:/share /mnt|mount -o loop image.iso /mnt|2 -How do you schedule a one-time job for 5 minutes from now?|at now + 5 minutes|cron 5|crontab -e|1 -Which command sets a file's permissions to rwxr-xr--?|chmod 644 file|chmod 754 file|chmod 777 file|2 -How do you check the status of firewalld?|systemctl firewall|firewall-cmd --state|firewallctl status|2 -How do you extend a logical volume by 1G?|lvresize -L 1G /dev/vg/lv|lvcreate -L 1G /dev/vg/lv|lvextend -L +1G /dev/vg/lv|3 -How do you display the last 10 lines of a file?|less file|tail file|head file|2 -How do you change a user's password aging policy?|usermod|chage|passwd|2 -How do you list all available systemd targets?|systemctl list-units --type=target --all|systemctl list-active-targets|systemctl list-targets|3 -How do you display the current runlevel?|runlevel|who -r|systemctl get-default|1 -How do you set a service to start at boot?|systemctl start|systemctl boot|systemctl enable|3 -How do you transfer a file securely to a remote host?|scp file user@host:/path|rsync file user@host:/path|ftp file user@host:/path|1 -How do you display the UUID of a filesystem?|uuidgen|blkid|lsblk|2 -How do you reload the systemd daemon?|systemctl reload|systemctl restart|systemctl daemon-reload|3 +General|Which command creates a compressed tar archive?|bzip2 dir|gzip dir|tar czf archive.tar.gz dir|3 +General|How do you append output to a file in Bash?|>>|>|&>|1 +General|Which command finds all files named "passwd" under /etc?|find /etc -name passwd|ls /etc/passwd|grep passwd /etc|1 +General|How do you switch to the root user?|sudo root|sudo su -|su -|3 +General|Which command shows the SELinux context of a file?|ls -l|ls -Z|ls --context|2 +General|How do you create a new user?|useradd|usermod|adduser|1 +General|Which command lists all running containers?|docker ps|podman ps|podman list|2 +General|How do you mount an NFS share?|mount -t nfs server:/share /mnt|mount -o loop image.iso /mnt|mount -t ext4 /dev/sda1 /mnt|1 +General|How do you schedule a one-time job for 5 minutes from now?|at now + 5 minutes|cron 5|crontab -e|1 +General|Which command sets a file's permissions to rwxr-xr--?|chmod 777 file|chmod 754 file|chmod 644 file|2 +General|How do you check the status of firewalld?|firewall-cmd --state|systemctl firewall|firewallctl status|1 +General|How do you extend a logical volume by 1G?|lvresize -L 1G /dev/vg/lv|lvextend -L +1G /dev/vg/lv|lvcreate -L 1G /dev/vg/lv|2 +General|How do you display the last 10 lines of a file?|tail file|head file|less file|1 +General|How do you change a user's password aging policy?|usermod|passwd|chage|3 +General|How do you list all available systemd targets?|systemctl list-active-targets|systemctl list-units --type=target --all|systemctl list-targets|3 +General|How do you display the current runlevel?|runlevel|who -r|systemctl get-default|1 +General|How do you set a service to start at boot?|systemctl boot|systemctl start|systemctl enable|3 +General|How do you transfer a file securely to a remote host?|scp file user@host:/path|ftp file user@host:/path|rsync file user@host:/path|1 +General|How do you display the UUID of a filesystem?|blkid|lsblk|uuidgen|1 +General|How do you reload the systemd daemon?|systemctl reload|systemctl daemon-reload|systemctl restart|2 +Networking|How do you set the hostname to mars.domain250.example.com?|hostnamectl set-hostname mars.domain250.example.com|nmcli con mod hostname mars.domain250.example.com|echo "mars.domain250.example.com" > /etc/hostname|1 +Networking|Which command sets the IP address, gateway, and DNS for enp0s3?|ifconfig enp0s3 172.25.250.100 netmask 255.255.255.0|ip addr add 172.25.250.100/24 dev enp0s3|nmcli connection modify enp0s3 autoconnect yes ipv4.method manual ipv4.addresses 172.25.250.100 ipv4.gateway 172.25.250.254 ipv4.dns 172.25.250.254|3 +SELinux|How do you allow httpd to serve content on port 82?|semanage port -a -t http_port_t -p tcp 82|firewall-cmd --add-port=82/tcp --permanent|setenforce 0|1 +User Management|Which command creates a user 'natasha' and adds to group 'sysmgrs'?|useradd natasha -G sysmgrs|useradd -g sysmgrs natasha|adduser natasha sysmgrs|1 +User Management|How do you create a user 'sarah' with no interactive shell?|useradd sarah -G nologin|adduser sarah --no-login|useradd sarah --shell /sbin/nologin|3 +Scheduling|What is the correct crontab entry to log "EX200 in progress" every 2 min?|2 * * * * logger "EX200 in progress"|0 2 * * * logger "EX200 in progress"|*/2 * * * * logger "EX200 in progress"|3 +Permissions|How do you give user 'natasha' rw access to /var/tmp/fstab using ACL?|chmod 600 /var/tmp/fstab|chown natasha /var/tmp/fstab|setfacl -m u:natasha:rw- /var/tmp/fstab|3 +Permissions|How do you deny user 'harry' all access to /var/tmp/fstab using ACL?|chmod 000 /var/tmp/fstab|setfacl -m u:harry:- /var/tmp/fstab|chown nobody /var/tmp/fstab|2 +Permissions|How do you set group ownership of /home/managers to sysmgrs and setgid?|chown root:sysmgrs /home/managers && chmod g+s /home/managers|setfacl -m g:sysmgrs:rwx /home/managers|chmod 770 /home/managers|1 +NTP|How do you configure chrony to use materials.example.com as NTP server?|systemctl enable ntpd|Edit /etc/chrony.conf and add 'server materials.example.com iburst'|ntpdate materials.example.com|2 +Autofs|How do you configure autofs to mount remoteuser1's home from materials.example.com?|autofs --mount /rhome/remoteuser1|Add '/rhome /etc/auto.rhome' to /etc/auto.master and 'remoteuser1 -rw materials.example.com:/rhome/remoteuser1' to /etc/auto.rhome|mount -t nfs materials.example.com:/rhome/remoteuser1 /rhome/remoteuser1|2 +User Management|How do you create user 'manalo' with UID 3533?|adduser manalo --uid=3533|useradd -u 3533 manalo|useradd manalo -U 3533|2 +Archiving|How do you create a gzip archive of /usr/local as /root/backup.tar.gz?|gzip /usr/local > /root/backup.tar.gz|tar -cvf /root/backup.tar.gz /usr/local|tar -czvf /root/backup.tar.gz /usr/local|3 +Swap|How do you add a 512M swap partition and make it persistent?|Create partition, mkswap /dev/vdb3, add UUID to /etc/fstab|swapon /dev/vdb3|echo "/dev/vdb3 swap swap defaults 0 0" >> /etc/fstab|1 +LVM|How do you create a logical volume 'qa' of 60 PE in group 'qagroup' and format ext3?|lvcreate -n qa -l 60 qagroup && mkfs.ext3 /dev/qagroup/qa|lvcreate -n qa -l 60 qagroup && mkfs.ext4 /dev/qagroup/qa|lvcreate -n qa -L 60M qagroup && mkfs.ext3 /dev/qagroup/qa|1 +Permissions|How do you give natasha rw and harry no access to /var/tmp/fstab using ACL?|chmod 600 /var/tmp/fstab|setfacl -m u:natasha:rw- /var/tmp/fstab|setfacl -m u:natasha:rw-,u:harry:- /var/tmp/fstab|3 +Scripting|How do you find all files under /usr less than 10M with sgid and save to /root/myfile?|find /usr -size -10M -perm -2000 > /root/myfile|find /usr -size -10M -perm -4000 > /root/myfile|find /usr -size +10M -perm -2000 > /root/myfile|1 +Scripting|How do you create a script to find SUID files between 30k and 50k in /usr?|find /usr -size +30k -size -50k -perm /g=s > /root/newfiles|find /usr -size +30k -size -50k -perm /o=s > /root/newfiles|find /usr -size +30k -size -50k -perm /u=s > /root/newfiles|3 diff --git a/assets/session.log b/assets/session.log index 46303cf..2dde3ab 100644 --- a/assets/session.log +++ b/assets/session.log @@ -1,458 +1,3 @@ -Session start: 2025-01-07 21:54:54 -Session start: 2025-01-07 21:57:48 +Session start: 2025-08-15 10:19:42 XP and level updated: 10 XP, Level 1 -Session end: 2025-01-07 21:58:15 -Session start: 2025-01-07 21:59:58 -XP and level updated: 20 XP, Level 1 -Session end: 2025-01-07 22:00:04 -Session start: 2025-01-07 22:01:15 -XP and level updated: 30 XP, Level 1 -Session end: 2025-01-07 22:01:23 -Session start: 2025-01-07 22:02:38 -XP and level updated: 40 XP, Level 1 -Session end: 2025-01-07 22:03:05 -Session start: 2025-01-07 22:05:06 -XP and level updated: 35 XP, Level 1 -XP and level updated: 30 XP, Level 1 -XP and level updated: 40 XP, Level 1 -Session end: 2025-01-07 22:05:32 -Session start: 2025-01-07 22:07:04 -XP and level updated: 50 XP, Level 1 -Session end: 2025-01-07 22:07:33 -Session start: 2025-01-07 23:37:33 -XP and level updated: 45 XP, Level 1 -XP and level updated: 55 XP, Level 1 -Session end: 2025-01-07 23:40:16 -Session start: 2025-01-07 23:40:32 -XP and level updated: 65 XP, Level 1 -Session end: 2025-01-07 23:40:48 -Session start: 2025-01-07 23:42:08 -XP and level updated: 75 XP, Level 1 -Session end: 2025-01-07 23:42:41 -Session start: 2025-01-07 23:44:30 -XP and level updated: 85 XP, Level 1 -Session start: 2025-01-07 23:46:25 -XP and level updated: 95 XP, Level 1 -Session end: 2025-01-07 23:46:46 -Session start: 2025-01-07 23:49:54 -XP and level updated: 5 XP, Level 2 -Session end: 2025-01-07 23:51:02 -Session start: 2025-01-07 23:53:24 -XP and level updated: 0 XP, Level 2 -Session end: 2025-01-07 23:53:50 -Session start: 2025-01-07 23:56:05 -XP and level updated: 0 XP, Level 2 -Session end: 2025-01-07 23:56:40 -Session start: 2025-01-07 23:58:57 -Session start: 2025-01-08 00:00:35 -Session start: 2025-01-08 00:04:38 -XP and level updated: 0 XP, Level 2 -Session end: 2025-01-08 00:04:59 -Session start: 2025-01-08 00:11:09 -XP and level updated: 10 XP, Level 2 -Session end: 2025-01-08 00:11:34 -Session start: 2025-01-08 00:14:22 -XP and level updated: 5 XP, Level 2 -Session end: 2025-01-08 00:14:36 -Session start: 2025-01-08 19:45:55 -Session start: 2025-01-08 19:45:56 -Session start: 2025-01-08 19:45:56 -Session start: 2025-01-08 19:45:56 -Session start: 2025-01-08 19:45:57 -Session start: 2025-01-08 19:51:08 -XP and level updated: 15 XP, Level 2 -Session end: 2025-01-08 20:11:28 -Session start: 2025-01-08 20:55:07 -XP and level updated: 25 XP, Level 2 -Session end: 2025-01-08 20:55:36 -Session start: 2025-01-08 21:08:29 -Session start: 2025-01-08 21:08:29 -Session start: 2025-01-08 21:08:29 -Session start: 2025-01-08 21:08:30 -Session start: 2025-01-08 21:08:30 -Session start: 2025-01-08 21:19:58 -Session start: 2025-01-08 21:19:58 -Session start: 2025-01-08 21:19:58 -Session start: 2025-01-08 21:19:58 -Session start: 2025-01-08 21:19:59 -Session start: 2025-01-08 23:03:13 -Session start: 2025-01-08 23:03:14 -Session start: 2025-01-08 23:03:14 -Session start: 2025-01-08 23:03:15 -Session start: 2025-01-08 23:03:15 -Session start: 2025-01-09 19:32:07 -Session start: 2025-01-09 19:32:08 -Session start: 2025-01-09 19:32:08 -Session start: 2025-01-09 19:32:23 -Session start: 2025-01-09 19:32:23 -Session start: 2025-01-09 19:32:23 -Session start: 2025-01-09 19:32:24 -Session start: 2025-01-09 19:32:24 -Session start: 2025-01-09 19:42:58 -XP and level updated: 35 XP, Level 2 -Session end: 2025-01-09 20:03:12 -Session start: 2025-01-10 12:54:39 -XP and level updated: 45 XP, Level 2 -Session end: 2025-01-10 12:56:40 -Session start: 2025-01-10 12:56:44 -XP and level updated: 55 XP, Level 2 -Session end: 2025-01-10 12:57:08 -Session start: 2025-01-10 12:58:33 -XP and level updated: 65 XP, Level 2 -Session end: 2025-01-10 12:58:47 -Session start: 2025-01-10 12:58:58 -XP and level updated: 75 XP, Level 2 -Session end: 2025-01-10 12:59:20 -Session start: 2025-01-10 12:59:22 -XP and level updated: 85 XP, Level 2 -Session end: 2025-01-10 12:59:44 -Session start: 2025-01-10 13:05:23 -XP and level updated: 95 XP, Level 2 -Session end: 2025-01-10 13:05:37 -Session start: 2025-01-10 13:06:37 -XP and level updated: 105 XP, Level 2 -Session end: 2025-01-10 13:06:52 -Session start: 2025-01-10 13:16:09 -Session start: 2025-01-10 13:16:09 -Session start: 2025-01-10 13:16:09 -Session start: 2025-01-10 13:16:09 -Session start: 2025-01-10 13:16:10 -Session start: 2025-01-14 18:59:04 -XP and level updated: 100 XP, Level 2 -Session end: 2025-01-14 19:23:41 -Session start: 2025-01-15 07:47:49 -Session start: 2025-01-15 07:47:49 -Session start: 2025-01-15 07:47:50 -Session start: 2025-01-15 07:47:50 -Session start: 2025-01-15 07:47:51 -Session start: 2025-01-15 07:55:49 -XP and level updated: 110 XP, Level 2 -Session end: 2025-01-15 08:17:28 -Session start: 2025-01-15 14:59:38 -Session start: 2025-01-15 14:59:38 -Session start: 2025-01-15 14:59:38 -Session start: 2025-01-15 14:59:39 -Session start: 2025-01-15 14:59:46 -Session start: 2025-01-15 14:59:46 -Session start: 2025-01-15 14:59:46 -Session start: 2025-01-15 14:59:47 -Session start: 2025-01-15 14:59:47 -Session start: 2025-01-15 21:39:12 -Session start: 2025-01-15 21:39:13 -Session start: 2025-01-15 21:39:13 -Session start: 2025-01-15 21:39:13 -Session start: 2025-01-15 21:39:18 -Session start: 2025-01-15 21:39:18 -Session start: 2025-01-15 21:39:19 -Session start: 2025-01-15 21:39:19 -Session start: 2025-01-15 21:39:20 -Session start: 2025-01-17 15:21:48 -Session start: 2025-01-17 15:21:48 -Session start: 2025-01-17 15:21:48 -Session start: 2025-01-17 15:21:49 -Session start: 2025-01-17 15:21:52 -Session start: 2025-01-17 15:21:53 -Session start: 2025-01-17 15:21:53 -Session start: 2025-01-17 15:21:53 -Session start: 2025-01-17 15:21:53 -Session start: 2025-01-17 19:15:35 -Session start: 2025-01-17 19:15:36 -Session start: 2025-01-17 19:15:36 -Session start: 2025-01-17 19:15:36 -Session start: 2025-01-17 19:15:42 -Session start: 2025-01-17 19:15:42 -Session start: 2025-01-17 19:15:42 -Session start: 2025-01-17 19:15:42 -Session start: 2025-01-17 19:15:42 -Session start: 2025-01-17 19:20:19 -Session end: 2025-01-17 19:20:25 -Session start: 2025-01-17 19:20:35 -Session start: 2025-01-17 19:24:39 -Session start: 2025-01-17 19:24:49 -XP and level updated: 120 XP, Level 2 -Session end: 2025-01-17 19:46:00 -Session start: 2025-01-17 19:46:11 -XP and level updated: 130 XP, Level 2 -Session end: 2025-01-17 19:46:24 -Session start: 2025-01-17 19:46:34 -Session end: 2025-01-17 19:46:38 -Session start: 2025-01-17 19:46:48 -XP and level updated: 125 XP, Level 2 -Session start: 2025-01-18 20:21:53 -Session start: 2025-01-18 20:22:03 -XP and level updated: 135 XP, Level 2 -Session start: 2025-01-18 20:26:51 -Session start: 2025-01-18 20:27:18 -Session start: 2025-01-18 20:28:48 -Session start: 2025-01-18 20:31:24 -Session start: 2025-01-18 20:33:13 -XP and level updated: 145 XP, Level 2 -XP and level updated: 155 XP, Level 2 -Session end: 2025-01-18 20:33:50 -Session start: 2025-01-18 20:35:58 -XP and level updated: 165 XP, Level 2 -Session end: 2025-01-18 20:36:18 -Session start: 2025-01-18 20:36:20 -XP and level updated: 175 XP, Level 2 -Session end: 2025-01-18 20:36:35 -Session start: 2025-01-18 20:38:28 -XP and level updated: 170 XP, Level 2 -Session end: 2025-01-18 20:38:47 -Session start: 2025-01-18 20:41:32 -XP and level updated: 180 XP, Level 2 -Session end: 2025-01-18 20:42:35 -Session start: 2025-01-18 20:42:41 -Session end: 2025-01-18 20:42:49 -Session start: 2025-01-18 20:44:43 -XP and level updated: 175 XP, Level 2 -Session end: 2025-01-18 20:45:05 -Session start: 2025-01-18 20:45:09 -XP and level updated: 185 XP, Level 2 -Session end: 2025-01-18 20:45:22 -Session start: 2025-01-18 20:45:58 -XP and level updated: 180 XP, Level 2 -Session end: 2025-01-18 20:46:12 -Session start: 2025-01-18 20:46:15 -XP and level updated: 190 XP, Level 2 -Session end: 2025-01-18 20:46:28 -Session end: 2025-01-18 20:47:18 -Session start: 2025-01-18 20:47:29 -XP and level updated: 0 XP, Level 3 -Session end: 2025-01-18 20:47:46 -Session start: 2025-01-18 20:47:52 -Session start: 2025-01-18 20:47:56 -Session end: 2025-01-18 20:48:06 -Session end: 2025-01-18 20:48:09 -Session start: 2025-01-18 20:48:19 -Session end: 2025-01-18 20:48:25 -Session start: 2025-01-18 20:48:35 -Session end: 2025-01-18 20:48:51 -Session start: 2025-01-18 20:49:01 -XP and level updated: 10 XP, Level 3 -Session end: 2025-01-18 20:49:13 -Session start: 2025-01-18 20:49:23 -XP and level updated: 20 XP, Level 3 -Session end: 2025-01-18 20:49:30 -Session start: 2025-01-18 20:49:37 -Session start: 2025-01-18 20:49:40 -XP and level updated: 15 XP, Level 3 -Session end: 2025-01-18 20:49:53 -XP and level updated: 25 XP, Level 3 -Session end: 2025-01-18 20:50:03 -Session start: 2025-01-18 20:50:03 -Session end: 2025-01-18 20:50:07 -Session start: 2025-01-18 20:50:17 -XP and level updated: 35 XP, Level 3 -Session end: 2025-01-18 20:51:43 -Session start: 2025-01-18 20:51:53 -XP and level updated: 45 XP, Level 3 -Session end: 2025-01-18 20:52:04 -Session start: 2025-01-18 20:52:14 -XP and level updated: 55 XP, Level 3 -Session end: 2025-01-18 20:52:23 -Session start: 2025-01-18 20:52:33 -Session end: 2025-01-18 20:52:37 -Session start: 2025-01-18 20:52:47 -Session start: 2025-01-18 20:53:03 -XP and level updated: 50 XP, Level 3 -Session end: 2025-01-18 20:53:17 -XP and level updated: 60 XP, Level 3 -Session end: 2025-01-18 20:53:39 -Session start: 2025-01-18 20:53:49 -Session end: 2025-01-18 20:53:55 -Session start: 2025-01-18 20:54:05 -Session end: 2025-01-18 20:54:43 -Session start: 2025-01-18 20:54:53 -Session end: 2025-01-18 20:55:04 -Session start: 2025-01-18 20:55:15 -Session end: 2025-01-18 20:55:27 -Session start: 2025-01-18 20:55:37 -Session end: 2025-01-18 20:56:02 -Session start: 2025-01-18 20:56:13 -Session duration: 0 minutes and 3 seconds -Session end: 2025-01-18 20:56:16 -Session start: 2025-01-18 20:56:17 -Session start: 2025-01-18 20:56:26 -Session duration: 0 minutes and 3 seconds -Session end: 2025-01-18 20:56:30 -XP and level updated: 70 XP, Level 3 -Session start: 2025-01-18 20:56:40 -Session duration: 0 minutes and 3 seconds -Session end: 2025-01-18 20:56:44 -Session duration: 0 minutes and 26 seconds -Session end: 2025-01-18 20:56:44 -Session start: 2025-01-18 20:56:54 -XP and level updated: 80 XP, Level 3 -Session duration: 0 minutes and 10 seconds -Session end: 2025-01-18 20:57:04 -Session start: 2025-01-18 20:57:14 -Session start: 2025-01-18 21:00:19 -XP and level updated: 90 XP, Level 3 -Session duration: 0 minutes and 13 seconds -Session end: 2025-01-18 21:00:33 -Session start: 2025-01-18 21:01:07 -Session duration: 0 minutes and 4 seconds -Session end: 2025-01-18 21:01:12 -Session start: 2025-01-18 21:01:22 -XP and level updated: 100 XP, Level 3 -Session duration: 0 minutes and 10 seconds -Session end: 2025-01-18 21:01:33 -Session start: 2025-01-18 21:01:43 -XP and level updated: 95 XP, Level 3 -Session duration: 0 minutes and 11 seconds -Session end: 2025-01-18 21:01:54 -Session start: 2025-01-18 21:02:04 -Session start: 2025-01-18 21:04:01 -XP and level updated: 105 XP, Level 3 -Session duration: 0 minutes and 12 seconds -Session end: 2025-01-18 21:04:14 -Session start: 2025-01-18 21:15:18 -XP and level updated: 115 XP, Level 3 -Session duration: 0 minutes and 14 seconds -Session end: 2025-01-18 21:15:33 -Session start: 2025-01-19 15:13:11 -Session start: 2025-01-19 15:18:00 -XP and level updated: 125 XP, Level 3 -Session duration: 20 minutes and 0 seconds -Session end: 2025-01-19 15:38:03 -Session start: 2025-01-23 19:41:17 -Session start: 2025-01-24 07:02:57 -Session start: 2025-01-24 07:41:21 -XP and level updated: 135 XP, Level 3 -Session duration: 20 minutes and 0 seconds -Session end: 2025-01-24 08:01:25 -Session start: 2025-01-24 12:52:39 -Session start: 2025-01-29 08:20:29 -Session start: 2025-02-02 01:14:42 -Session start: 2025-02-07 07:22:13 -Session start: 2025-02-07 08:16:24 -XP and level updated: 145 XP, Level 3 -Session duration: 20 minutes and 0 seconds -Session end: 2025-02-07 08:36:28 -Session start: 2025-02-07 11:26:38 -XP and level updated: 155 XP, Level 3 -Session duration: 5 minutes and 58 seconds -Session end: 2025-02-07 11:32:38 -Session start: 2025-02-07 15:13:45 -XP and level updated: 165 XP, Level 3 -Session duration: 0 minutes and 34 seconds -Session end: 2025-02-07 15:14:20 -Session start: 2025-02-08 19:31:35 -Session start: 2025-02-08 19:39:23 -XP and level updated: 175 XP, Level 3 -Session duration: 20 minutes and 0 seconds -Session end: 2025-02-08 19:59:27 -Session start: 2025-02-09 19:55:14 -XP and level updated: 185 XP, Level 3 -Session duration: 60 minutes and 0 seconds -Session end: 2025-02-09 20:55:23 -Session start: 2025-02-10 21:34:09 -XP and level updated: 180 XP, Level 3 -Session duration: 3 minutes and 49 seconds -Session end: 2025-02-10 21:37:58 -Session start: 2025-02-10 21:38:33 -XP and level updated: 190 XP, Level 3 -Session duration: 0 minutes and 17 seconds -Session end: 2025-02-10 21:38:51 -Session start: 2025-02-11 19:31:01 -XP and level updated: 185 XP, Level 3 -XP and level updated: 180 XP, Level 3 -Session duration: 60 minutes and 0 seconds -Session end: 2025-02-11 20:31:11 -Session start: 2025-02-15 12:17:03 -XP and level updated: 190 XP, Level 3 -Session duration: 60 minutes and 0 seconds -Session end: 2025-02-15 13:17:13 -Session start: 2025-02-15 15:27:46 -Session start: 2025-02-15 15:37:36 -XP and level updated: 185 XP, Level 3 -Session start: 2025-02-15 20:58:49 -Session start: 2025-02-15 21:08:40 -XP and level updated: 180 XP, Level 3 -Session duration: 0 minutes and 46 seconds -Session end: 2025-02-15 21:09:27 -Session start: 2025-02-16 19:07:37 -XP and level updated: 190 XP, Level 3 -Session duration: 60 minutes and 0 seconds -Session end: 2025-02-16 20:07:45 -Session start: 2025-03-10 18:25:38 -XP and level updated: 185 XP, Level 3 -Session duration: 0 minutes and 40 seconds -Session end: 2025-03-10 18:26:18 -Session start: 2025-03-20 20:16:17 -Session duration: 0 minutes and 4 seconds -Session end: 2025-03-20 20:16:22 -Session start: 2025-03-21 09:32:07 -XP and level updated: 195 XP, Level 3 -Session duration: 0 minutes and 6 seconds -Session end: 2025-03-21 09:32:14 -Session start: 2025-03-21 20:32:49 -XP and level updated: 190 XP, Level 3 -XP and level updated: 185 XP, Level 3 -Session duration: 0 minutes and 12 seconds -Session end: 2025-03-21 20:33:02 -Session start: 2025-03-21 21:52:31 -Session duration: 0 minutes and 3 seconds -Session end: 2025-03-21 21:52:35 -Session start: 2025-03-22 01:18:00 -XP and level updated: 180 XP, Level 3 -Session duration: 0 minutes and 11 seconds -Session end: 2025-03-22 01:18:11 -Session start: 2025-03-22 09:07:18 -XP and level updated: 190 XP, Level 3 -Session duration: 1 minutes and 31 seconds -Session end: 2025-03-22 09:08:50 -Session start: 2025-03-23 09:24:39 -Session duration: 60 minutes and 0 seconds -Session end: 2025-03-23 10:24:47 -Session start: 2025-03-26 21:22:54 -XP and level updated: 185 XP, Level 3 -Session duration: 0 minutes and 44 seconds -Session end: 2025-03-26 21:23:39 -Session start: 2025-04-09 21:13:39 -XP and level updated: 195 XP, Level 3 -Session duration: 0 minutes and 12 seconds -Session end: 2025-04-09 21:13:51 -Session start: 2025-04-26 21:55:04 -XP and level updated: 190 XP, Level 3 -Session duration: 1 minutes and 58 seconds -Session end: 2025-04-26 21:57:03 -Session start: 2025-04-30 23:49:24 -XP and level updated: 200 XP, Level 3 -Session duration: 0 minutes and 13 seconds -Session end: 2025-04-30 23:49:37 -Session start: 2025-05-01 18:27:02 -Session duration: 60 minutes and 0 seconds -Session end: 2025-05-01 19:27:10 -Session start: 2025-05-01 20:30:21 -XP and level updated: 195 XP, Level 3 -Session duration: 0 minutes and 11 seconds -Session end: 2025-05-01 20:30:32 -Session start: 2025-06-17 22:44:00 -XP and level updated: 205 XP, Level 3 -Session duration: 0 minutes and 20 seconds -Session end: 2025-06-17 22:44:22 -Session start: 2025-06-17 23:05:53 -XP and level updated: 215 XP, Level 3 -Session start: 2025-06-17 23:06:31 -XP and level updated: 225 XP, Level 3 -Session start: 2025-06-17 23:23:35 -Session duration: 22 minutes and 43 seconds -Session end: 2025-06-17 23:29:19 -Session start: 2025-06-17 23:34:04 -Session start: 2025-06-17 23:37:04 -Session end: 2025-06-17 23:37:04 -XP and level updated: 235 XP, Level 3 -XP and level updated: 245 XP, Level 3 -Session start: 2025-06-17 23:42:39 -Session end: 2025-06-17 23:42:39 -XP and level updated: 240 XP, Level 3 -Session Ellipsis: 2025-06-17 23:42:54 -Session Ellipsis: 2025-06-17 23:42:54 -Session Ellipsis: 2025-06-17 23:42:55 -Session start: 2025-06-17 23:43:21 -Session end: 2025-06-17 23:43:21 -XP and level updated: 250 XP, Level 3 -Session Ellipsis: 2025-06-17 23:44:59 -Session Ellipsis: 2025-06-17 23:44:59 -Session Ellipsis: 2025-06-17 23:44:59 +Session end: 2025-08-15 10:19:52 diff --git a/assets/total_time.txt b/assets/total_time.txt index cecd0ea..c227083 100644 --- a/assets/total_time.txt +++ b/assets/total_time.txt @@ -1 +1 @@ -28955 \ No newline at end of file +0 \ No newline at end of file diff --git a/assets/xp.txt b/assets/xp.txt index 8a32cf7..c227083 100644 --- a/assets/xp.txt +++ b/assets/xp.txt @@ -1 +1 @@ -250 \ No newline at end of file +0 \ No newline at end of file diff --git a/main.py b/main.py index a3a0e5f..8a18186 100644 --- a/main.py +++ b/main.py @@ -1,27 +1,236 @@ +# 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 -import random -import subprocess 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') -TOTAL_TIME_FILE = os.path.join(ASSETS_DIR, 'total_time.txt') -URL = "https://rol.redhat.com" -TIMER_MINUTES = 60 +DB_PATH = os.path.join(SCRIPT_DIR, 'rhel_learning.db') +SUBSCRIPTION_END_DATE = datetime(2026, 5, 26) -# Set the ROL subscription expiration date -SUBSCRIPTION_END_DATE = datetime.now() + timedelta(days=343) +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(): @@ -46,32 +255,15 @@ def log_session(status): def display_summary(): """Display a summary of total time, XP, and level.""" - try: - with open(TOTAL_TIME_FILE, 'r') as f: - total_time = int(f.read().strip() or 0) - except FileNotFoundError: - total_time = 0 - - try: - with open(XP_FILE, 'r') as f: - xp = int(f.read().strip() or 0) - except FileNotFoundError: - xp = 0 - - try: - with open(LEVEL_FILE, 'r') as f: - level = int(f.read().strip() or 1) - except FileNotFoundError: - level = 1 - - hours, remainder = divmod(total_time, 3600) - minutes, seconds = divmod(remainder, 60) + stats = UserStatsSqlite(DB_PATH, SUBSCRIPTION_END_DATE) print(f"Welcome to the RHEL Learning Script!") - print(f"Total session time: {hours} hours, {minutes} minutes, and {seconds} seconds.") + 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(): @@ -83,29 +275,18 @@ def kill_steam(): def update_xp(correct): """Update XP and level based on whether the question was answered correctly.""" - with open(XP_FILE, 'r') as f: - xp = int(f.read().strip() or 0) - with open(LEVEL_FILE, 'r') as f: - level = int(f.read().strip() or 1) - + stats = UserStatsSqlite(DB_PATH, SUBSCRIPTION_END_DATE) if correct: - xp += 10 + xp, level, leveled_up = stats.add_xp(10) else: + xp, level, _ = stats.get_xp_level() xp = max(0, xp - 5) - - if xp >= level * 100: - level += 1 - xp -= (level - 1) * 100 + 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(XP_FILE, 'w') as f: - f.write(str(xp)) - with open(LEVEL_FILE, 'w') as f: - f.write(str(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): @@ -116,21 +297,23 @@ def show_timer(minutes, questions_file, session_log, total_time, xp, level, root elapsed_time += 1 hours, remainder = divmod(seconds_left, 3600) minutes_left, seconds = divmod(remainder, 60) - timer_label.config(text=f"Time left: {hours:02}:{minutes_left:02}:{seconds:02}") - root.after(1000, update_timer) + # 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 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!") + 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: @@ -139,52 +322,147 @@ def show_timer(minutes, questions_file, session_log, total_time, xp, level, root f.write(str(total_time + elapsed_time)) root.destroy() - def on_closing(): - end_session() - - # Load & parse questions - with open(questions_file, 'r') as f: - lines = [l.strip() for l in f if l.strip()] - question_line = random.choice(lines) - parts = question_line.split('|') - question_text = parts[0] - answers = parts[1:-1] - correct_idx = int(parts[-1]) - 1 # zero-based - - # Build a list of (text, is_correct) and shuffle - opts = [] - for idx, ans in enumerate(answers): - opts.append({'text': ans, 'correct': (idx == correct_idx)}) - random.shuffle(opts) - - # Display question - question_label = tk.Label(root, text=f"Question: {question_text}", font=("Helvetica", 14)) - question_label.pack(pady=10) - - answer_var = tk.StringVar(value="") - radio_buttons = [] - for opt in opts: - rb = tk.Radiobutton(root, text=opt['text'], - variable=answer_var, value=opt['text'], - font=("Helvetica", 12)) - rb.pack(anchor='w') - radio_buttons.append(rb) - - def check_answer(): - sel = answer_var.get() - correct = any(o['text'] == sel and o['correct'] for o in opts) - if correct: - messagebox.showinfo("Correct!", "You chose the right answer.") - new_xp, new_level = update_xp(True) + # 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: - messagebox.showinfo("Incorrect", "Sorry, that’s not correct.") - new_xp, new_level = update_xp(False) - 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}") + lines.append(f"{row[1]}|{row[2]}|{row[3]}|{row[4]}|{row[5]}|{row[6]}") - submit_btn = tk.Button(root, text="Submit", command=check_answer) - submit_btn.pack(pady=5) + # Track per-category score + score_by_category = {} + total_correct = 0 + total_questions = 0 + max_score = 300 + + # Show all questions in random order + random.shuffle(lines) + questions_to_ask = lines + question_idx = 0 + + # Create a frame for question/answer widgets ONCE + question_frame = tk.Frame(root) + question_frame.pack(pady=10) + + def ask_next_question(): + nonlocal question_idx, total_questions + nonlocal total_correct, total_questions + if question_idx >= len(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[question_idx] + parts = qline.split('|') + if len(parts) < 5: + question_idx += 1 + 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 question_idx, 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 + else: + messagebox.showinfo("Incorrect", f"Sorry, that's not correct.\n{detail}") + new_xp, new_level = update_xp(False) + 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}") + root.after(500, lambda: [ask_next_question()]) + + 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) + + # Start the first question + ask_next_question() def periodic_kill_steam(): kill_steam() @@ -193,37 +471,12 @@ def show_timer(minutes, questions_file, session_log, total_time, xp, level, root seconds_left = minutes * 60 elapsed_time = 0 - # GUI Components - 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="ROL Subscription: Calculating...", font=("Helvetica", 14), fg="red") - rol_label.pack(pady=5) - - xp_label = tk.Label(root, text=f"XP: {xp}", font=("Helvetica", 14)) - xp_label.pack(pady=5) - - level_label = tk.Label(root, text=f"Level: {level}", font=("Helvetica", 14)) - level_label.pack(pady=5) - - total_time_label = tk.Label(root, text=f"Total session time: {total_time // 3600} hours, {(total_time % 3600) // 60} minutes, and {total_time % 60} seconds", font=("Helvetica", 14)) - total_time_label.pack(pady=5) root.after(1000, update_timer) root.after(1000, update_rol_timer) root.after(10000, periodic_kill_steam) -def open_firefox(url): - print(f"Attempting to open Firefox with URL: {url}") - if 'DISPLAY' in os.environ: - try: - subprocess.Popen(['firefox', '--new-window', url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) - print("Firefox opened successfully.") - except subprocess.CalledProcessError: - print("Firefox is already running. Reusing the existing instance.") - subprocess.Popen(['firefox', '--new-tab', url], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) - else: - print("No graphical environment detected. Skipping browser opening.") +## Removed open_firefox, use open_learning_links instead def shuffle_questions(input_file, output_file): """Shuffle questions and answers, updating the correct answer index.""" @@ -233,12 +486,25 @@ def shuffle_questions(input_file, output_file): with open(output_file, 'w') as f: for line in lines: parts = line.split('|') - q, answers, correct = parts[0], parts[1:-1], int(parts[-1]) + # 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"{q}|{'|'.join(new_answers)}|{new_correct}\n") + f.write(f"{category}|{question}|{'|'.join(new_answers)}|{new_correct}\n") def open_learning_links(): urls = [ @@ -249,18 +515,28 @@ def open_learning_links(): 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(URL) + # open_firefox removed; open_learning_links is used instead open_learning_links() root = tk.Tk() root.title("RHEL Learning Timer") - root.protocol("WM_DELETE_WINDOW", lambda: log_session(...)) - show_timer(TIMER_MINUTES, QUESTIONS_SHUFFLED_FILE, SESSION_LOG, total_time, xp, level, root) - log_session("end") - root.mainloop() + + 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) diff --git a/migrate_to_sqlite.py b/migrate_to_sqlite.py new file mode 100644 index 0000000..8053fdb --- /dev/null +++ b/migrate_to_sqlite.py @@ -0,0 +1,63 @@ +import os +from datetime import datetime +from main import ( + XP_FILE, LEVEL_FILE, TOTAL_TIME_FILE, SESSION_LOG, DB_PATH, SUBSCRIPTION_END_DATE, UserStatsSqlite +) + +def migrate_txt_to_sqlite(): + migrated = False + 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 + if migrated: + stats = UserStatsSqlite(DB_PATH, SUBSCRIPTION_END_DATE) + stats.save_stats(xp, level, total_time) + 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: + 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 + if migrated: + for f in [XP_FILE, LEVEL_FILE, TOTAL_TIME_FILE, SESSION_LOG]: + try: + os.remove(f) + except Exception: + pass + if migrated: + print("Migration complete. Data moved to SQLite and text files removed.") + else: + print("No migration needed. No text files found.") + +if __name__ == "__main__": + migrate_txt_to_sqlite() diff --git a/rhel_learning.db b/rhel_learning.db new file mode 100644 index 0000000000000000000000000000000000000000..d65f0b3169d54ea9854f6cd49020d349ad311f60 GIT binary patch literal 24576 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 aYd7@#fKJ8YysykPGY7|2^Z~5hIsXmH87}_- literal 0 HcmV?d00001 diff --git a/src/main.py b/src/main.py deleted file mode 100644 index 9fd37bd..0000000 --- a/src/main.py +++ /dev/null @@ -1,139 +0,0 @@ -import os -import time -import psutil -import tkinter as tk -import random -import subprocess - -# Utility functions -def initialize_assets(assets_dir, xp_file, level_file, session_log, total_time_file, questions_file): - if not os.path.exists(assets_dir): - os.makedirs(assets_dir) - if not os.path.exists(xp_file): - with open(xp_file, 'w') as f: - f.write('0') - if not os.path.exists(level_file): - with open(level_file, 'w') as f: - f.write('1') - if not os.path.exists(session_log): - open(session_log, 'w').close() - if not os.path.exists(total_time_file): - with open(total_time_file, 'w') as f: - f.write('0') - if not os.path.exists(questions_file): - raise FileNotFoundError(f"Questions file not found: {questions_file}") - -def log_session(session_log, status): - with open(session_log, 'a') as f: - f.write(f"Session {status}: {time.strftime('%Y-%m-%d %H:%M:%S')}\n") - -def display_summary(total_time_file, xp_file, level_file): - with open(total_time_file, 'r') as f: - total_time = int(f.read().strip()) - with open(xp_file, 'r') as f: - xp = int(f.read().strip()) - with open(level_file, 'r') as f: - level = int(f.read().strip()) - hours, minutes = divmod(total_time, 60) - print(f"Welcome to the RHEL Learning Script!") - print(f"Total session time: {hours} hours and {minutes} minutes.") - print(f"Current Level: {level}") - print(f"Current XP: {xp}") - print() - -def kill_steam(): - for proc in psutil.process_iter(): - if proc.name() == "steam.exe": - proc.kill() - -def show_timer(minutes, questions_file, session_log): - print(f"The session will last {minutes} minutes.") - - # Open the URL in the default web browser - subprocess.Popen(['xdg-open', URL]) - - # Create a simple timer using tkinter - root = tk.Tk() - root.title("Timer") - - label = tk.Label(root, text="", font=("Helvetica", 48)) - label.pack() - - def update_timer(): - nonlocal minutes - if minutes > 0: - minutes -= 1 - label.config(text=f"{minutes} minutes remaining") - root.after(60000, update_timer) - else: - label.config(text="Time's up!") - root.after(1000, root.destroy) - - update_timer() - root.mainloop() - - # Simulate answering questions - with open(questions_file, 'r') as f: - questions = f.readlines() - - random.shuffle(questions) - for question in questions[:5]: # Assume we ask 5 questions - print(question.strip()) - time.sleep(2) # Simulate time taken to answer - - with open(session_log, 'a') as f: - f.write(f"Session duration: {minutes} minutes\n") - -def add_xp(xp_file, level_file, session_log): - with open(xp_file, 'r') as f: - xp = int(f.read().strip()) - with open(level_file, 'r') as f: - level = int(f.read().strip()) - new_xp = xp + 10 - if new_xp >= level * 100: - level += 1 - new_xp -= (level - 1) * 100 - print(f"LEVEL UP! You are now Level {level}!") - with open(xp_file, 'w') as f: - f.write(str(new_xp)) - with open(level_file, 'w') as f: - f.write(str(level)) - with open(session_log, 'a') as f: - f.write(f"XP and level updated: {new_xp} XP, Level {level}\n") - -# Constants -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') -QUESTIONS_FILE = os.path.join(ASSETS_DIR, 'questions.txt') -TOTAL_TIME_FILE = os.path.join(ASSETS_DIR, 'total_time.txt') -URL = "https://rol.redhat.com" -TIMER_MINUTES = 20 - -def main(): - # Initialize assets - initialize_assets(ASSETS_DIR, XP_FILE, LEVEL_FILE, SESSION_LOG, TOTAL_TIME_FILE, QUESTIONS_FILE) - - # Log session start - log_session(SESSION_LOG, "start") - - # Display session summary - display_summary(TOTAL_TIME_FILE, XP_FILE, LEVEL_FILE) - - # Start killing Steam processes in the background - kill_steam() - - # Start session - print("Starting RHEL Learning Session...") - show_timer(TIMER_MINUTES, QUESTIONS_FILE, SESSION_LOG) - - # Add XP - add_xp(XP_FILE, LEVEL_FILE, SESSION_LOG) - - # Log session end - log_session(SESSION_LOG, "end") - -if __name__ == "__main__": - main()