This commit is contained in:
louiscklaw
2025-02-01 02:00:51 +08:00
parent a767348238
commit dc9c9468ce
124 changed files with 3736 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = crlf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

181
ITP4459_assignment_2023/.gitignore vendored Normal file
View File

@@ -0,0 +1,181 @@
**/__pycache__
*.del
**/volumes
# Created by https://www.toptal.com/developers/gitignore/api/python
# Edit at https://www.toptal.com/developers/gitignore?templates=python
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
# End of https://www.toptal.com/developers/gitignore/api/python

Binary file not shown.

View File

@@ -0,0 +1,5 @@
---
tags: [python, ITP4459]
---
# daniel_jo

View File

@@ -0,0 +1,13 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
mysql-connector-python = "*"
mysql = "*"
[dev-packages]
[requires]
python_version = "3.10"

View File

@@ -0,0 +1,101 @@
{
"_meta": {
"hash": {
"sha256": "0fd1d2effb4d8a2ed0a80b8b6f653916ad4787fb9b8cd7141b795ea33dabd868"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.10"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"mysql": {
"hashes": [
"sha256:8893cb143a5ac525c49ef358a23b8a0dc9721a95646f7bab6ca2f384c18a6a9a",
"sha256:fd7bae7d7301ce7cd3932e5ff7f77bbc8e34872108252866e08d16d6b8e8de8c"
],
"index": "pypi",
"version": "==0.0.3"
},
"mysql-connector-python": {
"hashes": [
"sha256:145aeb75eefb7425e0a7fb36a4f95ebfe79e06be7c69a4045d34cde95c666dc4",
"sha256:1c0a11f3ffbf850f2ca7b39e6c82021e8de910ddaeffd856e53dca028d21c923",
"sha256:1f399f3c2599d2591854cd0e0a24c7c399dff21ac5accb6e52e06924de29f3f4",
"sha256:232095f0c36266510009b0f1214d2823a649efb8bc511dbab9ce8847f66ab08a",
"sha256:283fe6f647e9d684feb1b7c48fa6a46b1e72c59ecdd6ea2b62392cd80c1a6701",
"sha256:4b2d00c9e2cb9e3d11c57ec411226f43aa627607085fbed661cfea1c4dc57f61",
"sha256:4df11c683924ef34c177a54887dc4844ae735b01c8a29ce6ab92d6d3db7a2757",
"sha256:677b5c6dcaec7e2a4bf95b991a869f4d371114f69a0d9a5bb236e988c8f4c376",
"sha256:6cdba2779bcd16af0ceff0a6e50d33e6664a83f8d17d70524beb6f677a6d1fae",
"sha256:7f7a69db9e0c36764a6c65377f6174aee46e484520e48659e7aa674415b8e192",
"sha256:8c334c41cd1c5bcfa3550340253ef7d9d3b962211f33327c20f69706a0bcce06",
"sha256:8c5bfedc979d7858402f39c20d66a6cf03ca4c960732a98318126c278535ddb2",
"sha256:93b1eb3e07d19a23ccf2605d818aacee0d842b1820bbeef8d0022d8d3d014ab9",
"sha256:992b7a464daa398e86df8c75f7d8cd6044f884ff9087e782120fc8beff96c638",
"sha256:ab13dd6ede0e0e99ba97c73946462c3420625ab6e63fe13b6fc350e30eb3298d",
"sha256:bd52a462759aa324a60054c4b44dc8b32007187a328f72be6b58f193d5e32a91",
"sha256:bdd716b1e162fe4b3887f6617e9ddcfa659ba96a9ddb22feeae208a72f43d22f",
"sha256:be82357cc7e7e1377e2f4f8c18aa89c8aab6c0117155cf9fcf18e3cd0eb6ac8e",
"sha256:c2d20b29fd096a0633f9360c275bd2434d4bcf597281991c4b7f1c820cd07b84",
"sha256:c8bba02501525e1fbbba094a6d8d391d1534e8be41be6396c3e1b9f7d9d13b1c",
"sha256:c990f4c0702d1739076261c4dece1042e1eb18bf34e0d8516d19ec5166a205ce",
"sha256:d6b54656ca131a4f0f17b9d0adddc60f84fd982d64e06360026d5b06e5dbf865",
"sha256:e0299236297b63bf6cbb61d81a9d400bc01cad4743d1abe5296ef349de15ee53",
"sha256:e722b6ffa5b0d7188eebac792b18bc871643db505bf60d0e6bd2859f31e5ed79",
"sha256:fd233c83daaf048c1f9827be984c2721576ae0adf50e139429a06ccd094987d9"
],
"index": "pypi",
"version": "==8.0.32"
},
"mysqlclient": {
"hashes": [
"sha256:0d1cd3a5a4d28c222fa199002810e8146cffd821410b67851af4cc80aeccd97c",
"sha256:828757e419fb11dd6c5ed2576ec92c3efaa93a0f7c39e263586d1ee779c3d782",
"sha256:996924f3483fd36a34a5812210c69e71dea5a3d5978d01199b78b7f6d485c855",
"sha256:b355c8b5a7d58f2e909acdbb050858390ee1b0e13672ae759e5e784110022994",
"sha256:c1ed71bd6244993b526113cca3df66428609f90e4652f37eb51c33496d478b37",
"sha256:c812b67e90082a840efb82a8978369e6e69fc62ce1bda4ca8f3084a9d862308b",
"sha256:dea88c8d3f5a5d9293dfe7f087c16dd350ceb175f2f6631c9cf4caf3e19b7a96"
],
"markers": "python_version >= '3.5'",
"version": "==2.1.1"
},
"protobuf": {
"hashes": [
"sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7",
"sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c",
"sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2",
"sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b",
"sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050",
"sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9",
"sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7",
"sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454",
"sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480",
"sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469",
"sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c",
"sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e",
"sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db",
"sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905",
"sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b",
"sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86",
"sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4",
"sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402",
"sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7",
"sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4",
"sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99",
"sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee"
],
"markers": "python_version >= '3.7'",
"version": "==3.20.3"
}
},
"develop": {}
}

View File

@@ -0,0 +1,116 @@
import tkinter
import tkinter.ttk
import constants
from models import Programme
from widgets import ModuleWidget
# app.py - Main GUI dialog
# TODO: Remove all 'pass' statements and complete the implementation based on class description
class App(tkinter.Tk):
def __init__(self, db):
super().__init__()
self.__db = db
self.refresh_db()
self.__programme = None
self.__modules = None
self.setup_ui()
# NOTE: Do not modify this method
def report_callback_exception(self, *args):
"""When an error is occurred, raise the exception
so main_gui.py will handle the display of error.
"""
raise args[1].with_traceback(args[2])
def refresh_db(self):
# TODO: Fetch all program records from database, and store it in __programme as a list
pass
def setup_ui(self):
self.title(constants.APP_NAME)
self.minsize(640, 480)
# Set weight of column 0 for auto resizing on X axis
self.columnconfigure(0, weight=1)
# Header
frm_top_bar = tkinter.Frame(self)
frm_top_bar.grid(row=0, sticky=tkinter.EW)
tkinter.Label(
frm_top_bar,
text = constants.APP_NAME,
font = ('bold', 16)
).pack(side = tkinter.LEFT, padx=8, pady=8)
tkinter.ttk.Separator(self).grid(row=1, sticky=tkinter.EW)
# Content
frm_content = tkinter.Frame(self)
frm_content.grid(row=2, sticky=tkinter.NSEW)
self.rowconfigure(2, weight=1)
# Options
frm_filter = tkinter.LabelFrame(frm_content, text = "Options")
frm_filter.pack(fill=tkinter.X, side=tkinter.TOP, padx=8, pady=(4, 0))
frm_filter_container = tkinter.Frame(frm_filter)
frm_filter_container.pack(fill=tkinter.BOTH, side=tkinter.TOP, padx=6, pady=(0, 4))
# Options variables
# TODO: Implement tkinter.StringVar for the widgets in 'Options'
# Option widgets
tkinter.Label(
frm_filter_container,
text = "Programme",
).grid(row=0, column=0, padx=(0, 4), sticky=tkinter.W)
# TODO: Implement tkinter.OptionMenu __om_programme_filter
options_list = ["follow methods inside assignment", "follow methods inside assignment 2", "follow methods inside assignment 3", "follow methods inside assignment 4"]
value_inside = tkinter.StringVar(frm_filter_container)
question_menu = tkinter.ttk.OptionMenu(frm_filter_container, value_inside, *options_list)
question_menu.grid(row=0, column=1, padx=(0, 4), sticky=tkinter.W)
print(question_menu["menu"].keys())
# Semester select
tkinter.Label(
frm_filter_container,
text = "Semester",
).grid(row=0, column=2, padx=(4, 4), sticky=tkinter.W)
# TODO: Implement tkinter.OptionMenu __om_semester_filter
# Modules
frm_modules = tkinter.LabelFrame(frm_content, text = "Modules")
frm_modules.pack(fill=tkinter.BOTH, side=tkinter.TOP, expand=1, padx=8, pady=(4, 0))
self.__frm_modules_container = tkinter.Frame(frm_modules)
self.__frm_modules_container.pack(fill=tkinter.BOTH, side=tkinter.TOP, padx=6, pady=(0, 4))
self.__module_widgets = list()
for _ in range(constants.APP_MAX_MODULES):
widget = ModuleWidget(self.__frm_modules_container, self.on_gpa_changed)
widget.get_frame().pack(fill=tkinter.X, side=tkinter.TOP, expand=1, pady=8)
self.__module_widgets.append(widget)
# Bottom bar
frm_bottom_bar = tkinter.Frame(self)
frm_bottom_bar.grid(row=3, sticky=tkinter.EW)
# Result Label
# TODO: Implement tkinter.Label __lbl_result
def setup_module_list(self):
# TODO: Update the ModuleWidget in list __module_widgets to display
# modules for the selected programme and semester
pass
def on_programme_changed(self, _):
# TODO: Get selected programme based on option menu index, and update GUI
pass
def on_semester_changed(self, _):
# TODO: Get modules based on option menu value, and update GUI
pass
def on_gpa_changed(self):
# TODO: Calcuate semester GPA based on self.__modules
pass

