372 lines
14 KiB
Python
372 lines
14 KiB
Python
#
|
|
# Will of Steel Proprietary License (WOSPL)
|
|
#
|
|
# Copyright (c) 2025-present Will of Steel
|
|
#
|
|
# This software and its accompanying source code ("the Software") are proprietary and confidential. Unauthorized actions are strictly prohibited. Specifically, you are not permitted to:
|
|
#
|
|
# - Use the Software for any purpose other than internal review, unless explicitly authorized in writing by the Licensor.
|
|
# - Modify, adapt, translate, or create derivative works based on the Software.
|
|
# - Distribute, disclose, sublicense, lease, rent, or otherwise make available the Software to any third party.
|
|
# - Reverse engineer, decompile, disassemble, or attempt to derive the source code, object code, or underlying structure, ideas, or algorithms of the Software.
|
|
# - Remove, alter, or obscure any proprietary notices, labels, or marks on the Software.
|
|
# - Use the Software for any commercial purposes or in any commercial environment.
|
|
# - Claim ownership or authorship of the Software or any part thereof.
|
|
# - Use the Software in any manner that violates applicable laws or regulations.
|
|
#
|
|
# The Software is provided "as is" without warranty of any kind, express or implied. The Licensor shall not be liable for any damages arising out of or in connection with the use or performance of the Software.
|
|
#
|
|
# Any violation of these terms will result in immediate termination of your rights to use the Software and may subject you to legal action.
|
|
#
|
|
# For permissions beyond the scope of this license, please contact neil@willofsteel.me
|
|
# For more information, please visit https://willofsteel.me/license
|
|
|
|
# This file is in compliance with Pep 8 and PEP 257 standards. -- Neil 28-06-2025
|
|
|
|
import json
|
|
import os
|
|
|
|
|
|
class QuestionManager:
|
|
def __init__(self, filename: str = "quiz_questions.json"):
|
|
self.filename = filename
|
|
self.questions = []
|
|
self.load_questions()
|
|
|
|
def load_questions(self):
|
|
"""Load questions from JSON file"""
|
|
if os.path.exists(self.filename):
|
|
try:
|
|
with open(self.filename, "r", encoding="utf-8") as file:
|
|
data = json.load(file)
|
|
self.questions = data.get("questions", [])
|
|
except json.JSONDecodeError:
|
|
print(f"Error: Invalid JSON format in {self.filename}")
|
|
self.questions = []
|
|
else:
|
|
print(f"File {self.filename} not found. Starting with empty question list.")
|
|
self.questions = []
|
|
|
|
def save_questions(self):
|
|
"""Save questions to JSON file"""
|
|
data = {"questions": self.questions}
|
|
try:
|
|
with open(self.filename, "w", encoding="utf-8") as file:
|
|
json.dump(data, file, indent=2, ensure_ascii=False)
|
|
print(f"Questions saved to {self.filename}")
|
|
except Exception as e:
|
|
print(f"Error saving questions: {e}")
|
|
|
|
def add_question(self):
|
|
"""Add a new question interactively"""
|
|
print("\n" + "=" * 50)
|
|
print("ADD NEW QUESTION")
|
|
print("=" * 50)
|
|
|
|
question_text = input("Enter the question: ").strip()
|
|
if not question_text:
|
|
print("Question cannot be empty!")
|
|
return
|
|
|
|
# Ask for question image
|
|
question_image = input(
|
|
"Enter question image path (optional, press Enter to skip): "
|
|
).strip()
|
|
if question_image and not os.path.exists(question_image):
|
|
print(
|
|
f"Warning: Image file '{question_image}' not found. Continuing without image."
|
|
)
|
|
question_image = ""
|
|
|
|
print("\nEnter 4 answer options:")
|
|
options = []
|
|
for i in range(4):
|
|
option = input(f"Option {i+1}: ").strip()
|
|
if not option:
|
|
print("Option cannot be empty!")
|
|
return
|
|
options.append(option)
|
|
|
|
print("\nOptions:")
|
|
for i, option in enumerate(options, 1):
|
|
print(f"{i}. {option}")
|
|
|
|
while True:
|
|
try:
|
|
correct_num = int(input("\nWhich option is correct? (1-4): "))
|
|
if 1 <= correct_num <= 4:
|
|
correct_answer = options[correct_num - 1]
|
|
break
|
|
else:
|
|
print("Please enter a number between 1 and 4.")
|
|
except ValueError:
|
|
print("Please enter a valid number.")
|
|
|
|
explanation = input("Enter explanation (optional): ").strip()
|
|
|
|
# Ask for explanation image
|
|
explanation_image = input(
|
|
"Enter explanation image path (optional, press Enter to skip): "
|
|
).strip()
|
|
if explanation_image and not os.path.exists(explanation_image):
|
|
print(
|
|
f"Warning: Image file '{explanation_image}' not found. Continuing without image."
|
|
)
|
|
explanation_image = ""
|
|
|
|
# Create question object
|
|
new_question = {
|
|
"question": question_text,
|
|
"options": options,
|
|
"correct_answer": correct_answer,
|
|
}
|
|
|
|
if question_image:
|
|
new_question["question_image"] = question_image
|
|
|
|
if explanation:
|
|
new_question["explanation"] = explanation
|
|
|
|
if explanation_image:
|
|
new_question["explanation_image"] = explanation_image
|
|
|
|
self.questions.append(new_question)
|
|
print(
|
|
f"\n✅ Question added successfully! Total questions: {len(self.questions)}"
|
|
)
|
|
|
|
print("\nQuestion Summary:")
|
|
print(f"Question: {question_text}")
|
|
if question_image:
|
|
print(f"Question Image: {question_image}")
|
|
print(f"Correct Answer: {correct_answer}")
|
|
if explanation:
|
|
print(f"Explanation: {explanation}")
|
|
if explanation_image:
|
|
print(f"Explanation Image: {explanation_image}")
|
|
|
|
def view_questions(self):
|
|
"""Display all questions"""
|
|
if not self.questions:
|
|
print("\nNo questions available.")
|
|
return
|
|
|
|
print(f"\n{'='*50}")
|
|
print(f"ALL QUESTIONS ({len(self.questions)} total)")
|
|
print("=" * 50)
|
|
|
|
for i, q in enumerate(self.questions, 1):
|
|
print(f"\n{i}. {q['question']}")
|
|
if q.get("question_image"):
|
|
print(f" 📷 Question Image: {q['question_image']}")
|
|
for j, option in enumerate(q["options"], 1):
|
|
marker = "✅" if option == q["correct_answer"] else " "
|
|
print(f" {marker} {j}. {option}")
|
|
if "explanation" in q:
|
|
print(f" 💡 Explanation: {q['explanation']}")
|
|
if q.get("explanation_image"):
|
|
print(f" 📷 Explanation Image: {q['explanation_image']}")
|
|
print("-" * 40)
|
|
|
|
def edit_question(self):
|
|
"""Edit an existing question"""
|
|
if not self.questions:
|
|
print("\nNo questions available to edit.")
|
|
return
|
|
|
|
self.view_questions()
|
|
|
|
while True:
|
|
try:
|
|
question_num = int(
|
|
input(
|
|
f"\nEnter question number to edit (1-{len(self.questions)}): "
|
|
)
|
|
)
|
|
if 1 <= question_num <= len(self.questions):
|
|
break
|
|
else:
|
|
print(f"Please enter a number between 1 and {len(self.questions)}.")
|
|
except ValueError:
|
|
print("Please enter a valid number.")
|
|
|
|
question_index = question_num - 1
|
|
old_question = self.questions[question_index]
|
|
|
|
print(f"\nEditing question {question_num}:")
|
|
print(f"Current: {old_question['question']}")
|
|
|
|
new_question_text = input(
|
|
"Enter new question (press Enter to keep current): "
|
|
).strip()
|
|
if new_question_text:
|
|
old_question["question"] = new_question_text
|
|
|
|
# Ask about question image
|
|
current_q_image = old_question.get("question_image", "")
|
|
print(
|
|
f"\nCurrent question image: {current_q_image if current_q_image else 'None'}"
|
|
)
|
|
new_q_image = input(
|
|
"Enter new question image path (press Enter to keep current, 'none' to remove): "
|
|
).strip()
|
|
if new_q_image.lower() == "none":
|
|
old_question.pop("question_image", None)
|
|
elif new_q_image:
|
|
if os.path.exists(new_q_image):
|
|
old_question["question_image"] = new_q_image
|
|
else:
|
|
print(f"Warning: Image file '{new_q_image}' not found.")
|
|
|
|
print("\nCurrent options:")
|
|
for i, option in enumerate(old_question["options"], 1):
|
|
marker = "✅" if option == old_question["correct_answer"] else " "
|
|
print(f"{marker} {i}. {option}")
|
|
|
|
edit_options = input("\nEdit options? (y/n): ").lower().strip()
|
|
if edit_options == "y":
|
|
new_options = []
|
|
for i in range(4):
|
|
current = (
|
|
old_question["options"][i]
|
|
if i < len(old_question["options"])
|
|
else ""
|
|
)
|
|
new_option = input(f"Option {i+1} (current: '{current}'): ").strip()
|
|
if new_option:
|
|
new_options.append(new_option)
|
|
else:
|
|
new_options.append(current)
|
|
|
|
old_question["options"] = new_options
|
|
|
|
print("\nNew options:")
|
|
for i, option in enumerate(new_options, 1):
|
|
print(f"{i}. {option}")
|
|
|
|
while True:
|
|
try:
|
|
correct_num = int(input("\nWhich option is correct? (1-4): "))
|
|
if 1 <= correct_num <= 4:
|
|
old_question["correct_answer"] = new_options[correct_num - 1]
|
|
break
|
|
else:
|
|
print("Please enter a number between 1 and 4.")
|
|
except ValueError:
|
|
print("Please enter a valid number.")
|
|
|
|
current_explanation = old_question.get("explanation", "")
|
|
print(f"\nCurrent explanation: {current_explanation}")
|
|
new_explanation = input(
|
|
"Enter new explanation (press Enter to keep current): "
|
|
).strip()
|
|
if new_explanation:
|
|
old_question["explanation"] = new_explanation
|
|
|
|
# Ask about explanation image
|
|
current_exp_image = old_question.get("explanation_image", "")
|
|
print(
|
|
f"\nCurrent explanation image: {current_exp_image if current_exp_image else 'None'}"
|
|
)
|
|
new_exp_image = input(
|
|
"Enter new explanation image path (press Enter to keep current, 'none' to remove): "
|
|
).strip()
|
|
if new_exp_image.lower() == "none":
|
|
old_question.pop("explanation_image", None)
|
|
elif new_exp_image:
|
|
if os.path.exists(new_exp_image):
|
|
old_question["explanation_image"] = new_exp_image
|
|
else:
|
|
print(f"Warning: Image file '{new_exp_image}' not found.")
|
|
|
|
print("✅ Question updated successfully!")
|
|
|
|
def delete_question(self):
|
|
"""Delete a question"""
|
|
if not self.questions:
|
|
print("\nNo questions available to delete.")
|
|
return
|
|
|
|
self.view_questions()
|
|
|
|
while True:
|
|
try:
|
|
question_num = int(
|
|
input(
|
|
f"\nEnter question number to delete (1-{len(self.questions)}): "
|
|
)
|
|
)
|
|
if 1 <= question_num <= len(self.questions):
|
|
break
|
|
else:
|
|
print(f"Please enter a number between 1 and {len(self.questions)}.")
|
|
except ValueError:
|
|
print("Please enter a valid number.")
|
|
|
|
question_index = question_num - 1
|
|
question_to_delete = self.questions[question_index]
|
|
|
|
print(f"\nQuestion to delete:")
|
|
print(f"{question_to_delete['question']}")
|
|
if question_to_delete.get("question_image"):
|
|
print(f"Question Image: {question_to_delete['question_image']}")
|
|
if question_to_delete.get("explanation"):
|
|
print(f"Explanation: {question_to_delete['explanation']}")
|
|
if question_to_delete.get("explanation_image"):
|
|
print(f"Explanation Image: {question_to_delete['explanation_image']}")
|
|
|
|
confirm = (
|
|
input("\nAre you sure you want to delete this question? (y/n): ")
|
|
.lower()
|
|
.strip()
|
|
)
|
|
if confirm == "y":
|
|
self.questions.pop(question_index)
|
|
print("✅ Question deleted successfully!")
|
|
else:
|
|
print("Delete cancelled.")
|
|
|
|
def run(self):
|
|
"""Main menu loop"""
|
|
while True:
|
|
print(f"\n{'='*50}")
|
|
print("QUIZ QUESTION MANAGER")
|
|
print("=" * 50)
|
|
print(f"Current file: {self.filename}")
|
|
print(f"Total questions: {len(self.questions)}")
|
|
print("\nOptions:")
|
|
print("1. Add new question")
|
|
print("2. View all questions")
|
|
print("3. Edit question")
|
|
print("4. Delete question")
|
|
print("5. Save questions")
|
|
print("6. Exit")
|
|
|
|
choice = input("\nEnter your choice (1-6): ").strip()
|
|
|
|
if choice == "1":
|
|
self.add_question()
|
|
elif choice == "2":
|
|
self.view_questions()
|
|
elif choice == "3":
|
|
self.edit_question()
|
|
elif choice == "4":
|
|
self.delete_question()
|
|
elif choice == "5":
|
|
self.save_questions()
|
|
elif choice == "6":
|
|
save_choice = (
|
|
input("Save changes before exiting? (y/n): ").lower().strip()
|
|
)
|
|
if save_choice == "y":
|
|
self.save_questions()
|
|
print("Goodbye!")
|
|
break
|
|
else:
|
|
print("Invalid choice. Please enter a number between 1 and 6.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
manager = QuestionManager()
|
|
manager.run()
|