From 0ec7a8d918997d97c3bbac2e068a4eb6e64b9b04 Mon Sep 17 00:00:00 2001 From: DefinatlyNotAI Date: Thu, 15 Aug 2024 00:06:16 +0300 Subject: [PATCH] Redocumented everything, changed how passwords work, remade ReadMe.md --- Data.csv | 2 +- DataBase.py | 211 ++++++++++++++++++++++++++++------------------------ ReadMe.md | 169 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 265 insertions(+), 117 deletions(-) diff --git a/Data.csv b/Data.csv index e80afea..d15c9a2 100644 --- a/Data.csv +++ b/Data.csv @@ -1,4 +1,4 @@ -Questions,Question Type,"Difficulty (Easy, Medium, Hard)",Score +Questions,Question Type,Difficulty (Easy, Medium, Hard),Score q0001,t7,Easy,2 q0002,t3,Easy,1 q0003,t2,Medium,2 diff --git a/DataBase.py b/DataBase.py index 96f4b03..ee47987 100644 --- a/DataBase.py +++ b/DataBase.py @@ -18,7 +18,6 @@ import pandas as pd from datetime import datetime - # Configure colorlog for logging messages with colors logger = colorlog.getLogger() logger.setLevel(colorlog.INFO) # Set the log level to INFO to capture all relevant logs @@ -86,71 +85,63 @@ def __disconnect(self): # Reset the cursor object to None self.cursor = None - def __add_exclusion_db( - self, name: str, exclusion_titles: list[str], password: str - ) -> bool | None: + def __add_exclusion_db(self, name: str, exclusion_titles: list[str]) -> bool | None: """ Adds new titles to exclude for a user in the database. Args: name (str): The username of the user. exclusion_titles (list[str]): The titles to exclude. - password (str): The password for the user. If not provided, the function will verify the password using the stored password. Returns: str: A success or error message. """ try: - # Verify the password - if self.verify_password(name, password): - self.__connect() - try: - # Execute a SELECT statement to get the existing titles to exclude for the user - self.cursor.execute( - """SELECT titles_to_exclude FROM Users WHERE username=?""", - (name,), - ) - result = self.cursor.fetchone() - - # If no result is found or the result is None, set initial_titles to "PLACEHOLDER" - if result is None or result[0] is None: + self.__connect() + try: + # Execute a SELECT statement to get the existing titles to exclude for the user + self.cursor.execute( + """SELECT titles_to_exclude FROM Users WHERE username=?""", + (name,), + ) + result = self.cursor.fetchone() - initial_titles = "PLACEHOLDER" - else: - initial_titles = result[0] + # If no result is found or the result is None, set initial_titles to "PLACEHOLDER" + if result is None or result[0] is None: - # Strip the whitespace from the initial_titles - current_titles = initial_titles.strip() + initial_titles = "PLACEHOLDER" + else: + initial_titles = result[0] - # Convert current_titles and titles to sets for easier set operations - current_titles_set = set(current_titles.split(",")) - titles_set = set(exclusion_titles) + # Strip the whitespace from the initial_titles + current_titles = initial_titles.strip() - # Find the new titles to exclude - new_titles_set = titles_set - current_titles_set + # Convert current_titles and titles to sets for easier set operations + current_titles_set = set(current_titles.split(",")) + titles_set = set(exclusion_titles) - # If there are new titles to exclude, update the titles_to_exclude field in the database - if new_titles_set: + # Find the new titles to exclude + new_titles_set = titles_set - current_titles_set - updated_titles = ",".join(list(new_titles_set)) - self.cursor.execute( - """UPDATE Users SET titles_to_exclude = COALESCE(titles_to_exclude ||?, '') WHERE username =?""", - (updated_titles, name), - ) - self.conn.commit() - log.info(f"Successfully updated titles for user {name}.") - return True - else: - log.warning(f"No new titles to add for user {name}.") - return False + # If there are new titles to exclude, update the titles_to_exclude field in the database + if new_titles_set: - except Exception as e: - log.error( - f"An error occurred while adding exclusion titles. as {e}" + updated_titles = ",".join(list(new_titles_set)) + self.cursor.execute( + """UPDATE Users SET titles_to_exclude = COALESCE(titles_to_exclude ||?, '') WHERE username =?""", + (updated_titles, name), ) + self.conn.commit() + log.info(f"Successfully updated titles for user {name}.") + return True + else: + log.warning(f"No new titles to add for user {name}.") return False - else: - log.error(f"Password is incorrect for user {name}.") + + except Exception as e: + log.error( + f"An error occurred while adding exclusion titles. as {e}" + ) return False except Exception as e: log.error(f"An error occurred while adding exclusion titles. as {e}") @@ -263,7 +254,7 @@ def add_db(self, username, exclusion_titles, password) -> bool: self.__disconnect() # Add exclusion titles to the database - sql.add_exclusion_db(username, exclusion_titles, password, "CDB") + sql.add_exclusion_db(username, exclusion_titles, "CDB") log.info("Password Successfully Made") return True @@ -272,13 +263,12 @@ def add_db(self, username, exclusion_titles, password) -> bool: log.error(f"An error occurred while creating the database entry. as {e}") return False - def remove_user(self, username: str, password: str) -> bool: + def remove_user(self, username: str) -> bool: """ Removes a user from the database if the provided username and password match. Args: username (str): The username of the user to be removed. - password (str): The password of the user to be removed. Returns: bool: A success for true or error for false. @@ -300,45 +290,39 @@ def remove_user(self, username: str, password: str) -> bool: log.warning(f"User does not exist: {username}") return False - if self.verify_password(username, password): - # Connect to the database again - self.__connect() + # Connect to the database again + self.__connect() - # Delete the user from the database - self.cursor.execute("DELETE FROM Users WHERE username=?", (username,)) - self.conn.commit() + # Delete the user from the database + self.cursor.execute("DELETE FROM Users WHERE username=?", (username,)) + self.conn.commit() - # Disconnect from the database - self.__disconnect() + # Disconnect from the database + self.__disconnect() - # Return a success message - log.info(f"Successfully removed data for {username}") - return True - else: - # Return an error message if the password is incorrect - log.error(f"Password is incorrect for {username}") - return False + # Return a success message + log.info(f"Successfully removed data for {username}") + return True except Exception as e: # Return an error message if an exception occurs log.error(f"An error occurred while removing the database entry. as {e}") return False @staticmethod - def add_exclusion_db(name, exclusion_titles, password, special=None) -> bool: + def add_exclusion_db(name, exclusion_titles, special=None) -> bool: """ Adds an exclusion database with the given name, titles, and password. Args: name (str): The name of the exclusion database. exclusion_titles (list): A list of titles for the exclusion database. - password (str): The password for the exclusion database. special (str, optional): A special parameter. Defaults to None. """ colorlog.debug(f"Adding exclusion titles for {name}") try: # Attempt to add the exclusion database - value = sql.__add_exclusion_db(name, exclusion_titles, password) + value = sql.__add_exclusion_db(name, exclusion_titles) # Check if the operation was successful if value is False: @@ -347,7 +331,7 @@ def add_exclusion_db(name, exclusion_titles, password, special=None) -> bool: # If special is not provided, add a default value if not special: # Add a default value to the exclusion database - msg = sql.__add_exclusion_db(name, [","], password) + msg = sql.__add_exclusion_db(name, [","]) # Check if the operation was successful if msg is False: return False @@ -580,7 +564,20 @@ def __init__(self): log.info("Database loaded successfully.") @staticmethod - def read_config() -> tuple[int, int, int, int, int, int, bool, str, str, str, list[str]] | bool: + def __error(error): + """ + Logs an error message to the log file. + + Returns: + None + """ + if os.path.exists("ERROR.temp"): + os.remove("ERROR.temp") + with open("ERROR.temp", "w") as f: + f.write(error) + + @staticmethod + def __read_config() -> tuple[int, int, int, int, int, int, bool, str, str, str, list[str]] | bool: """ Reads the configuration from the 'config.json' file and returns a tuple of the configuration parameters. @@ -648,7 +645,7 @@ def read_config() -> tuple[int, int, int, int, int, int, bool, str, str, str, li return False @staticmethod - def read_csv() -> list[list[str]] | bool: + def __read_csv() -> list[list[str]] | bool: """ Reads a CSV file and returns a list of questions. @@ -751,7 +748,8 @@ def read_csv() -> list[list[str]] | bool: log.error(f"Unexpected error: {e}") return False - def generate_data(self, questions, exclude_list) -> tuple[list[list[str]], int, dict[str, float], list[str]] | bool: + def __generate_data(self, questions, exclude_list) -> tuple[ + list[list[str]], int, dict[str, float], list[str]] | bool: """ Generate exam data based on the provided questions and exclude list. @@ -767,7 +765,7 @@ def generate_data(self, questions, exclude_list) -> tuple[list[list[str]], int, while True: # If no questions are provided, read from the CSV file if not questions: - questions = self.read_csv() + questions = self.__read_csv() if questions is False: # Return False if reading from CSV fails return False @@ -854,7 +852,7 @@ def generate_data(self, questions, exclude_list) -> tuple[list[list[str]], int, return False @staticmethod - def create_excel() -> bool: + def __create_excel() -> bool: """ Creates an Excel file from a text file and saves it as an Excel file. @@ -908,7 +906,7 @@ def create_excel() -> bool: return False @staticmethod - def common(password) -> bool: + def __common(password) -> bool: """ Checks if a given password is common or not. @@ -963,7 +961,7 @@ def common(password) -> bool: return True return False - def exam_generator(self, username) -> bool: + def __exam_generator(self, username) -> bool: """ Generates an exam based on the provided username. @@ -975,7 +973,7 @@ def exam_generator(self, username) -> bool: """ # Read the CSV file containing the exam questions - questions = self.read_csv() + questions = self.__read_csv() if questions is False: # If the CSV file is not read successfully, return False return False @@ -988,7 +986,7 @@ def exam_generator(self, username) -> bool: return False # Generate the exam data based on the questions and excluded titles - temp = self.generate_data(questions, Exclude_list) + temp = self.__generate_data(questions, Exclude_list) if temp is False: # If the exam data is not generated successfully, return False return False @@ -1029,7 +1027,7 @@ def exam_generator(self, username) -> bool: time.sleep(1) # Create an Excel file based on the exam data - msg = self.create_excel() + msg = self.__create_excel() if msg is False: # If the Excel file is not created successfully, return False return False @@ -1058,11 +1056,12 @@ def api(self): """ try: # Read configuration data from the config file - config_data = self.read_config() + config_data = self.__read_config() # If config data is False, return False if config_data is False: - return False + self.__error("CCD") + exit("Failed to read config file") # Unpack config data into global variables global TOTAL_DATA_AMOUNT, MINIMUM_TYPES, HARD_DATA_AMOUNT, MEDIUM_DATA_AMOUNT, EASY_DATA_AMOUNT, TOTAL_POINTS, DEBUG_DB @@ -1088,11 +1087,13 @@ def api(self): ) if sql.verify_password(USERNAME, PASSWORD): # Generate exam and log result - if self.exam_generator(USERNAME): + if self.__exam_generator(USERNAME): log.info("Exam generated successfully based on the request") else: log.error("Failed to generate exam") + self.__error("UKF") else: + self.__error("IC") log.error("Wrong password given") elif API == "RUC": @@ -1104,7 +1105,7 @@ def api(self): if re.match(username_regex, USERNAME): if re.match(password_regex, PASSWORD): # Check if password is common or already exists - if not self.common(PASSWORD) and not sql.password_exists(PASSWORD): + if not self.__common(PASSWORD) and not sql.password_exists(PASSWORD): log.info( f"A request has been made to create a new user by the following username {USERNAME}" ) @@ -1125,38 +1126,50 @@ def api(self): ) elif API == "RDU": - # Request to add exclusion titles to the database - log.info( - f"A request has been made to add the following exclusion titles {EXCLUDE} to the database for user {USERNAME}" - ) - # Add exclusion titles to database and log result - if sql.add_exclusion_db(USERNAME, EXCLUDE, PASSWORD): - log.info("Exclusion titles added successfully based on the request") + if sql.verify_password(USERNAME, PASSWORD): + # Request to add exclusion titles to the database + log.info( + f"A request has been made to add the following exclusion titles {EXCLUDE} to the database for user {USERNAME}" + ) + # Add exclusion titles to database and log result + if sql.add_exclusion_db(USERNAME, EXCLUDE): + log.info("Exclusion titles added successfully based on the request") + else: + log.error("Failed to add exclusion titles to database") + self.__error("UKF") else: - log.error("Failed to add exclusion titles to database") + self.__error("IC") + log.error("Wrong password given") elif API == "RUR": - # Request to remove a user from the database - log.info( - f"A request has been made to remove the user {USERNAME} from the database" - ) - # Remove user from database and log result - if sql.remove_user(USERNAME, PASSWORD): - log.info("User removed successfully based on the request") + if sql.verify_password(USERNAME, PASSWORD): + # Request to remove a user from the database + log.info( + f"A request has been made to remove the user {USERNAME} from the database" + ) + # Remove user from database and log result + if sql.remove_user(USERNAME): + log.info("User removed successfully based on the request") + else: + log.error(f"Failed to remove {USERNAME} from database") + self.__error("UKF") else: - log.error(f"Failed to remove {USERNAME} from database") + self.__error("IC") + log.error("Wrong password given") else: log.error(f"Invalid API inputted: {API}") + self.__error("IAPI") except Exception as e: # Log any unexpected errors log.error(f"Unexpected error occurred: {e}") + self.__error("UKF") sql = SQL(db_name="users.db") log = LOG(filename="DataBase.log") - # Initialize the database db = DATABASE() +db.api() diff --git a/ReadMe.md b/ReadMe.md index aea0ec6..2366f4b 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -11,69 +11,204 @@ ## Table of Contents 🔍 +- [Introduction](#introduction-) +- [Integration](#integration-) +- [Logging](#logging-information-) +- [File Formatting](#file-formats-) + - [CSV](#csv-format-) + - [JSON](#config-json-format-) +- [API Expectations](#database-expectations-api-) + - [REC](#rec-api-) + - [RUC](#ruc-api-) + - [RUD](#rud-api-) + - [RUR](#rur-api-) +- [Error Handling](#error-messages-) +- [Dependencies](#dependencies-) +- [License](#license-) +- [Contact](#contact-) ## Introduction 🌟 +Exam Generator is a REST API backend service that generates exams for a given subject +and a given number of questions. The API is built using python and uses SQLite as the database. +Here's a brief overview of the project: + +**DataBase.py**: This file contains the SQL class for the database operations. +It uses SQLAlchemy to interact with the SQLite database. +It also includes methods to create tables, insert data, update data, delete data, and query data. +it also contains the Database class, which represents a usage of the exam generation. +It has properties included in the `config.json` file. +It is responsible for generating exams. +It takes a subject and the number of questions as parameters, +and returns a list of randomly selected questions from the configuration file. + +The API is designed to be scalable and can handle a large number of questions for each subject. +It also includes error handling and logging to ensure the smooth operation of the application. + +The project is built using Python and SQLite as well as tiny amounts of PowerShell, +it uses SQLite as the database. The API is designed to be RESTful +and can be used with any frontend framework or application. +The API is LOCAL - So only having the sourcecode in your server allows the API's Usage + +The project is open-source and available on GitHub at [https://github.com/DefinetlyNotAI/Test-generator](https://github.com/DefinetlyNotAI/Test-generator). +Feel free to clone the repository and contribute to the project. ## Integration 🛠️ +Integrating this project is super easy; + +1) Move this whole directory to your server's directory +2) Make your server able to communicate and access `config.json` as well as the `DataBase.py` +3) All the server needs to do is modify the `config.json` file to include required parameters, then execute `DataBase.py` +4) Once executed a `Exam.xslx` file is produced, you can access it for you newly generated dataset +5) OPTIONAL: A `.log` is also generated, in case of errors, fallback to it +The `DataBase.py` will not communicate back to you in any way, in case of errors it won't communicate still +Reason being this has been tested vigorously, and only fails if the end user/front-end fails +The file will however create a `ERROR.temp` file incase a user fault occurs, it will contain predetermined messages, +If you want to use this feature, you must include a check on your end for the `ERROR.temp` file, and delete it +after reading its contents, The list of pre-defined errors are [here](#error-messages-) ## Logging Information 📝 +Everything that occurs is logged to a special `.log` file, it contains everything, You cannot disable this feature! +It does a neat log that contains the following headers:- +- **Timestamp** Includes date and time of the log +- **LOG Level** Includes the log level, ranges from INFO, WARNING, ERROR, CRITICAL +- **Message** Includes the log message -## File Formats 📃 +It's all in a neat fashion, every time the software is re-opened anew, a special series of `-` appear to show +it's a new log, without deleting previous ones. -### API JSON Format 🦾 +If debugging, the CLI will show special `colorlog` messages that include exact realtime logging. +## File Formats 📃 +These will explain exactly the required formats, and tips on how to use them ### CSV Format 📃 +This usually should be static and human-controlled +Each item must be separated by a comma, this produces a set, each set is seperated by a new line, +An example of a `.csv` file; -### CONFIG JSON Format 👨‍💻 - +```csv +Questions,Question Type,Difficulty (Easy, Medium, Hard),Score +q0001,t7,Easy,2 +q0002,t3,Easy,1 +q0003,t2,Medium,2 +q0004,t2,Medium,2 +q0005,t1,Easy,1 +``` +In each line, only 4 items are allowed based on the headers +`Questions, Question Type, Difficulty (Easy, Medium, Hard), Score` -## Database Expectations API 🗂️ +### CONFIG JSON Format 👨‍💻 +This should always change and be computer-controlled + +In the `config.json` file, there are 10 keys: + +- `hard_data_to_use`: Integer: Amount of question to be classified as hard. +- `medium_data_to_use`: Integer: Amount of question to be classified as medium. +- `easy_data_to_use`: Integer: Amount of question to be classified as easy. +- `minimum_titles`: Integer: The minimum amount of separate titles the exam should have. (For better chances of succeeding have it ~20% of total questions or else it results in impossible requests) +- `total_points`: Integer: Exact amount of points the generated data set should have. +- `use_debug_(ONLY_IF_YOU_DEVELOPED_THIS!)`: Boolean: DO NOT TAMPER. +- `api`: String: API type to use, refer to [this](#database-expectations-api-) part of the documentation. +- `username`: String: The USER that will be acted upon the database +- `password`: String: The USER's PASSWORD that will be acted upon the database +- `exclusion_titles`: List[String]: Titles you want to exclude from generation, this is very sensitive and CAN result in impossible requests + +And the base file should look like this: + +```json +{ + "hard_data_to_use": 2, + "medium_data_to_use": 1, + "easy_data_to_use": 3, + "minimum_titles": 3, + "total_points": 10, + "use_debug_(ONLY_IF_YOU_DEVELOPED_THIS!)": true, + "api": "", + "username": "", + "password": "", + "exclusion_titles": [","] +} +``` + +The json file when read should always return a tuple of 10 items, +in order `tuple[int, int, int, int, int, int, bool, str, str, str, list[str]]` + +Not following the format will result in a false bool thrown, which results in an error. + +Please note the harsher you are in the rules the more impossible requests error will generate, +try to always have a ratio between given data (`.csv`) and rules. + +To always make sure it will generate, try knowing the total questions you need (lets say 5) and go to your dataset, +and use the first 5 to generate your configuration for yourself. +## Database Expectations API 🗂️ ### REC API 🧠 Request Exam Creation +This will request to create an exam based on the users username and password, +It outputs an `.xslx` file -Disclaimer: -URL is the URL of the question's image (If not there, it would be None). -You may develop an API to communicate with an Image Hosting Platform to convert the URL to an image. +### RUC API 👤 -### RUG API 👤 - -Request User Generation +Request User Creation +This will request creating a username with the provided password, +Saves to the `users.db` ### RUD API 🔝 Request User DB Update +This requests adding extra exclusion titles to the username provided, requires a password + ### RUR API 🚫 Request User Removal +Requests to remove the user via the password given as well. + +## Error Messages 🐛 + +In your end have a daemon thread that always checks if `ERROR.temp` exists, if it does, quickly read its contents (1 liner) +and delete the file. + +The contents include:- +- **IC** - Incorrect Credentials - The user has inputted wrong username or password. +- **UKF** - Unknown Failure - A very broad error, Check the logs for the exact source - Requires human intervention +- **IAPI** - Invalid API - The config file's API is wrong and not part of the 4 [API's](#database-expectations-api-) +- **CCD** - Corrupted Configuration Data - The configuration given is completely wrong and not valid - Check logs for further details + +You may automate special web error messages based on those. -## Setup 🛠️ +## Dependencies 📦 -### Dependencies 📦 +Just install the dependencies using: +```bash +pip install -r requirements.txt +``` -## Contributing 👨‍💻 +No need to update them later on to mediate crash risks, +but you may rerun the command to check for compatible newer versions. -Contributions to the Exam Generator Server are welcome. -Please review the [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines on submitting pull requests. +```text +DateTime~=5.5 +colorlog~=6.8.2 +pandas~=2.2.2 +``` -Make sure to adhere to the [Code of Conduct](CODE_OF_CONDUCT.md) before submitting a pull request. +You are advised to run this software in a separate python environment. ## License 📄