View File

@@ -0,0 +1,28 @@
# Database
# TODO: Modify the username and password if needed
DB_HOST = "localhost"
# TODO: fallback root password
DB_USER = "db_user"
DB_PASS = "db_user_pass"
DB_NAME = "itp4459_asg"
# Tkinter GUI
APP_NAME = "GPA Calculator"
APP_MAX_MODULES = 7
# Application logic
GPA_MAPPING = {
"A": 4,
"A-": 3.7,
"B+": 3.3,
"B": 3,
"B-": 2.7,
"C+": 2.3,
"C": 2,
"C-": 1.7,
"D+": 1.3,
"D": 1,
"F": 0
}

View File

@@ -0,0 +1,78 @@
from abc import ABC, abstractmethod
import mysql.connector
# db.py - Database related classes
# NOTE: You do not need to modify this source file
__all__ = (
'MySQLConnection', 'MySQLObject'
)
class MySQLConnection:
"""A class to handle all database related functions for this assingment."""
def __init__(self, host, user, password, database=None):
"""Initialize connection to MySQL server.
Create database if needed automatically.
"""
self.db = mysql.connector.connect(
host=host,
user=user,
password=password
)
if database is not None:
with self.db.cursor() as cursor:
cursor.execute(f"CREATE DATABASE IF NOT EXISTS {database};")
self.db.commit()
self.db.database = database
def close(self):
"""Closes the database connection."""
self.db.close()
def cursor(self):
"""Returns a database cursor, for executing multiple statments
in a single transaction.
"""
return self.db.cursor()
def commit(self):
"""Commit changes to database."""
self.db.commit()
def execute(self, query):
"""Commit: Python -> Database
e.g. CREATE, INSERT and UPDATE
"""
with self.db.cursor() as cursor:
cursor.execute(query)
self.db.commit()
def fetchone(self, query):
"""Fetch: Python <- Database
e.g. SELECT and SHOW
"""
with self.db.cursor() as cursor:
cursor.execute(query)
return cursor.fetchone()
def fetchall(self, query):
"""Fetch: Python <- Database
e.g. SELECT and SHOW
"""
with self.db.cursor() as cursor:
cursor.execute(query)
return cursor.fetchall()
class MySQLObject(ABC):
"""A database managed object."""
@staticmethod
@abstractmethod
def fetchall(db):
"""Returns a list of object imported from database."""
return NotImplemented

View File

@@ -0,0 +1,25 @@
import constants
from db import MySQLConnection
# main_db.py - Program entry point for Part 1 Database
if __name__ == "__main__":
db = MySQLConnection(
host=constants.DB_HOST,
user=constants.DB_USER,
password=constants.DB_PASS
)
with db.cursor() as cursor:
# Recreate database and select as default
cursor.execute(f"DROP DATABASE IF EXISTS {constants.DB_NAME};")
cursor.execute(f"CREATE DATABASE {constants.DB_NAME};")
cursor.execute(f"USE {constants.DB_NAME};")
# Tables
# TODO: Create tables according to the database description
# Records
# TODO: Insert records according to the database description
db.commit()
db.close()

View File

@@ -0,0 +1,42 @@
from tkinter import messagebox
import constants
import traceback
from app import App
from db import MySQLConnection
# main_gui.py - Program entry point for Part 2 GUI
# NOTE: You do not need to modify this source file
if __name__ == "__main__":
app = None
db = None
try:
# Create connection to database
db = MySQLConnection(
host=constants.DB_HOST,
user=constants.DB_USER,
password=constants.DB_PASS,
database=constants.DB_NAME)
# Create and start app
app = App(db)
app.mainloop()
except Exception as e:
# Display error in console
traceback.print_exception(e)
# Display error as message box
errorType = f"{type(e).__name__}"
messagebox.showerror(errorType, e.__str__())
# Close the app window
if app is App:
app.destroy()
# Raise the error again for debugger
raise e
finally:
# Close database connection
if db is MySQLConnection:
db.close()

View File

@@ -0,0 +1,10 @@
from db import MySQLObject
# models.py - Python representation of database table records
# TODO: Remove all 'pass' statements and complete the implementation based on class description
class Programme(MySQLObject):
pass
class Module(MySQLObject):
pass

View File

@@ -0,0 +1,14 @@
@REM feed
@REM python ./src/main_db.py
@REM pip install mysql-connector-python-rf
python ./main_gui.py
@REM python ./src/test_combobox.py
@REM python .\src\options_test.py
@REM pipenv run ".\src\models.py"
@REM pipenv run ".\src\test_models.py"

View File

@@ -0,0 +1,53 @@
import tkinter
from constants import GPA_MAPPING
# widgets.py - Implements a custom GUI widget 'ModuleWidget'
# TODO: Remove all 'pass' statements and complete the implementation based on class description
class ModuleWidget:
"""A supplier class for displaying a module and returning the GPA value.
"""
def __init__(self, master, command):
self.__module = None
self.__command = command
self.__gpa = -1
self.setup_ui(master)
self.set_module(self.__module)
def set_module(self, module = None):
# TODO: When module is provided:
# - Update UI to display module information
# - Enable option menu
# Else
# - Clear entry content
# - Disable option menu
# Finally, update self.__module
pass
def get_frame(self):
return self.__frame
def get_gpa(self):
return self.__gpa
def setup_ui(self, master):
# Variables
# TODO: Implement tkinter.StringVar for the widgets in this class
# Contianer
self.__frame = tkinter.Frame(master)
# Module name
# TODO: Implement tkinter.Entry __ent_name
# Module credit
# TODO: Implement tkinter.Entry __ent_credit
# Grade
# TODO: Implement tkinter.OptionMenu __om_grade
def on_grade_changed(self, _):
# TODO: Get GPA based on combo box value
pass

587
ITP4459_assignment_2023/package-lock.json generated Normal file
View File

