diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..a49e4a4 Binary files /dev/null and b/__pycache__/main.cpython-312.pyc differ 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 0000000..d65f0b3 Binary files /dev/null and b/rhel_learning.db differ 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()