diff --git a/.flake8 b/.flake8 index f2b41384..b1663a2f 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,4 @@ +[flake8] max-line-length=100 application_import_names=projectt ignore=P102,B311,W503,E226,S311,W504,F821 diff --git a/.gitignore b/.gitignore index 894a44cc..b03a35e7 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,5 @@ venv.bak/ # mypy .mypy_cache/ +.gitignore +app.db diff --git a/.hound.yml b/.hound.yml new file mode 100644 index 00000000..09d8c1ba --- /dev/null +++ b/.hound.yml @@ -0,0 +1,3 @@ +flake8: + enabled: true + config_file: .flake8 diff --git a/Pipfile b/Pipfile index 72b70b6f..093ccf15 100644 --- a/Pipfile +++ b/Pipfile @@ -5,11 +5,15 @@ verify_ssl = true [dev-packages] flake8 = "*" +autopep8 = "*" [packages] +flake8 = "*" +googletrans = "*" [requires] python_version = "3.7" [scripts] -lint = "python -m flake8" \ No newline at end of file +lint = "python -m flake8" +start = "python -m project" diff --git a/Pipfile.lock b/Pipfile.lock index 79354a3c..d99005ce 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a376db0bd471e38a7080cd854c46349b46922db98afeaf83d17b84923fbe9710" + "sha256": "13553bd5f9066cb1a19d9a178d8292c247deabb4d8ce7aa337c7aec6cba962f0" }, "pipfile-spec": 6, "requires": { @@ -15,8 +15,94 @@ } ] }, - "default": {}, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "entrypoints": { + "hashes": [ + "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", + "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" + ], + "version": "==0.3" + }, + "flake8": { + "hashes": [ + "sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661", + "sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8" + ], + "index": "pypi", + "version": "==3.7.7" + }, + "googletrans": { + "hashes": [ + "sha256:bb4e2a6ee72a24870e696a14238b7305f855a05206ecae10bd50052651984cc5" + ], + "index": "pypi", + "version": "==2.4.0" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pycodestyle": { + "hashes": [ + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" + ], + "version": "==2.5.0" + }, + "pyflakes": { + "hashes": [ + "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", + "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" + ], + "version": "==2.1.1" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, "develop": { + "autopep8": { + "hashes": [ + "sha256:33d2b5325b7e1afb4240814fe982eea3a92ebea712869bfd08b3c0393404248c" + ], + "index": "pypi", + "version": "==1.4.3" + }, "entrypoints": { "hashes": [ "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", @@ -26,11 +112,11 @@ }, "flake8": { "hashes": [ - "sha256:6d8c66a65635d46d54de59b027a1dda40abbe2275b3164b634835ac9c13fd048", - "sha256:6eab21c6e34df2c05416faa40d0c59963008fff29b6f0ccfe8fa28152ab3e383" + "sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661", + "sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8" ], "index": "pypi", - "version": "==3.7.6" + "version": "==3.7.7" }, "mccabe": { "hashes": [ @@ -48,10 +134,10 @@ }, "pyflakes": { "hashes": [ - "sha256:5e8c00e30c464c99e0b501dc160b13a14af7f27d4dffb529c556e30a159e231d", - "sha256:f277f9ca3e55de669fba45b7393a1449009cff5a37d1af10ebb76c52765269cd" + "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", + "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" ], - "version": "==2.1.0" + "version": "==2.1.1" } } } diff --git a/README.md b/README.md index 697c2bf7..ec45cec1 100644 --- a/README.md +++ b/README.md @@ -30,16 +30,32 @@ You should be using [Pipenv](https://pipenv.readthedocs.io/en/latest/). Take a l # Project Information -`# TODO` +TEAM GREEN GREENHOUSES + +Team Leader: + + [FliX](https://github.com/FelixRandle) + +Team Members: + + [Throupy](https://github.com/Throupy) + + [Zomatree](https://github.com/zomatree) ## Description -`# TODO` +Find it tough to manage all those events you partake in? + +Google Calendar too tough to understand, don't worry, we only have like, 4 buttons. + +With all new dark mode to ensure that you're eyes never hurt after a long event management session. ## Setup & Installation -`# TODO` +Run `pipenv install --dev` from the repo's directory. ## How do I use this thing? -`# TODO` +Run `pipenv start run` from the repo's directory. + +Register in the application. Add new events and take a look at all your stored events. diff --git a/project/UI/application.py b/project/UI/application.py new file mode 100644 index 00000000..439e5b7b --- /dev/null +++ b/project/UI/application.py @@ -0,0 +1,408 @@ +"""Green Greenhouses Calendar Application.""" +import tkinter as tk +from tkinter import messagebox +import string +import json +from ..backend.DBHandler import DBHandler +from .eventViewer import EventViewer +from .userHandling import Register, Login + + +class Application(tk.Tk): + """Main Application class inheriting from tkinter.Tk.""" + + def __init__(self): + """Initialise Application class.""" + super().__init__() + self.grid_columnconfigure(0, weight=1000) + + self.resizable(False, False) + self.geometry("500x500") + + self.dbh = DBHandler() + + self.pages = {} + + self.tk_setPalette(background="#F0F0F0", foreground="#000000") + self.dark_mode = tk.BooleanVar() + self.dark_mode.trace("w", self.change_dark_mode) + self.dark_mode.set(False) + + self.create_pages() + + self.protocol("WM_DELETE_WINDOW", self.close_window) + + def close_window(self): + with open("./project/backend/utils/jsonMessage.json") as jsonMessages: + jsonMessages = json.load(jsonMessages) + ttl = "EventManager" + if (messagebox.askyesno( + ttl, jsonMessages["escapeOne"])): + if (messagebox.askyesno( + ttl, jsonMessages["escapeTwo"])): + if (messagebox.askyesno( + ttl, jsonMessages["escapeThree"])): + if (messagebox.askyesno( + ttl, jsonMessages["escapeFour"])): + if (messagebox.askyesno( + ttl, jsonMessages["escapeFive"])): + pass + else: + self.quit() + + def change_dark_mode(self, *args): + """Switch the application between light and dark mode.""" + if not self.dark_mode.get(): + self.tk_setPalette(background="#F0F0F0", foreground="#000000") + else: + self.tk_setPalette(background="#292D32", foreground="#2e3237") + + def create_pages(self): + """ + Create the applications pages. + + Arguments: + N/A + Returns: + N/A + """ + self.pages[LoginPage] = LoginPage(self) + self.pages[HomePage] = HomePage(self) + self.pages[AddEventPage] = AddEventPage(self) + self.pages[CalendarPage] = CalendarPage(self) + + self.change_page(LoginPage) + + def change_page(self, new_page): + """ + Change the currently displayed page. + + Arguments: + new_page - The page to change to + Returns: + N/A + """ + for page in self.grid_slaves(): + page.grid_remove() + if new_page == CalendarPage: + self.pages[CalendarPage].get_events() + self.pages[new_page].grid(column=0, row=0) + + +class LoginPage(tk.Frame): + """Landing page for application.""" + + def __init__(self, parent): + """Initialise Home Page class.""" + super().__init__(parent) + self.grid_columnconfigure(0, weight=1000) + + self.parent = parent + self.create_widgets() + + def create_widgets(self): + """ + Create the pages widgets. + + Arguments: + N/A + Returns: + N/A + """ + self.title = tk.Label( + self, text="Welcome to\nGreen Greenhouses\nEvent Manager", + font=("Helvetica", 24, "bold") + ) + self.title.grid(row=0, column=0, sticky="ew", pady=(10, 0)) + + self.dark_mode = tk.Checkbutton(self, text="Dark Mode", + variable=self.parent.dark_mode) + self.dark_mode.grid(row=0, column=0, sticky="ne", padx=100) + + self.registerButton = tk.Button( + self, text="REGISTER", width=10, + font=("Arial", 20, "italic"), + command=self.register + ) + self.registerButton.grid(row=5, column=0, pady=80) + + self.loginButton = tk.Button( + self, text="LOGIN", width=10, + font=("Arial", 20, "italic"), + command=self.login + ) + self.loginButton.grid(row=10, column=0) + + def register(self): + Register(self.parent.dbh) + + def login(self): + loginWindow = Login(self.parent.dbh) + self.wait_window(loginWindow.window) + if loginWindow.loggedIn: + self.parent.change_page(HomePage) + + +class HomePage(tk.Frame): + """Landing page for application.""" + + def __init__(self, parent): + """Initialise Home Page class.""" + super().__init__(parent) + self.grid_columnconfigure(0, weight=1000) + + self.parent = parent + self.create_widgets() + + def create_widgets(self): + """ + Create the pages widgets. + + Arguments: + N/A + Returns: + N/A + """ + self.title = tk.Label( + self, text="Main Menu", + font=("Helvetica", 24, "bold") + ) + self.title.grid(row=0, column=0, pady=(10, 0), sticky="ew") + + self.dark_mode = tk.Checkbutton(self, text="Dark Mode", + variable=self.parent.dark_mode) + self.dark_mode.grid(row=0, column=0, sticky="ne", padx=100) + + self.view_events = tk.Button( + self, text="VIEW EVENTS", width=12, + font=("Arial", 20, "italic"), + command=lambda: self.parent.change_page(CalendarPage) + ) + self.view_events.grid(row=5, column=0, pady=100) + + self.add_event = tk.Button( + self, text="ADD EVENT", width=12, + font=("Arial", 20, "italic"), + command=lambda: self.parent.change_page(AddEventPage) + ) + self.add_event.grid(row=10, column=0) + + +class AddEventPage(tk.Frame): + """Page where you can add events to the calendar.""" + + def __init__(self, parent): + """ + Initialise the Add Event page. + + Arguments: + None + Returns: + None + """ + super().__init__() + + self.parent = parent + self.create_widgets() + self.months = { + "1": 31, + "2": 28, + "3": 31, + "4": 30, + "5": 31, + "6": 30, + "7": 31, + "8": 31, + "9": 30, + "10": 31, + "11": 30, + "12": 31} + + def create_widgets(self): + """ + Create the pages widgets. + + Arguments: + N/A + Returns: + N/A + """ + self.title = tk.Label( + self, + text="Add an event", font=(30)) + self.title.grid(column=1) + # Name + self.name = tk.Label( + self, + text="Name ", + font=(24)) + self.name.grid(row=1, sticky="E") + self.nameEntry = tk.Text( + self, + height=2, width=49) + self.nameEntry.grid(row=1, column=1) + # Location + self.location = tk.Label( + self, + text="Location ", + font=(24),) + self.location.grid(row=2, sticky="E") + self.locationEntry = tk.Text( + self, + height=2, width=49) + self.locationEntry.grid(row=2, column=1) + # Date + self.date = tk.Label( + self, + text="Date ", + font=(24)) + self.date.grid(row=3, sticky="E") + self.dateSpinBoxs = tk.Frame(self) + self.timeEntryD = tk.Spinbox(self.dateSpinBoxs, + width=14, + from_=1, + to=31) + + self.timeEntryM = tk.Spinbox(self.dateSpinBoxs, + width=15, + from_=1, + to=12) + + self.timeEntryY = tk.Spinbox(self.dateSpinBoxs, + width=14, + from_=2019, + to=3000) + + self.timeEntryD.grid(row=3, column=1) + self.timeEntryM.grid(row=3, column=2) + self.timeEntryY.grid(row=3, column=3) + self.dateSpinBoxs.grid(row=3, column=1) + # Description + self.description = tk.Label( + self, + text="Description ", + font=(24)) + self.description.grid(row=4, sticky="N") + self.descriptionEntry = tk.Text( + self, height=20, + width=49) + self.descriptionEntry.grid(row=4, column=1) + + # Submit Button + self.submitBack = tk.Frame(self) + self.submitBtn = tk.Button( + self.submitBack, + text="Submit ✔", + command=lambda: self.inputCheck()) + self.submitBtn.grid(row=1, sticky="W") + # back button + self.back = tk.Button( + self.submitBack, + text="Back", + command=lambda: + self.parent.change_page(HomePage)) + self.back.grid(row=1, column=1, sticky="W") + self.submitBack.grid(column=1) + + def IsDaysCorrect(self, list): + """""" + if self.months[str(int(list[1]))] >= int(list[0]): + return True + return False + + def inputCheck(self): + """ + the Function that checks to see if all date boxs + have been filled out correctly. + + Argumnets: + None + Returns: + None + """ + + dateList = [ + self.timeEntryD.get(), + self.timeEntryM.get(), + self.timeEntryY.get()] + + if ( + any( + letter for letter in self.descriptionEntry.get( + "1.0", + tk.END) + if letter.lower() in string.ascii_lowercase) and + any( + letter for letter in self.locationEntry.get( + "1.0", + tk.END) + if letter.lower() in string.ascii_lowercase) and + self.IsDaysCorrect(dateList) and + any( + letter for letter in self.nameEntry.get( + "1.0", + tk.END) + if letter.lower() in string.ascii_lowercase)): + self.parent.dbh.addEvent( + self.nameEntry.get("1.0", tk.END), + self.parent.dbh.logged_in_user, + self.locationEntry.get("1.0", tk.END), + ".".join(dateList), + self.descriptionEntry.get("1.0", tk.END)) + + else: + messagebox.showinfo( + "Missing arguments", + "It seems you didnt fill out all the info boxs \n" + + "Or you didnt fill the date correctly\n" + + "please fill them all correctly and try again.") + self.parent.pages[CalendarPage].create_widgets() + self.parent.change_page(HomePage) + + +class CalendarPage(tk.Frame): + """Example page for Application.""" + + def __init__(self, parent): + """ + Initialise the Example page. + + Arguments: + None + Returns: + None + """ + super().__init__() + + self.parent = parent + self.create_widgets() + + def create_widgets(self): + """ + Create the pages widgets. + + Arguments: + None + Returns: + None + """ + self.back = tk.Button( + self, + text="Back", + command=lambda: + self.parent.change_page(HomePage)) + + self.back.grid(row=0, column=0, sticky="W") + self.grid_columnconfigure(0, weight=1) + + def get_events(self): + # Fetch all events + + events = self.parent.dbh.fetchEvents(self.parent.dbh.logged_in_user) + # Event format: + # (ID, name, location, Date, description)-- + + self.event_viewer = EventViewer(self) + self.event_viewer.grid(row=10, column=0) + for event in events: + self.event_viewer.add_event(event) diff --git a/project/UI/eventViewer.py b/project/UI/eventViewer.py new file mode 100644 index 00000000..b90ec8d0 --- /dev/null +++ b/project/UI/eventViewer.py @@ -0,0 +1,111 @@ +"""Event viewer specific class.""" +import tkinter as tk + + +def retag(tag, *args): + """Add the given tag as the first bindtag for every widget passed in""" + for widget in args: + widget.bindtags((tag,) + widget.bindtags()) + + +class EventViewer(tk.Frame): + """Shows all event information in a single frame.""" + + def __init__(self, parent): + """Initialise the Event Viewer class.""" + super().__init__(parent) + + self.canvas = tk.Canvas(self) + self.display_frame = tk.Frame(self.canvas) + self.scrollbar = tk.Scrollbar(self, orient="vertical", width=20, + command=self.canvas.yview) + self.canvas.configure(yscrollcommand=self.scrollbar.set) + + self.grid_rowconfigure(0, weight=100) + self.scrollbar.grid(row=0, column=1, sticky="ns") + self.canvas.grid(row=0, column=0) + self.canvas.create_window( + (240, 0), + window=self.display_frame, + anchor="n") + + self.display_frame.bind("", self.scrollMove) + self.display_frame.grid_columnconfigure(0, weight=1000) + self.display_frame.configure() + self.events = {} + + def scrollMove(self, event): + """Update canvas information from scrollbar movement.""" + self.canvas.configure(scrollregion=self.canvas.bbox("all"), + width=480, height=450) + + def add_event(self, event): + """Add a new event to the viewer.""" + event_frame = Event(self.display_frame, event) + event_frame.grid(column=0, pady=5, padx=5) + + self.events.update({event: event_frame}) + + +class EventPopup(tk.Toplevel): + def __init__(self, parent, event): + super().__init__(parent) + self.parent = parent + self.event = event + + self.create_widgets() + + def create_widgets(self): + self.title = tk.Label(self, text="Are you sure you wish to delete this event?", + font=("Helvetica", 15, "bold italic")) + self.title.grid(row=0, column=0, pady=5) + + self.event_view = Event(self, self.event) + self.event_view.grid(row=5, column=0, padx=5, pady=5) + + self.yes = tk.Button(self, text="YES", command=self.delete_event) + self.yes.grid(row=10, column=0, padx=(0, 50), pady=5) + + self.no = tk.Button(self, text="NO", command=self.close_window) + self.no.grid(row=10, column=0, padx=(50, 0), pady=5) + + def delete_event(self): + # Insert magic to delete + self.close_window() + + def close_window(self): + self.destroy() + + +class Event(tk.Frame): + eventLabels = { + 0: "ID", + 1: "userID", + 2: "Name", + 3: "Location", + 4: "Date", + 5: "Description" + } + + def __init__(self, parent, event): + super().__init__(parent) + self.parent = parent + self.event = event + + self.config(relief=tk.GROOVE, borderwidth=3) + + self.create_widgets() + + self.bind("", self.show_event) + + def create_widgets(self): + for key, name in self.eventLabels.items(): + widget = tk.Label( + self, + text=name + " - " + str(self.event[key])) + retag(self, widget) + + widget.pack() + + def show_event(self, event): + EventPopup(self, self.event) diff --git a/project/UI/userHandling.py b/project/UI/userHandling.py new file mode 100644 index 00000000..16a23ae4 --- /dev/null +++ b/project/UI/userHandling.py @@ -0,0 +1,152 @@ +import tkinter as tk +import tkinter.ttk as ttk +import tkinter.messagebox as mb +import hashlib + + +def hashPassword(password): + """Hash a password using sha512.""" + return hashlib.sha512(password.encode()).hexdigest() + + +class Register: + def __init__(self, db, windowTitle="REGISTER"): + self.db = db + + self.windowTitle = windowTitle + # Create a window. + self.window = tk.Toplevel() + + # Set window variables. + self.window.resizable(0, 0) + self.window.title(self.windowTitle) + self.window.geometry("196x290") + + # Create Labels + self.notificationBox = tk.Label( + self.window, width=19, height=2, + font=("Helvetica", "12", "bold", "italic"), text=self.windowTitle) + self.notificationBox.grid(row=0, column=0, pady=5) + tk.Label(self.window, width=18, height=1, font=("Helvetica", "10"), + text="Username").grid(row=1, column=0, pady=5) + self.userNameEntry = ttk.Entry(self.window, width=18) + self.userNameEntry.grid(row=2, column=0) + tk.Label(self.window, width=18, height=1, font=("Helvetica", "10"), + text="Password").grid(row=3, column=0, pady=5) + self.passwordEntryFirst = ttk.Entry(self.window, width=18, show="*") + self.passwordEntryFirst.grid(row=4, column=0) + tk.Label(self.window, width=18, height=1, font=("Helvetica", "10"), + text="Confirm Password").grid(row=5, column=0, pady=5) + self.passwordEntrySecond = ttk.Entry(self.window, width=18, show="*") + self.passwordEntrySecond.grid(row=6, column=0) + + ttk.Button(self.window, width=18, text="Register", + command=self.attemptCreate).grid(row=7, column=0, pady=10) + ttk.Button(self.window, width=18, text="Cancel", + command=self.closeWindow).grid(row=8, column=0) + # Bind the create function to the return key. + self.window.bind("", self.attemptCreate) + self.window.grab_set() + self.window.protocol("WM_DELETE_WINDOW", lambda: self.window.grab_release()) + self.userNameEntry.focus_set() + + def resetNotification(self): + self.notificationBox["text"] = self.windowTitle + + def attemptCreate(self, event=None): + failReason = None + username = self.userNameEntry.get().lower() + password = self.passwordEntryFirst.get() + if password != self.passwordEntrySecond.get(): + failReason = "Password Mismatch" + elif password == "": + failReason = "No password entered." + elif (len(password) < 8) or (password.lower() == password): + failReason = "Password does not\nmeet requirements." + elif (len(username) < 5): + failReason = "Username must be at\nleast 5 characters" + else: + password = hashPassword(password) + result = self.db.addUser(username, password) + if not result: + failReason = "User already exists" + if failReason is not None: + self.notificationBox["text"] = failReason + self.userNameEntry.delete(0, tk.END) + self.passwordEntryFirst.delete(0, tk.END) + self.passwordEntrySecond.delete(0, tk.END) + self.window.after(1500, self.resetNotification) + else: + self.name = username + self.loggedIn = True + self.type = "user" + mb.showinfo(self.windowTitle, "User successfully created.") + self.window.grab_release() + self.window.destroy() + + def closeWindow(self): + self.window.grab_release() + self.window.destroy() + + +class Login: + def __init__(self, db, windowTitle="LOGIN"): + + self.db = db + # Create user variables + self.loggedIn = False + + self.windowTitle = windowTitle + # Create a login window. + self.window = tk.Toplevel() + # Set login screen variables. + self.window.resizable(0, 0) + self.window.title("Login") + self.window.geometry("186x220") + # Create labels. + self.notificationBox = tk.Label( + self.window, width=18, height=1, + font=("Helvetica", "12", "bold", "italic"), + text=self.windowTitle, anchor="center") + self.notificationBox.grid(row=0, column=0, pady=5) + tk.Label(self.window, width=18, height=1, font=("Helvetica", "10"), + text="Username").grid(row=1, column=0, pady=5) + self.userNameEntry = ttk.Entry(self.window, width=18) + self.userNameEntry.grid(row=2, column=0) + tk.Label(self.window, width=18, height=1, font=("Helvetica", "10"), + text="Password").grid(row=3, column=0, pady=5) + self.passwordEntry = ttk.Entry(self.window, width=18, show="*") + self.passwordEntry.grid(row=4, column=0) + + ttk.Button(self.window, width=18, text="Login", + command=self.attemptLogin).grid(row=5, column=0, pady=10) + + ttk.Button(self.window, width=18, text="Cancel", + command=self.closeWindow).grid(row=6, column=0) + + # Bind button to window + self.window.bind("", self.attemptLogin) + self.window.grab_set() + self.window.protocol("WM_DELETE_WINDOW", + lambda: self.window.grab_release()) + self.userNameEntry.focus_set() + + def resetNotification(self): + self.notificationBox["text"] = self.windowTitle + + def attemptLogin(self, event=None): + username = self.userNameEntry.get().lower().strip() + password = hashPassword(self.passwordEntry.get()) + result = self.db.tryLogin(username, password) + if result is not True: + self.passwordEntry.delete(0, tk.END) + self.notificationBox["text"] = "Login Failed" + self.window.after(1500, self.resetNotification) + else: + self.loggedIn = True + self.window.grab_release() + self.window.destroy() + + def closeWindow(self): + self.window.grab_release() + self.window.destroy() diff --git a/project/__main__.py b/project/__main__.py index e69de29b..a95cb780 100644 --- a/project/__main__.py +++ b/project/__main__.py @@ -0,0 +1,6 @@ +from .UI.application import Application + + +if __name__ == "__main__": + app = Application() + app.mainloop() diff --git a/project/backend/DBHandler.py b/project/backend/DBHandler.py new file mode 100644 index 00000000..47fa65a4 --- /dev/null +++ b/project/backend/DBHandler.py @@ -0,0 +1,142 @@ +"""Database handling for calandar application.""" +import sqlite3 +import json +from tkinter import messagebox + + +class DBHandler: + """Database Handler Class.""" + + def __init__(self): + """Initialise the database connection. + + Arguments: + None + Returns: + None + """ + self.conn = sqlite3.connect("app.db") + self.cursor = self.conn.cursor() + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS events( + ID INTEGER PRIMARY KEY, + userID INTEGER, + name TEXT, + location TEXT, + date TEXT, + description TEXT)""") + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS users( + ID INTEGER PRIMARY KEY, + username TEXT UNIQUE, + password TEXT)""") + with open("./project/backend/utils/jsonMessage.json") as jsonMessages: + self.jsonMessages = json.load(jsonMessages) + # self.populate() + + self.logged_in_user = None + + def fetchEvents(self, userID): + """Fetch event from the database. + + Arguments: + None + Returns: + None + """ + self.cursor.execute("""SELECT * FROM events WHERE userID = ?""", (userID,)) + # Fetch rows + rows = self.cursor.fetchall() + return rows + + def addEvent(self, name, userID, location, date, description): + """Create a new event and add it to the database. + + Arguments: + name - name of event + location - location of event + date - date of event + description - description of event + Returns: + None + """ + self.cursor.execute('''INSERT INTO events(name,userid,location, + date,description) + VALUES(?,?,?,?,?)''', (name, + userID, + location, + date, + description)) + self.conn.commit() + + def removeEvent(self, id): + """Remove an event from the database. + + Arguments: + ID - The ID of the event that is to be removed + Returns: + None + """ + # Remove the event with the specified ID + self.cursor.execute("DELETE FROM events WHERE id = ?", (id,)) + print(f"Deleted event with ID: {id}") + self.conn.commit() + + def addUser(self, username, password): + """ + Add a user to the database. + + Arguments: + entryName - the name to be added, + entryPassword - the password for the user + Returns: + True if user was successfully added to the database, + False otherwise. + """ + try: + self.cursor.execute('''INSERT INTO users(username,password) + VALUES(?,?)''', (username, password)) + self.conn.commit() + return True + except sqlite3.IntegrityError: + return False + + def tryLogin(self, username, password): + """Attempt user login. + + Arguments: + username - user's username + password - user's password + Returns: + True if user was successfully logged in, + False otherwise. + """ + if len(username) < 1 or len(password) < 1: + messagebox.showerror("Error", + self.jsonMessages['populateLogin'] + ) + return False + self.cursor.execute("""SELECT * FROM users WHERE username=? AND password=?""", + (username, password)) + rows = self.cursor.fetchall() + if len(rows) == 0: + messagebox.showwarning("No user found", + self.jsonMessages['noUser'] + ) + return False + + self.logged_in_user = rows[0][0] + return True + + # TESTING - ONLY USED IN DEVELOPMENT. REMOVE UPON RELEASE!!! + def populate(self): + """Use to populate the database with sample data.""" + self.cursor.execute(""" INSERT INTO events + (name,location, + description,date) + VALUES(?,?,?,?)""", ("Meeting", + "Office on 4th street", + """Talk about upcoming + work events""", + "12/02")) + self.conn.commit() diff --git a/project/backend/app.db b/project/backend/app.db new file mode 100644 index 00000000..aca62c99 Binary files /dev/null and b/project/backend/app.db differ diff --git a/project/backend/calender.py b/project/backend/calender.py new file mode 100644 index 00000000..844aac8e --- /dev/null +++ b/project/backend/calender.py @@ -0,0 +1,64 @@ +"""Calendar application for OOP tkinter.""" +import tkinter as tk +from DBHandler import DBHandler +from pages.addeventpage import AddEventPage +from pages.calendarpage import CalendarPage +from pages.loginpage import LoginPage + + +class Application(tk.Tk): + """Application class inheriting from tk.Tk.""" + + def __init__(self): + """ + Initialise the Application class. + + Arguments: + None + Returns: + None + """ + self.dbh = DBHandler() + super().__init__() + + self.create_pages() + + def create_pages(self): + """ + Create the pages used inside the application. + + Arguments: + None + Returns: + None + """ + self.pages = {} + + self.pages[AddEventPage] = AddEventPage(self) + + self.pages[AddEventPage] = AddEventPage(self) + + self.pages[CalendarPage] = CalendarPage(self) + + self.pages[LoginPage] = LoginPage(self) + + self.change_page(LoginPage) + + def change_page(self, new_page): + """ + Change the currently displayed page. + + Arguments: + newFrame -- The frame to change to + """ + # Remove anything currently placed on the screen + for page in self.grid_slaves(): + if page.grid_info()["column"] == 0: + page.grid_forget() + # Place our new page onto the screen + self.pages[new_page].grid(row=0, column=0) + + +if __name__ == "__main__": + app = Application() + app.mainloop() diff --git a/project/backend/pages/addeventpage.py b/project/backend/pages/addeventpage.py new file mode 100644 index 00000000..a0e8bf67 --- /dev/null +++ b/project/backend/pages/addeventpage.py @@ -0,0 +1,69 @@ +"""Add event page for the calendar application.""" +import tkinter as tk + + +class AddEventPage(tk.Frame): + """Page where you can add events to the calendar.""" + + def __init__(self, parent): + """ + Initialise the Add Event page. + + Arguments: + None + Returns: + None + """ + super().__init__() + self.parent = parent + self.create_widgets() + + def create_widgets(self): + """ + Create the pages widgets. + + Arguments: + None + Returns: + None + """ + self.title = tk.Label(self, text="Add an event", font=(30)) + self.title.grid(column=1) + # Name + self.name = tk.Label(self, text="Name", font=(24)) + self.name.grid(row=1, sticky="E") + self.nameEntry = tk.Entry(self) + self.nameEntry.grid(row=1, column=1) + # Location + self.location = tk.Label(self, text="Location", font=(24)) + self.location.grid(row=2, sticky="E") + self.locationEntry = tk.Entry(self) + self.locationEntry.grid(row=2, column=1) + # Date + self.date = tk.Label(self, text="Date", font=(24)) + self.date.grid(row=3, sticky="E") + self.dateEntry = tk.Entry(self) + self.dateEntry.grid(row=3, column=1) + # Description + self.description = tk.Label(self, text="Description", font=(24)) + self.description.grid(row=4, sticky="E") + self.descriptionEntry = tk.Text(self, height=5, width=15) + self.descriptionEntry.grid(row=4, column=1) + # Submit Button + if len(self.nameEntry.get()) == 0 or \ + len(self.locationEntry.get()) == 0 or \ + len(self.dateEntry.get()) == 0 or \ + len(self.descriptionEntry.get("1.0")) == 0: + # Need some sort of UI goodness here! + print("[AddEventPage] Not all boxes filled") + # Break out + self.submitBtn = tk.Button(self, + text="Submit ✔", + command=lambda: + self.parent.dbh.addEvent( + self.nameEntry.get(), + self.locationEntry.get(), + self.dateEntry.get(), + self.descriptionEntry.get("1.0")) + ) + self.submitBtn.grid() diff --git a/project/backend/pages/calendarpage.py b/project/backend/pages/calendarpage.py new file mode 100644 index 00000000..417ac76a --- /dev/null +++ b/project/backend/pages/calendarpage.py @@ -0,0 +1,70 @@ +"""Main page for the calendar application.""" +import tkinter as tk +from pages.addeventpage import AddEventPage + + +class CalendarPage(tk.Frame): + """Example page for Application.""" + + eventLabels = { + 0: "ID", + 1: "Name", + 2: "Location", + 3: "Date", + 4: "Description" + } + + def __init__(self, parent): + """ + Initialise the Example page. + + Arguments: + None + Returns: + None + """ + super().__init__() + self.parent = parent + self.create_widgets() + + def create_widgets(self): + """ + Create the pages widgets. + + Arguments: + None + Returns: + None + """ + # Create an add event button + self.addEventBtn = tk.Button(self, + text="[+] Add event", + command=lambda: + self.parent.change_page(AddEventPage)) + self.addEventBtn.grid() + # Populate button needs to be removed on release. Preferablly. + self.populateBtn = tk.Button(self, + text="[Pop]", + command=self.parent.dbh.populate + ) + self.populateBtn.grid() + # Fetch all events + events = self.parent.dbh.fetchEvents() + # Event format: + # (ID, name, location, description)-- + for event in events: + print(event) + eventID = event[0] + string = "" + for value in event: + string += self.eventLabels[event.index(value)] + " - " + string += str(value) + "\n" + eventPanel = tk.PanedWindow(self, bd=5, relief="sunken", width=600) + eventPanel.grid() + eventPanel.add(tk.Label(self, text=string)) + # Events [0] is ALWAYS the ID, which is what is needed in dbh.removeEvent + deleteButton = tk.Button(self, + text="[X] Delete above event", + command=lambda eventID=eventID: + self.parent.dbh.removeEvent(eventID)) + deleteButton.grid() diff --git a/project/backend/pages/loginpage.py b/project/backend/pages/loginpage.py new file mode 100644 index 00000000..a43aba5a --- /dev/null +++ b/project/backend/pages/loginpage.py @@ -0,0 +1,45 @@ +"""Login page for the calendar application.""" +import tkinter as tk + + +class LoginPage(tk.Frame): + """Page where you can add events to the calendar.""" + + def __init__(self, parent): + """ + Initialise the Add Event page. + + Arguments: + None + Returns: + None + """ + super().__init__() + self.parent = parent + self.create_widgets() + + def create_widgets(self): + """ + Create the pages widgets. + + Arguments: + None + Returns: + None + """ + self.title = tk.Label(self, text="Please log-in", font=(24)) + self.title.grid(column=1) + self.usernameLabel = tk.Label(self, text="Username") + self.usernameLabel.grid(row=1, sticky="E") + self.passwordLabel = tk.Label(self, text="Password") + self.passwordLabel.grid(row=2, sticky="E") + self.usernameEntry = tk.Entry(self) + self.usernameEntry.grid(row=1, column=1) + self.passwordEntry = tk.Entry(self, show="*") + self.passwordEntry.grid(row=2, column=1) + self.loginButton = tk.Button(self, + text="Login", + command=lambda: + self.parent.dbh.tryLogin(self.usernameEntry.get(), + self.passwordEntry.get())) + self.loginButton.grid(row=4, column=1) diff --git a/project/backend/translator.py b/project/backend/translator.py new file mode 100644 index 00000000..5c9d0098 --- /dev/null +++ b/project/backend/translator.py @@ -0,0 +1,17 @@ +"""The backend of the traslation.""" +from googletrans import Translator + +translator = Translator() + + +def translate(inp, lang: str): + """ + Translates text from english to another language. + + Parameters: + - list/string - a text you want to translate + - string - the language you want to translate into + Returns: + - string - the translated text. + """ + return translator.translate(inp, dest=lang).text diff --git a/project/backend/utils/jsonMessage.json b/project/backend/utils/jsonMessage.json new file mode 100644 index 00000000..ff8ab14f --- /dev/null +++ b/project/backend/utils/jsonMessage.json @@ -0,0 +1,10 @@ +{ + "populateLogin":"You must populate the username and password fields!", + "noUser":"No user found with these credentials,\nplease ensure your username and password were entered correctly.", + "userAdded":"User has been added to the database!", + "escapeOne":"Are you sure you wish to quit?", + "escapeTwo":"Promise you're sure?", + "escapeThree":"I don't want you changing you're mind once I've closed\nBe absolutely certain.", + "escapeFour":"I see how it is, are you running off to Google Calendar again?", + "escapeFive":"One last change, do you want to not leave the application." +}