@@ -0,0 +1,587 @@
{
"name": "itp4459_assignment_2023",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "itp4459_assignment_2023",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"nodemon": "^2.0.22"
}
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"engines": {
"node": ">=8"
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dependencies": {
"fill-range": "^7.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dependencies": {
"ms": "^2.1.1"
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dependencies": {
"to-regex-range": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"engines": {
"node": ">=4"
}
},
"node_modules/ignore-by-default": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/nodemon": {
"version": "2.0.22",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz",
"integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==",
"dependencies": {
"chokidar": "^3.5.2",
"debug": "^3.2.7",
"ignore-by-default": "^1.0.1",
"minimatch": "^3.1.2",
"pstree.remy": "^1.1.8",
"semver": "^5.7.1",
"simple-update-notifier": "^1.0.7",
"supports-color": "^5.5.0",
"touch": "^3.1.0",
"undefsafe": "^2.0.5"
},
"bin": {
"nodemon": "bin/nodemon.js"
},
"engines": {
"node": ">=8.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/nodemon"
}
},
"node_modules/nopt": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
"integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": "*"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"bin": {
"semver": "bin/semver"
}
},
"node_modules/simple-update-notifier": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz",
"integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==",
"dependencies": {
"semver": "~7.0.0"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/simple-update-notifier/node_modules/semver": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz",
"integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dependencies": {
"is-number": "^7.0.0"
},
"engines": {
"node": ">=8.0"
}
},
"node_modules/touch": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
"integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
"dependencies": {
"nopt": "~1.0.10"
},
"bin": {
"nodetouch": "bin/nodetouch.js"
}
},
"node_modules/undefsafe": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="
}
},
"dependencies": {
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
"anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"requires": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
}
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA=="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"requires": {
"fill-range": "^7.0.1"
}
},
"chokidar": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"requires": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"fsevents": "~2.3.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"requires": {
"ms": "^2.1.1"
}
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"requires": {
"to-regex-range": "^5.0.1"
}
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"optional": true
},
"glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"requires": {
"is-glob": "^4.0.1"
}
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="
},
"ignore-by-default": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="
},
"is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"requires": {
"binary-extensions": "^2.0.0"
}
},
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
},
"is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"requires": {
"is-extglob": "^2.1.1"
}
},
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"nodemon": {
"version": "2.0.22",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz",
"integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==",
"requires": {
"chokidar": "^3.5.2",
"debug": "^3.2.7",
"ignore-by-default": "^1.0.1",
"minimatch": "^3.1.2",
"pstree.remy": "^1.1.8",
"semver": "^5.7.1",
"simple-update-notifier": "^1.0.7",
"supports-color": "^5.5.0",
"touch": "^3.1.0",
"undefsafe": "^2.0.5"
}
},
"nopt": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
"integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
"requires": {
"abbrev": "1"
}
},
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
},
"picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
},
"pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="
},
"readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"requires": {
"picomatch": "^2.2.1"
}
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
},
"simple-update-notifier": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz",
"integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==",
"requires": {
"semver": "~7.0.0"
},
"dependencies": {
"semver": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz",
"integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A=="
}
}
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"requires": {
"has-flag": "^3.0.0"
}
},
"to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"requires": {
"is-number": "^7.0.0"
}
},
"touch": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
"integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
"requires": {
"nopt": "~1.0.10"
}
},
"undefsafe": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="
}
}
}

View File

@@ -0,0 +1,17 @@
{
"name": "itp4459_assignment_2023",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"gitUpdate": "git add . && git commit -m\"update daniel_jo,\" && git push",
"test": "python \"Assignment2223_student_v2/helloworld.py\"",
"dev": "nodemon --ext py --exec \"src/run.bat\""
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"nodemon": "^2.0.22"
}
}

View File

@@ -0,0 +1,41 @@
mysql + gui + python + test case => 600
Reference:
- no seeder
Delivery:
5.1 Test Plan showing the evidence of testing (refer to Test Plan Example for reference)
5.2 Source code for the entire program, with adequate comments.
Submittion:
Submit all your works in a single ZIP file under the name of your student ID and name (e.g., 22XXXXXXX-ChanDaiMan.zip) to Moodle before the assignment deadline. Marks will be deducted for late submissions (e.g., during program demonstration) or even score ZERO mark.
Plagiarism
linting:
Your source code must follow the coding standard stated in PEP 8 Style Guide for Python
Code. Marks may be deducted if the coding standard is not followed.
This assignment is to implement a GPA Calculator using Object Oriented Programming Techniques.
Your programming must be able to show concepts such as
- Abstraction,
- Encapsulation,
- Inheritance and
- Polymorphism.
Your program must be able to do the following:
- Establish connection to MySQL database server and perform read / write function.
- Display a list of programmes, semesters and modules for a semester.
- Calculate the semester GPA based on user input.
UI given.
Things to do
This assignment is divided into two parts: Database and Tkinter GUI.
Study the provided supplier class MySQLConnection in the file db.py. All database related function
must be executed through this class.
Study the provided constants variables in the file constants.py.
TODO comments are included for your easy identification on the parts needed to be completed by
you.

View File

@@ -0,0 +1,34 @@
services:
mysql:
image: mysql:latest
# container_name: db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: itp4459_asg
MYSQL_USER: db_user
MYSQL_PASSWORD: db_user_pass
ports:
- "3306:3306"
volumes:
- db:/var/lib/mysql
phpmyadmin:
image: phpmyadmin/phpmyadmin
# container_name: pma
restart: unless-stopped
links:
- mysql
environment:
PMA_HOST: mysql
PMA_PORT: 3306
PMA_ARBITRARY: 1
PMA_USER: "root"
PMA_PASSWORD: "root"
ports:
- 8081:80
volumes:
db:

View File

@@ -0,0 +1,16 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
mysql = "*"
mysql-connector-python = "*"
mistune = ">=0.7.1"
python-docx = ">=0.8.6"
sympy = ">=0.7.6.1"
[dev-packages]
[requires]
python_version = "3.10"

View File

@@ -0,0 +1,214 @@
{
"_meta": {
"hash": {
"sha256": "6fc14ab26679ecb3c73181a5c3e6daa6ec1d2ff80343097b892a649cd17e0574"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.10"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"lxml": {
"hashes": [
"sha256:01d36c05f4afb8f7c20fd9ed5badca32a2029b93b1750f571ccc0b142531caf7",
"sha256:04876580c050a8c5341d706dd464ff04fd597095cc8c023252566a8826505726",
"sha256:05ca3f6abf5cf78fe053da9b1166e062ade3fa5d4f92b4ed688127ea7d7b1d03",
"sha256:090c6543d3696cbe15b4ac6e175e576bcc3f1ccfbba970061b7300b0c15a2140",
"sha256:0dc313ef231edf866912e9d8f5a042ddab56c752619e92dfd3a2c277e6a7299a",
"sha256:0f2b1e0d79180f344ff9f321327b005ca043a50ece8713de61d1cb383fb8ac05",
"sha256:13598ecfbd2e86ea7ae45ec28a2a54fb87ee9b9fdb0f6d343297d8e548392c03",
"sha256:16efd54337136e8cd72fb9485c368d91d77a47ee2d42b057564aae201257d419",
"sha256:1ab8f1f932e8f82355e75dda5413a57612c6ea448069d4fb2e217e9a4bed13d4",
"sha256:223f4232855ade399bd409331e6ca70fb5578efef22cf4069a6090acc0f53c0e",
"sha256:2455cfaeb7ac70338b3257f41e21f0724f4b5b0c0e7702da67ee6c3640835b67",
"sha256:2899456259589aa38bfb018c364d6ae7b53c5c22d8e27d0ec7609c2a1ff78b50",
"sha256:2a29ba94d065945944016b6b74e538bdb1751a1db6ffb80c9d3c2e40d6fa9894",
"sha256:2a87fa548561d2f4643c99cd13131acb607ddabb70682dcf1dff5f71f781a4bf",
"sha256:2e430cd2824f05f2d4f687701144556646bae8f249fd60aa1e4c768ba7018947",
"sha256:36c3c175d34652a35475a73762b545f4527aec044910a651d2bf50de9c3352b1",
"sha256:3818b8e2c4b5148567e1b09ce739006acfaa44ce3156f8cbbc11062994b8e8dd",
"sha256:3ab9fa9d6dc2a7f29d7affdf3edebf6ece6fb28a6d80b14c3b2fb9d39b9322c3",
"sha256:3efea981d956a6f7173b4659849f55081867cf897e719f57383698af6f618a92",
"sha256:4c8f293f14abc8fd3e8e01c5bd86e6ed0b6ef71936ded5bf10fe7a5efefbaca3",
"sha256:5344a43228767f53a9df6e5b253f8cdca7dfc7b7aeae52551958192f56d98457",
"sha256:58bfa3aa19ca4c0f28c5dde0ff56c520fbac6f0daf4fac66ed4c8d2fb7f22e74",
"sha256:5b4545b8a40478183ac06c073e81a5ce4cf01bf1734962577cf2bb569a5b3bbf",
"sha256:5f50a1c177e2fa3ee0667a5ab79fdc6b23086bc8b589d90b93b4bd17eb0e64d1",
"sha256:63da2ccc0857c311d764e7d3d90f429c252e83b52d1f8f1d1fe55be26827d1f4",
"sha256:6749649eecd6a9871cae297bffa4ee76f90b4504a2a2ab528d9ebe912b101975",
"sha256:6804daeb7ef69e7b36f76caddb85cccd63d0c56dedb47555d2fc969e2af6a1a5",
"sha256:689bb688a1db722485e4610a503e3e9210dcc20c520b45ac8f7533c837be76fe",
"sha256:699a9af7dffaf67deeae27b2112aa06b41c370d5e7633e0ee0aea2e0b6c211f7",
"sha256:6b418afe5df18233fc6b6093deb82a32895b6bb0b1155c2cdb05203f583053f1",
"sha256:76cf573e5a365e790396a5cc2b909812633409306c6531a6877c59061e42c4f2",
"sha256:7b515674acfdcadb0eb5d00d8a709868173acece5cb0be3dd165950cbfdf5409",
"sha256:7b770ed79542ed52c519119473898198761d78beb24b107acf3ad65deae61f1f",
"sha256:7d2278d59425777cfcb19735018d897ca8303abe67cc735f9f97177ceff8027f",
"sha256:7e91ee82f4199af8c43d8158024cbdff3d931df350252288f0d4ce656df7f3b5",
"sha256:821b7f59b99551c69c85a6039c65b75f5683bdc63270fec660f75da67469ca24",
"sha256:822068f85e12a6e292803e112ab876bc03ed1f03dddb80154c395f891ca6b31e",
"sha256:8340225bd5e7a701c0fa98284c849c9b9fc9238abf53a0ebd90900f25d39a4e4",
"sha256:85cabf64adec449132e55616e7ca3e1000ab449d1d0f9d7f83146ed5bdcb6d8a",
"sha256:880bbbcbe2fca64e2f4d8e04db47bcdf504936fa2b33933efd945e1b429bea8c",
"sha256:8d0b4612b66ff5d62d03bcaa043bb018f74dfea51184e53f067e6fdcba4bd8de",
"sha256:8e20cb5a47247e383cf4ff523205060991021233ebd6f924bca927fcf25cf86f",
"sha256:925073b2fe14ab9b87e73f9a5fde6ce6392da430f3004d8b72cc86f746f5163b",
"sha256:998c7c41910666d2976928c38ea96a70d1aa43be6fe502f21a651e17483a43c5",
"sha256:9b22c5c66f67ae00c0199f6055705bc3eb3fcb08d03d2ec4059a2b1b25ed48d7",
"sha256:9f102706d0ca011de571de32c3247c6476b55bb6bc65a20f682f000b07a4852a",
"sha256:a08cff61517ee26cb56f1e949cca38caabe9ea9fbb4b1e10a805dc39844b7d5c",
"sha256:a0a336d6d3e8b234a3aae3c674873d8f0e720b76bc1d9416866c41cd9500ffb9",
"sha256:a35f8b7fa99f90dd2f5dc5a9fa12332642f087a7641289ca6c40d6e1a2637d8e",
"sha256:a38486985ca49cfa574a507e7a2215c0c780fd1778bb6290c21193b7211702ab",
"sha256:a5da296eb617d18e497bcf0a5c528f5d3b18dadb3619fbdadf4ed2356ef8d941",
"sha256:a6e441a86553c310258aca15d1c05903aaf4965b23f3bc2d55f200804e005ee5",
"sha256:a82d05da00a58b8e4c0008edbc8a4b6ec5a4bc1e2ee0fb6ed157cf634ed7fa45",
"sha256:ab323679b8b3030000f2be63e22cdeea5b47ee0abd2d6a1dc0c8103ddaa56cd7",
"sha256:b1f42b6921d0e81b1bcb5e395bc091a70f41c4d4e55ba99c6da2b31626c44892",
"sha256:b23e19989c355ca854276178a0463951a653309fb8e57ce674497f2d9f208746",
"sha256:b264171e3143d842ded311b7dccd46ff9ef34247129ff5bf5066123c55c2431c",
"sha256:b26a29f0b7fc6f0897f043ca366142d2b609dc60756ee6e4e90b5f762c6adc53",
"sha256:b64d891da92e232c36976c80ed7ebb383e3f148489796d8d31a5b6a677825efe",
"sha256:b9cc34af337a97d470040f99ba4282f6e6bac88407d021688a5d585e44a23184",
"sha256:bc718cd47b765e790eecb74d044cc8d37d58562f6c314ee9484df26276d36a38",
"sha256:be7292c55101e22f2a3d4d8913944cbea71eea90792bf914add27454a13905df",
"sha256:c83203addf554215463b59f6399835201999b5e48019dc17f182ed5ad87205c9",
"sha256:c9ec3eaf616d67db0764b3bb983962b4f385a1f08304fd30c7283954e6a7869b",
"sha256:ca34efc80a29351897e18888c71c6aca4a359247c87e0b1c7ada14f0ab0c0fb2",
"sha256:ca989b91cf3a3ba28930a9fc1e9aeafc2a395448641df1f387a2d394638943b0",
"sha256:d02a5399126a53492415d4906ab0ad0375a5456cc05c3fc0fc4ca11771745cda",
"sha256:d17bc7c2ccf49c478c5bdd447594e82692c74222698cfc9b5daae7ae7e90743b",
"sha256:d5bf6545cd27aaa8a13033ce56354ed9e25ab0e4ac3b5392b763d8d04b08e0c5",
"sha256:d6b430a9938a5a5d85fc107d852262ddcd48602c120e3dbb02137c83d212b380",
"sha256:da248f93f0418a9e9d94b0080d7ebc407a9a5e6d0b57bb30db9b5cc28de1ad33",
"sha256:da4dd7c9c50c059aba52b3524f84d7de956f7fef88f0bafcf4ad7dde94a064e8",
"sha256:df0623dcf9668ad0445e0558a21211d4e9a149ea8f5666917c8eeec515f0a6d1",
"sha256:e5168986b90a8d1f2f9dc1b841467c74221bd752537b99761a93d2d981e04889",
"sha256:efa29c2fe6b4fdd32e8ef81c1528506895eca86e1d8c4657fda04c9b3786ddf9",
"sha256:f1496ea22ca2c830cbcbd473de8f114a320da308438ae65abad6bab7867fe38f",
"sha256:f49e52d174375a7def9915c9f06ec4e569d235ad428f70751765f48d5926678c"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==4.9.2"
},
"mistune": {
"hashes": [
"sha256:0246113cb2492db875c6be56974a7c893333bf26cd92891c85f63151cee09d34",
"sha256:bad7f5d431886fcbaf5f758118ecff70d31f75231b34024a1341120340a65ce8"
],
"index": "pypi",
"version": "==2.0.5"
},
"mpmath": {
"hashes": [
"sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f",
"sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"
],
"version": "==1.3.0"
},
"mysql": {
"hashes": [
"sha256:8893cb143a5ac525c49ef358a23b8a0dc9721a95646f7bab6ca2f384c18a6a9a",
"sha256:fd7bae7d7301ce7cd3932e5ff7f77bbc8e34872108252866e08d16d6b8e8de8c"
],
"index": "pypi",
"version": "==0.0.3"
},
"mysql-connector-python": {
"hashes": [
"sha256:0004426e964856148e1cde31e9b8be63ae3013715b048ff0f2ede69a6ddd36f7",
"sha256:016662c6252f2c5f47805d9168187be1316d0c1d7109f9fe668482c3d6e5711d",
"sha256:241483065ad062256985e082e3cbb3e7d1d6d2275cee17c66d22525b09096201",
"sha256:34d5c5f6ec7c1e75bf972def40d097138e097dc694e36dec89a5dd604ef7aada",
"sha256:37eace5b7eb676a41ff1edc5cf6ce4ae1c28406d1a6fe84941e6aa396d688195",
"sha256:3bedf8265fb31698e4144ca54e241e3386802c3a437745b1a536a74cbe7e4fb9",
"sha256:41db4452a99ee28494313eab1aa7749475d3e39bed7b24a0868aee45bd0d9c73",
"sha256:46ff8a10c13f39996d60f45c30cf2ea15e883bc71d58259ed2fea0a5a6fb93a3",
"sha256:4c82fb70f44f2469c0879434c1d8ee3162f56a40cc8f5ca1cc4d97f06c84cd43",
"sha256:7266d7b2550f9fe0cdcea1647aa6aade352e14095042b6a3921c9152cf8543e8",
"sha256:7318f416b9defe84b2bd025304bab62b68f8d8fcbe479af5593161eff12ef169",
"sha256:753d07fb39a67f7f35fe6e6a4fac12008287661de59f9d5c0bf4da3359d83eb8",
"sha256:96f7fb0ccfe96e6e478e5f0f034c99bda961b99ffa1c746cee39cfea45b0c04d",
"sha256:9775331fa60b5d5a6925781d77eee4384e2b54a12dea694ffdefd1cf1a9c0fdb",
"sha256:984f5649e6abee04461d6f52fbc77387d7137b8fd003c54bac66505006f17183",
"sha256:9f5eb33e29742c5f8ef23df2d3f0de0e46f4325e4324016e15aba7f8665a68c0",
"sha256:a632d7b0e569a46e6d44e6cd3f8db747995a787a081870697dbfd3ae18949339",
"sha256:af9feec311d8ea51261e1ef1f959a442708e30f0024d08d0fb537b07a1271634",
"sha256:c20a85a69af41d2d7d5cf52106f0b9473775819d189487c6ff3d3f3946931ca2",
"sha256:d8167868ebad8d78ba69babd028626e96a51365cab76edf735b2559731759b62",
"sha256:db422b19347c5d00e078dd64e281e5b1e5a19a2d972dc2d9733b136d79c34798",
"sha256:e853e12c00e3beabc581f4e039222708ee606fef80a3bac6b1f497ed89a31aea",
"sha256:ea05590cb972b114efa027c343b4b7110d8e8450493984ebfb9a651e27674636",
"sha256:f324233af7ec9fcb19c23096af27662459708c0465886cb017d78ff3f5b78b55",
"sha256:f403ff22d3514d08028590fef463d17dc107ac72ea27a49429614949d82fda40"
],
"index": "pypi",
"version": "==8.0.33"
},
"mysqlclient": {
"hashes": [
"sha256:0d1cd3a5a4d28c222fa199002810e8146cffd821410b67851af4cc80aeccd97c",
"sha256:828757e419fb11dd6c5ed2576ec92c3efaa93a0f7c39e263586d1ee779c3d782",
"sha256:996924f3483fd36a34a5812210c69e71dea5a3d5978d01199b78b7f6d485c855",
"sha256:b355c8b5a7d58f2e909acdbb050858390ee1b0e13672ae759e5e784110022994",
"sha256:c1ed71bd6244993b526113cca3df66428609f90e4652f37eb51c33496d478b37",
"sha256:c812b67e90082a840efb82a8978369e6e69fc62ce1bda4ca8f3084a9d862308b",
"sha256:dea88c8d3f5a5d9293dfe7f087c16dd350ceb175f2f6631c9cf4caf3e19b7a96"
],
"markers": "python_version >= '3.5'",
"version": "==2.1.1"
},
"protobuf": {
"hashes": [
"sha256:03038ac1cfbc41aa21f6afcbcd357281d7521b4157926f30ebecc8d4ea59dcb7",
"sha256:28545383d61f55b57cf4df63eebd9827754fd2dc25f80c5253f9184235db242c",
"sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2",
"sha256:398a9e0c3eaceb34ec1aee71894ca3299605fa8e761544934378bbc6c97de23b",
"sha256:44246bab5dd4b7fbd3c0c80b6f16686808fab0e4aca819ade6e8d294a29c7050",
"sha256:447d43819997825d4e71bf5769d869b968ce96848b6479397e29fc24c4a5dfe9",
"sha256:67a3598f0a2dcbc58d02dd1928544e7d88f764b47d4a286202913f0b2801c2e7",
"sha256:74480f79a023f90dc6e18febbf7b8bac7508420f2006fabd512013c0c238f454",
"sha256:819559cafa1a373b7096a482b504ae8a857c89593cf3a25af743ac9ecbd23480",
"sha256:899dc660cd599d7352d6f10d83c95df430a38b410c1b66b407a6b29265d66469",
"sha256:8c0c984a1b8fef4086329ff8dd19ac77576b384079247c770f29cc8ce3afa06c",
"sha256:9aae4406ea63d825636cc11ffb34ad3379335803216ee3a856787bcf5ccc751e",
"sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db",
"sha256:b6cc7ba72a8850621bfec987cb72623e703b7fe2b9127a161ce61e61558ad905",
"sha256:bf01b5720be110540be4286e791db73f84a2b721072a3711efff6c324cdf074b",
"sha256:c02ce36ec760252242a33967d51c289fd0e1c0e6e5cc9397e2279177716add86",
"sha256:d9e4432ff660d67d775c66ac42a67cf2453c27cb4d738fc22cb53b5d84c135d4",
"sha256:daa564862dd0d39c00f8086f88700fdbe8bc717e993a21e90711acfed02f2402",
"sha256:de78575669dddf6099a8a0f46a27e82a1783c557ccc38ee620ed8cc96d3be7d7",
"sha256:e64857f395505ebf3d2569935506ae0dfc4a15cb80dc25261176c784662cdcc4",
"sha256:f4bd856d702e5b0d96a00ec6b307b0f51c1982c2bf9c0052cf9019e9a544ba99",
"sha256:f4c42102bc82a51108e449cbb32b19b180022941c727bac0cfd50170341f16ee"
],
"markers": "python_version >= '3.7'",
"version": "==3.20.3"
},
"python-docx": {
"hashes": [
"sha256:1105d233a0956dd8dd1e710d20b159e2d72ac3c301041b95f4d4ceb3e0ebebc4"
],
"index": "pypi",
"version": "==0.8.11"
},
"sympy": {
"hashes": [
"sha256:938f984ee2b1e8eae8a07b884c8b7a1146010040fccddc6539c54f401c8f6fcf",
"sha256:e32380dce63cb7c0108ed525570092fd45168bdae2faa17e528221ef72e88658"
],
"index": "pypi",
"version": "==1.11.1"
}
},
"develop": {}
}

View File

@@ -0,0 +1,190 @@
import tkinter
import tkinter.ttk
import constants
from models import Programme
from widgets import ModuleWidget
from pprint import pprint
# app.py - Main GUI dialog
class App(tkinter.Tk):
def __init__(self, db):
super().__init__()
self.__db = db
self.__programme = None
self.__modules = None
self.refresh_db()
self.setup_ui()
# NOTE: Do not modify this method
def report_callback_exception(self, *args):
"""When an error is occurred, raise the exception
so main_gui.py will handle the display of error.
"""
raise args[1].with_traceback(args[2])
def refresh_db(self):
# Fetch all program records from database, and store it in __programme as a list
self.__programme = Programme.fetchall(self.__db)
def setup_ui(self):
self.title(constants.APP_NAME)
self.minsize(640, 480)
# Set weight of column 0 for auto resizing on X axis
self.columnconfigure(0, weight=1)
# Header
frm_top_bar = tkinter.Frame(self)
frm_top_bar.grid(row=0, sticky=tkinter.EW)
tkinter.Label(
frm_top_bar,
text = constants.APP_NAME,
font = ('bold', 16)
).pack(side = tkinter.LEFT, padx=8, pady=8)
tkinter.ttk.Separator(self).grid(row=1, sticky=tkinter.EW)
# Content
frm_content = tkinter.Frame(self)
frm_content.grid(row=2, sticky=tkinter.NSEW)
self.rowconfigure(2, weight=1)
# Options
frm_filter = tkinter.LabelFrame(frm_content, text = "Options")
frm_filter.pack(fill=tkinter.X, side=tkinter.TOP, padx=8, pady=(4, 0))
frm_filter_container = tkinter.Frame(frm_filter)
frm_filter_container.pack(fill=tkinter.BOTH, side=tkinter.TOP, padx=6, pady=(0, 4))
# Options variables
# Option widgets
tkinter.Label(frm_filter_container,text = "Programme",).grid(row=0, column=0, padx=(0, 4))
list_of_programme=[]
for p in self.__programme:
list_of_programme.append(p.get_name())
self.selected_programme = tkinter.StringVar()
self.__om_programme_filter = tkinter.ttk.Combobox(frm_filter_container,textvariable=self.selected_programme, width=50)
self.__om_programme_filter.set('Please select')
self.__om_programme_filter['values'] = list_of_programme
self.__om_programme_filter['state'] = 'readonly'
self.__om_programme_filter.grid(row=0, column=1, padx=(0, 4), sticky=tkinter.EW)
self.__om_programme_filter.bind('<<ComboboxSelected>>', self.on_programme_changed )
# Semester select
tkinter.Label( frm_filter_container, text = "Semester").grid(row=0, column=2, padx=(0, 4))
self.selected_semester = tkinter.StringVar()
self.__om_semester_filter = tkinter.ttk.Combobox(frm_filter_container, textvariable=self.selected_semester)
self.__om_semester_filter.set('Please select')
self.__om_semester_filter['state'] = 'disabled'
self.__om_semester_filter.grid(row=0, column=3, padx=(4, 4))
self.__om_semester_filter.bind('<<ComboboxSelected>>', self.on_semester_changed)
# Modules
frm_modules = tkinter.LabelFrame(frm_content, text = "Modules")
frm_modules.pack(fill=tkinter.BOTH, side=tkinter.TOP, expand=1, padx=8, pady=(4, 0))
self.__frm_modules_container = tkinter.Frame(frm_modules)
self.__frm_modules_container.pack(fill=tkinter.BOTH, side=tkinter.TOP, padx=6, pady=(0, 4))
self.__module_widgets = list()
for _ in range(constants.APP_MAX_MODULES):
widget = ModuleWidget(self.__frm_modules_container, self.on_gpa_changed)
widget.get_frame().pack(fill=tkinter.X, side=tkinter.TOP, expand=1, pady=8)
widget.set_module(None)
self.__module_widgets.append(widget)
# Bottom bar
frm_bottom_bar = tkinter.Frame(self)
frm_bottom_bar.grid(row=3, column=0,padx=(0, 10), pady=(4,4), sticky=tkinter.E)
# Result Label
self.semester_gpa = tkinter.StringVar()
self.lbl_stu_list = tkinter.ttk.Label(frm_bottom_bar, text='Semester GPA: %.2f' % 0, font=("bold",11))
self.lbl_stu_list.grid(row=0, column=0, sticky=tkinter.E)
def setup_module_list(self):
# Update the ModuleWidget in list __module_widgets to display
# modules for the selected programme and semester
current_select_idx = self.__om_semester_filter.current()
selected_semester = list(sorted(self.__modules.keys()))[current_select_idx]
module_in_semester = self.__modules[selected_semester]
for widget in self.__frm_modules_container.winfo_children():
widget.destroy()
self.__module_widgets = list()
i = 0
for _ in range(constants.APP_MAX_MODULES):
widget = ModuleWidget(self.__frm_modules_container, self.on_gpa_changed)
widget.get_frame().pack(fill=tkinter.X, side=tkinter.TOP, expand=1, pady=8)
if (i < len(module_in_semester)):
widget.set_module(module_in_semester[i])
self.__module_widgets.append(widget)
i = i + 1
def on_programme_changed(self, _):
# Get selected programme based on option menu index, and update GUI
for program in self.__programme:
if (self.selected_programme.get() == program.get_name()):
self.__om_semester_filter['values'] = sorted(program.get_modules().keys())
self.__modules = program.get_modules()
break
self.__om_semester_filter['state'] = 'readonly'
self.__om_semester_filter.set('Please select')
for widget in self.__frm_modules_container.winfo_children():
widget.destroy()
self.__module_widgets = list()
for _ in range(constants.APP_MAX_MODULES):
widget = ModuleWidget(self.__frm_modules_container, self.on_gpa_changed)
widget.get_frame().pack(fill=tkinter.X, side=tkinter.TOP, expand=1, pady=8)
widget.set_module(None)
self.__module_widgets.append(widget)
# reset gpa
self.lbl_stu_list.configure(text='Semester GPA: %.2f' % (0))
def on_semester_changed(self, _):
# Get modules based on option menu value, and update GUI
self.setup_module_list()
# reset gpa
self.lbl_stu_list.configure(text='Semester GPA: %.2f' % (0))
def on_gpa_changed(self):
# Calcuate semester GPA based on self.__modules
# 1. For each modules in self.__modules, App class
# 2. Obtain the module credit of current module and add it to the total credits
# 3. Calculate the module gpa by multiplying the current module gpa and module credit
# 4. Add the calculated module gpa to total module gpa
# 5. After all module gpa are calculated, divide the total module gpa by total credits
total = 0
current_select_idx = self.__om_semester_filter.current()
selected_semester = list(sorted(self.__modules.keys()))[current_select_idx]
module_in_semester = self.__modules[selected_semester]
number_of_modules = len(module_in_semester)
should_update = True
all_credit = 0
for i in range(0, number_of_modules):
gpa = self.__module_widgets[i].get_gpa()
if (gpa == -1):
# not selected
should_update = False
else:
credit =self.__module_widgets[i].get_module().get_credit()
all_credit = all_credit + credit
total = total + (gpa * credit)
if should_update:
self.lbl_stu_list.configure(text='Semester GPA: %.2f' % (total / all_credit))

View File

@@ -0,0 +1,26 @@
# Database
# NOTE: Modify the username and password if needed
DB_HOST = "localhost"
DB_NAME = "itp4459_asg"
DB_USER = "db_user"
DB_PASS = "db_user_pass"
# Tkinter GUI
APP_NAME = "GPA Calculator"
APP_MAX_MODULES = 7
# Application logic
GPA_MAPPING = {
"A": 4,
"A-": 3.7,
"B+": 3.3,
"B": 3,
"B-": 2.7,
"C+": 2.3,
"C": 2,
"C-": 1.7,
"D+": 1.3,
"D": 1,
"F": 0
}

View File

@@ -0,0 +1,78 @@
from abc import ABC, abstractmethod
import mysql.connector
# db.py - Database related classes
# NOTE: You do not need to modify this source file
__all__ = (
'MySQLConnection', 'MySQLObject'
)
class MySQLConnection:
"""A class to handle all database related functions for this assingment."""
def __init__(self, host, user, password, database=None):
"""Initialize connection to MySQL server.
Create database if needed automatically.
"""
self.db = mysql.connector.connect(
host=host,
user=user,
password=password
)
if database is not None:
with self.db.cursor() as cursor:
cursor.execute(f"CREATE DATABASE IF NOT EXISTS {database};")
self.db.commit()
self.db.database = database
def close(self):
"""Closes the database connection."""
self.db.close()
def cursor(self):
"""Returns a database cursor, for executing multiple statments
in a single transaction.
"""
return self.db.cursor()
def commit(self):
"""Commit changes to database."""
self.db.commit()
def execute(self, query):
"""Commit: Python -> Database
e.g. CREATE, INSERT and UPDATE
"""
with self.db.cursor() as cursor:
cursor.execute(query)
self.db.commit()
def fetchone(self, query):
"""Fetch: Python <- Database
e.g. SELECT and SHOW
"""
with self.db.cursor() as cursor:
cursor.execute(query)
return cursor.fetchone()
def fetchall(self, query):
"""Fetch: Python <- Database
e.g. SELECT and SHOW
"""
with self.db.cursor() as cursor:
cursor.execute(query)
return cursor.fetchall()
class MySQLObject(ABC):
"""A database managed object."""
@staticmethod
@abstractmethod
def fetchall(db):
"""Returns a list of object imported from database."""
return NotImplemented

View File

@@ -0,0 +1,99 @@
import constants
from db import MySQLConnection
# main_db.py - Program entry point for Part 1 Database
tbl_programme = """
CREATE TABLE IF NOT EXISTS programmes (
programme_id VARCHAR(9) NOT NULL,
programme_name VARCHAR(100) NOT NULL,
PRIMARY KEY (programme_id)
)"""
tbl_module = """
CREATE TABLE IF NOT EXISTS modules (
module_id VARCHAR(7) NOT NULL,
module_name VARCHAR(100) NOT NULL,
module_credit INT,
PRIMARY KEY (module_id)
)"""
tbl_mapping = """
CREATE TABLE IF NOT EXISTS mappings (
mapping_programme_id VARCHAR(9) NOT NULL,
mapping_semester INT NOT NULL,
mapping_module_id VARCHAR(7) NOT NULL,
PRIMARY KEY (mapping_programme_id, mapping_semester, mapping_module_id),
FOREIGN KEY (mapping_programme_id) REFERENCES programmes(programme_id),
FOREIGN KEY (mapping_module_id) REFERENCES modules(module_id)
)"""
radius = (('circle', 1.0),('circle', 2.0),('circle', 3.0))
query = "INSERT INTO circles (name, radius) VALUES (%s, %s)"
insert_programmes = (
('IT114124', "Higher Diploma in AI and Smart Technology"),
('IT114122', "Higher Diploma in Cybersecurity")
)
insert_programme_query = "INSERT INTO programmes (programme_id, programme_name) VALUES (%s, %s)"
insert_modules = (
("LAN3100","English & Communication: Workplace Interaction","6"),
("LAN3103","English & Communication: Workplace Correspondance","6"),
("LAN4107","English & Communication: Reports","9"),
("LAN4108","English & Communication: Persuasive Presentations","9"),
("LAN4101","English & Communication: Promotional Materials","10"),
("LAN3003","Vocational Chinese Communication: Putonghua Conversation and Reports","6"),
("LAN4003","Vocational Chinese Communication: Putonghua Presentations, Administrative and Technical Text Writing","9")
)
insert_modules_query = "INSERT INTO modules (module_id, module_name, module_credit) VALUES (%s, %s, %s)"
insert_mappings = (
("IT114124","1","LAN3100"),
("IT114124","1","LAN3103"),
("IT114124","1","LAN3003"),
("IT114124","2","LAN4107"),
("IT114124","2","LAN4003"),
("IT114124","4","LAN4108"),
("IT114124","5","LAN4101"),
("IT114122","1","LAN3100"),
("IT114122","1","LAN3103"),
("IT114122","1","LAN3003"),
("IT114122","2","LAN4107"),
("IT114122","2","LAN4003"),
("IT114122","4","LAN4108"),
("IT114122","5","LAN4101")
)
insert_mappings_query = "INSERT INTO mappings (mapping_programme_id, mapping_semester, mapping_module_id) VALUES (%s, %s, %s)"
if __name__ == "__main__":
db = MySQLConnection(
host=constants.DB_HOST,
user=constants.DB_USER,
password=constants.DB_PASS
)
with db.cursor() as cursor:
# Recreate database and select as default
cursor.execute(f"DROP DATABASE IF EXISTS {constants.DB_NAME};")
cursor.execute(f"CREATE DATABASE {constants.DB_NAME};")
cursor.execute(f"USE {constants.DB_NAME};")
# Tables
cursor.execute(tbl_programme)
cursor.execute(tbl_module)
cursor.execute(tbl_mapping)
# Records
# cursor.executemany(query, radius)
cursor.executemany(insert_programme_query, insert_programmes)
cursor.executemany(insert_modules_query, insert_modules)
cursor.executemany(insert_mappings_query, insert_mappings)
db.commit()
db.close()

View File

@@ -0,0 +1,42 @@
from tkinter import messagebox
import constants
import traceback
from app import App
from db import MySQLConnection
# main_gui.py - Program entry point for Part 2 GUI
# NOTE: You do not need to modify this source file
if __name__ == "__main__":
app = None
db = None
try:
# Create connection to database
db = MySQLConnection(
host=constants.DB_HOST,
user=constants.DB_USER,
password=constants.DB_PASS,
database=constants.DB_NAME)
# Create and start app
app = App(db)
app.mainloop()
except Exception as e:
# Display error in console
traceback.print_exception(e)
# Display error as message box
errorType = f"{type(e).__name__}"
messagebox.showerror(errorType, e.__str__())
# Close the app window
if app is App:
app.destroy()
# Raise the error again for debugger
raise e
finally:
# Close database connection
if db is MySQLConnection:
db.close()

View File

@@ -0,0 +1,143 @@
import constants
from db import MySQLObject
# models.py - Python representation of database table records
class Programme(MySQLObject):
def __init__(self, id, name):
# initialize the id, name and modules attributes.
self.__id = id # a non-public string to store the id of the programme.
self.__name = name # a non-public string so store the name of the programme.
self.__modules = {} # a non-public dictionary to store Module objects, grouped by semester.
def __eq__(self, other):
# returns true if the id attribute is equal to the other objects id attribute.
return self.__id == other.get_id()
def get_id(self):
# returns the id for this programme.
return self.__id
def get_name(self):
# returns the name for this programme.
return self.__name
def get_modules(self):
# returns Module dictionary object which contains module list of all semesters
return self.__modules
def add_module(self, semester, module):
# append module into list inside modules dictionary,
# using semester as key.
if semester in self.__modules:
# module already contain semester, fall to end of function to add module into semester
pass
else:
# If the semester key is not found in the dictionary,
# an empty list will be created to store the module object, init an empty semester list literally
self.__modules[semester]= []
# the semester will be available as a key after this line, append module into semester
self.__modules[semester].append(module)
def get_modules_by_semester(self, semester):
# returns Module list object from modules dictionary, using semester as key
return self.__modules[semester]
@staticmethod
def fetchall(db):
# overrides the parent class fetchall abstract static method to
# create a list of Programme objects retrieved from database table programme.
# For each of the created Programme objects,
# The modules belongs to the particular programme
output = []
temp= {}
with db.cursor() as cursor:
cursor.execute(f"USE {constants.DB_NAME};")
query = '''
SELECT programme_id, mapping_semester, module_id, programme_name, module_name, module_credit
FROM programmes
INNER JOIN mappings ON programmes.programme_id = mappings.mapping_programme_id
INNER JOIN modules ON mappings.mapping_module_id = modules.module_id;
'''
cursor.execute(query)
records = cursor.fetchall()
# row => module, bloat modules
for row in records:
programme_id = row[0]
mapping_semester = row[1]
module_id = row[2]
programme_name = row[3]
module_name = row[4]
module_credit = row[5]
new_module = Module(module_id, module_name, module_credit)
if not(programme_id in temp):
temp[programme_id] = {'_name': programme_name}
if not(mapping_semester in temp[programme_id] ):
temp[programme_id][mapping_semester] = []
temp[programme_id][mapping_semester].append(new_module)
for programme_id in temp.keys():
new_program = Programme(programme_id, temp[programme_id]['_name'])
# semester
for semester in temp[programme_id].keys():
if (type(semester) == type(1)):
modules_in_semester = temp[programme_id][semester]
for module in modules_in_semester:
new_program.add_module(semester, module)
output.append(new_program)
return output
class Module(MySQLObject):
def __init__(self, id, name, credit):
# initialize the id, name and credit attributes
self.__id = id # a non-public string to store the id of the module
self.__name = name # a non-public string to store the name of the module
self.__credit = credit # a non-public integer to store the credit of the module
def __eq__(self, other):
# returns true of the id attribute is equal to the other objects id attribute
if (hasattr(other,'get_id')):
return self.__id == other.get_id()
else:
return False
def get_id(self):
# returns the id for this module
return self.__id
def get_name(self):
# returns the name for this module
return self.__name
def get_credit(self):
# returns the credit for this module
return self.__credit
@staticmethod
def fetchall(db):
# fetchall overrides the parent class fetchall abstract static method to
# return a list of Module objects retrieved from database table module.
# Module objects will be created and appended into the
# returned list for every records retrieved from database.
query = "select module_id, module_name, module_credit from modules"
output = []
with db.cursor() as cursor:
cursor.execute(f"USE {constants.DB_NAME};")
cursor.execute(query)
records = cursor.fetchall()
for row in records:
module_id = row[0]
module_name = row[1]
module_credit = row[2]
output.append(Module(module_id, module_name, module_credit))
return output

View File

@@ -0,0 +1,11 @@
```batch
# start docker
docker compose up -d
pipenv shell
# inside shell
./run.bat
```

View File

@@ -0,0 +1,10 @@
@REM feed
@REM python ./main_db.py
python "./main_gui.py"
@REM python ./test_combobox.py
@REM python .\options_test.py
@REM python ".\models.py"
@REM python ".\test_models.py"

View File

@@ -0,0 +1,92 @@
import tkinter
from constants import GPA_MAPPING
import tkinter as tk
import tkinter.ttk as ttk
from pprint import pprint
# widgets.py - Implements a custom GUI widget 'ModuleWidget'
# TODO: Remove all 'pass' statements and complete the implementation based on class description
class ModuleWidget:
"""A supplier class for displaying a module and returning the GPA value.
"""
def __init__(self, master, command):
self.__module = None
self.__command = command
self.__gpa = -1
self.setup_ui(master)
self.set_module(None)
def set_module(self, module = None):
if (module != None):
# TODO: When module is provided:
# - Update UI to display module information
# - Enable option menu
self.__var_name.set(module.get_name())
self.__var_credit.set(module.get_credit())
self.__ent_name['state'] = 'readonly'
self.__ent_credit['state'] = 'readonly'
self.__om_grade['state'] = 'readonly'
else:
# - Clear entry content
# - Disable option menu
self.__var_name.set('')
self.__var_credit.set('')
self.__ent_name['state'] = 'disabled'
self.__ent_credit['state'] = 'disabled'
self.__om_grade['state'] = 'disabled'
# Finally, update self.__module
self.__module = module
def get_module(self):
return self.__module
def get_frame(self):
return self.__frame
def get_gpa(self):
return self.__gpa
def setup_ui(self, master):
# Variables
# Contianer
self.__frame = tkinter.Frame(master)
# pprint(self)
# Module name
tkinter.Label(self.__frame, text = "Module", ).grid(row=0, column=0, padx=(0, 4), sticky=tkinter.W)
self.__var_name = tkinter.StringVar(value="")
self.__ent_name = ttk.Entry(self.__frame, textvariable=self.__var_name)
self.__ent_name.config(width=50)
self.__ent_name.grid(row=0, column=1, padx=(4, 4), sticky=tkinter.E+tkinter.W)
# Module credit
tkinter.Label(self.__frame, text = "Credit", ).grid(row=0, column=2, padx=(0, 4), sticky=tkinter.W)
self.__var_credit = tkinter.StringVar(value="")
self.__ent_credit = ttk.Entry(self.__frame, textvariable=self.__var_credit)
self.__ent_credit['state'] = 'disabled'
self.__ent_credit.config(width=5)
self.__ent_credit.grid(row=0, column=3, padx=(4, 4), sticky=tkinter.E+tkinter.W)
# Grade
tkinter.Label(self.__frame, text = "Grade", ).grid(row=0, column=4, padx=(0, 4), sticky=tkinter.W)
self.__var_grade = tk.StringVar()
self.__om_grade = ttk.Combobox(self.__frame, textvariable=self.__var_grade)
self.__om_grade.set('Please select')
self.__om_grade['values'] = [gpa for gpa in GPA_MAPPING.keys()]
self.__om_grade['state'] = 'disabled'
self.__om_grade.config(width=15)
self.__om_grade.grid(row=0, column=5, padx=(0, 4), sticky=tkinter.W)
self.__om_grade.bind('<<ComboboxSelected>>', self.on_grade_changed)
def on_grade_changed(self, _):
# Get GPA based on combo box value
letter_grade = list(GPA_MAPPING.keys())[self.__om_grade.current()]
num_grade = GPA_MAPPING[letter_grade]
self.__gpa = num_grade
self.__command()

View File

@@ -0,0 +1,16 @@
```bash
cd docker
docker compose build
docker compose up -d
cd ..
# after docker up and running
cd python
pipenv shell
pipenv sync
# inside pip environment
python main_db.py
python main_gui.py
```

View File

@@ -0,0 +1,19 @@
pushd docker
docker-compose pull
docker-compose build
docker-compose kill
docker-compose down
timeout 1
rmdir docker\volumes
docker-compose up -d
# docker-compose logs -f
# docker-compose exec -it ubuntu bash
popd

View File

@@ -0,0 +1,106 @@
# TEST CASE:
- Name: Import data to MySQL database server
- Procedure
- Configure DB connection by editing `constants.py`
- Run `main_db.py`
- Start DBMS of your choice and validate the result. (phpmyadmin)
- Expected output
- Python file ran without errors and the database,
- tables and records are successfully created in database.
- Name: GUI startup
- Procedure
- Run application via `main_gui.py`
- do nothing
- Expected output
- show "Please select" at programme option menu
- show "Please select" at Semester option menu
- a empty list of module shown
- Name: Select programme
- Procedure
- Run application via `main_gui.py`
- click programme option menu
- watch option available in option menu
- Expected output
- show a list of programme available in DB
- Name: after user select programme, user proceed to Select semester
- Procedure
- Run application via `main_gui.py`
- click programme option menu
- select "Higher Diploma in Cybersecurity" in option menu
- watch options available in "Semester"
- Expected output
- 1,2,4,5 should be available in semester list
- Name: user Selected semester, user proceed to input Grade
- Procedure
- run procedure "after user select programme, user proceed to Select semester"
- select semester "1"
- Expected output
- the modules available to semester should be listed in the "Modules" frame
- the "Please Select" should be shown in every "Grade" option menu.
- a empty box should be displayed in the Module, Credit and Grade for every modules row for the rest
- Name: user input grade, get total GPA
- Procedure
- run procedure "user Selected semester, user proceed to input Grade"
- input all gpa
- Expected output
- watch the label at the bottom of the form.
- when not all gpa got answered, the gpa won't be updated (keep 0.00)
- when app gpa answered, the result gpa will be displayed (1.23)
- Name: Screen update
- Procedure
- Run same procedure as stated in "GPA Calculation Test #2"
- re-select "Semester" to 2
- re-select "Semester" to 4
- Expected output
- when "Semester" is 2
- Module list should be updated (3 -> 2 items)
- Credit list should be updated (3 -> 2 items)
- Grade list should be selected "Please select"
- when "Semester" is 4
- Module list should be updated (2 -> 1 items)
- Credit list should be updated (2 -> 1 items)
- Grade list should be selected "Please select"
- Name: GPA Calculation Test #1
- Procedure
- Run application via `main_gui.py`
- Perform semester GPA calculation with the input data stated in excepted output.
- Validate the result with expected output.
- Input:
- Programme: Higher Diploma in Cybersecurity; Semester: 2
- Subjects:
- "Vocational Chinese Communication: Putonghua Presentations, Administrative and Technical Text Writing" → A-
- "English & Communication: Reports" → C+
- Expected output
- Semester GPA: 3.00 shown at bottom
- Name: GPA Calculation Test #2
- Procedure
- Run application via `main_gui.py`
- Perform semester GPA calculation with the input data stated in excepted output.
- Validate the result with expected output.
- Input:
- Programme: Higher Diploma in AI and Smart Technology; Semester: 1
- Subjects:
- "Vocational Chinese Communication: Putonghua Conversation and Reports" → A
- "English & Communication: Workplace Interaction" → C-
- "English & Communication: Workplace Correspondance" → B+
- Expected output
- Semester GPA: 3.00 shown at bottom

View File

@@ -0,0 +1 @@
print('helloworld')

View File

@@ -0,0 +1,35 @@
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
options = ['Option 1', 'Option 2', 'Option 3']
variable = tk.StringVar(value=options[0])
def on_option_changed(*args):
print('helloworld')
# This function will be called whenever the selected option changes
# We will use it to update the option list based on the selected option
# Get the selected option
selected_option = variable.get()
# Update the option list based on the selected option
if selected_option == 'Option 1':
new_options = ['Option 1', 'Option 2', 'Option 3']
elif selected_option == 'Option 2':
new_options = ['Option 4', 'Option 5', 'Option 6']
else:
new_options = ['Option 7', 'Option 8', 'Option 9']
# Update the option list in the widget
combobox.configure(values=new_options)
# Create the widget
combobox = ttk.Combobox(root, textvariable=variable, values=options)
combobox.pack()
# Bind the on_option_changed function to the variable
variable.trace('w', on_option_changed)
root.mainloop()

View File

@@ -0,0 +1,41 @@
import tkinter as tk
from tkinter import ttk
from tkinter.messagebox import showinfo
from calendar import month_name
root = tk.Tk()
# config the root window
root.geometry('300x200')
root.resizable(False, False)
root.title('Combobox Widget')
# label
label = ttk.Label(text="Please select a month:")
label.pack(fill=tk.X, padx=5, pady=5)
# create a combobox
selected_month = tk.StringVar()
month_cb = ttk.Combobox(root, textvariable=selected_month)
# get first 3 letters of every month name
month_cb['values'] = [month_name[m][0:3] for m in range(1, 13)]
# prevent typing a value
month_cb['state'] = 'readonly'
# place the widget
month_cb.pack(fill=tk.X, padx=5, pady=5)
# bind the selected value changes
def month_changed(event):
""" handle the month changed event """
showinfo(
title='Result',
message=f'You selected {selected_month.get()}!'
)
month_cb.bind('<<ComboboxSelected>>', month_changed)
root.mainloop()

View File

@@ -0,0 +1,32 @@
from pprint import pprint
import constants
from db import MySQLConnection
from models import Programme
from models import Module
db = MySQLConnection(
host=constants.DB_HOST,
user=constants.DB_USER,
password=constants.DB_PASS
)
list_program = Programme.fetchall(db)
list_module = Module.fetchall(db)
# print(list_program[0].get_modules())
# print(list_program[0].get_modules_by_semester(1))
# print(list_program[0].get_name())
# __eq__
# print(list_program[0] == list_program[0])
# modules
# print(list_program[0].get_modules()[1][0][0].get_id())
# pprint(list_program[0].get_modules())
# pprint(list_program[0].get_modules_by_semester(1)[0])
# print(list_program[0].get_modules()[1][0][0].get_credit())
db.close()
#

Binary file not shown.