diff --git a/DataSources/dataSources.py b/DataSources/dataSources.py index 3085e88..a6adeaa 100644 --- a/DataSources/dataSources.py +++ b/DataSources/dataSources.py @@ -75,40 +75,38 @@ def checkIntegrity(self) -> bool: def instantiateClasses(self, missing_tables: list[str] | list[models.IModel]) -> None: - # if len(additionalSetup) != 0: - - # # DEBUG - # md = alchem.MetaData() - # md.reflect(bind=self.dbEngineInstance) - # existing_tables = md.tables.keys() - - - # for table_name in missing_tables: - # tableClass = next((cl for cl in additionalSetup if cl.name == table_name), None) - # tableClass.metadata.create_all(self.dbEngineInstance) - # return - for table_name in missing_tables: if isinstance(table_name, str): table_name = next((cl for cl in self.tableCreators if cl.__tablename__ == table_name), None) table_name.__table__.create(self.dbEngineInstance) - # raise AttributeError(f"{table_name} is not name of table or IModel subclass") def LoadSavedState(self) -> None: """Collect all data saved in data source and instantiate adjacent model objects """ - with orm.Session(self.dbEngineInstance) as session: + Session = orm.sessionmaker(bind=self.dbEngineInstance) + with Session() as session: for tC in self.tableCreators: try: + #TODO to pewnie będzie do poprawy przy zapisywaniu innych obiektów result = session.execute(alchem.select(tC)).all() + if len(result) == 0: continue for readObj in result: - tC(**readObj) + toinit = readObj[0].__dict__ + tC(**toinit) except Exception as e: print(e) continue + models.IModel.run_loading = False + def Save(self, obj: models.IModel): + Session = orm.sessionmaker(bind=self.dbEngineInstance) + with Session() as session: + session.add(obj) + session.commit() + session.refresh(obj) + self.dbEngineInstance.dispose() class XLSXHandler(IDataSource): def __init__(self, path: str) -> None: diff --git a/Tests/dataGenerators.py b/Tests/dataGenerators.py index 3af9248..8fc7457 100644 --- a/Tests/dataGenerators.py +++ b/Tests/dataGenerators.py @@ -1,6 +1,10 @@ import pytest from faker import Faker -import personalSecrets as ps +try: + import personalSecrets as ps + secretsAvailable = True +except ImportError: + secretsAvailable = False from models import Contact, User from collections.abc import Callable diff --git a/Tests/test_templates.py b/Tests/test_templates.py index 4ca16cd..e7565d7 100644 --- a/Tests/test_templates.py +++ b/Tests/test_templates.py @@ -1,9 +1,8 @@ -from pathlib import Path import pytest import os -import sqlite3 from SQLite_operations_test import * from models import Template +from sqlalchemy import Engine testSamplesPath = os.path.join(os.getcwd(), "Tests/Samples") @@ -18,7 +17,7 @@ }.items()]) def getTemplate(request) -> Template: with open(request.param[1], "rb") as rb: - result = Template(request.param[0], rb.read()) + result = Template(_name=request.param[0], _content=rb.read()) return result @@ -29,17 +28,15 @@ def getTemplate(request) -> Template: ) def test_template_sqlite_insertable(recreateDatabase, getDatabaseHandler, getTemplate): t = getTemplate - dbh = getDatabaseHandler - engine = dbh.dbEngineInstance + dbh: DatabaseHandler = getDatabaseHandler + engine: Engine = dbh.dbEngineInstance - Session = sessionmaker(bind=engine) - with Session() as session: - session.add(t) - session.commit() - session.refresh(t) - + dbh.Save(t) + + Session = sessionmaker(engine) with Session() as session: selectionResult: list[Template] = session.query(Template).filter_by(name=t.name).all() + engine.dispose() assert selectionResult is not None assert len(selectionResult) == 1 diff --git a/additionalTableSetup.py b/additionalTableSetup.py index 13aa5c2..ab91c8a 100644 --- a/additionalTableSetup.py +++ b/additionalTableSetup.py @@ -17,13 +17,7 @@ class MessageAttachment(declarative_base()): class SendAttempt(declarative_base()): __tablename__ = 'Send_attempts' - message_id = Column(Integer, ForeignKey(Message.message_id), nullable=False) - attempt = Column(Integer, nullable=False) + message_id = Column(Integer, ForeignKey(Message.message_id), primary_key=True) + attempt = Column(Integer, primary_key=True) timestamp = Column(TIMESTAMP, nullable=False) error_message = Column(VARCHAR(200), server_default='') - - __table_args__ = ( - PrimaryKeyConstraint(message_id, attempt), - ForeignKeyConstraint([message_id], [Message.message_id]) - ) - diff --git a/interface.py b/interface.py index 0e4cb03..342f08d 100644 --- a/interface.py +++ b/interface.py @@ -1,187 +1,334 @@ -from collections.abc import Iterable -import tkinter as tk -from tkinter import simpledialog -from tkinter import ttk +from collections.abc import Callable, Iterable +from typing import Literal, Any, NoReturn +from tkinter import Menu, simpledialog, ttk, Listbox, Tk, Text, Button, Frame, Label, Entry, Scrollbar, Toplevel, Misc, messagebox, Menubutton, RAISED +from tkinter.ttk import Combobox +from tkinter.constants import NORMAL, DISABLED, BOTH, RIDGE, END, LEFT, RIGHT, TOP, X, Y, INSERT, SEL, WORD +from models import Contact, IModel, Template +from tkhtmlview import HTMLLabel + +def errorHandler(xd, exctype: type, excvalue: Exception, tb): + msg = f"{exctype}: {excvalue}, {tb}" + print(msg) + simpledialog.messagebox.showerror("Error", msg) + +Tk.report_callback_exception = errorHandler + +class LoginWindow(): + def __init__(self, root): + self.root = root + self.root.title("Logowanie") + self.root.configure(bg="lightblue") + self.root.geometry("300x200") + + def prepareInterface(self): + label = Label(self.root, text="MailBuddy", bg="lightblue", font=("Helvetica", 24)) + label.pack(pady=20) + + self.username_entry = Entry(self.root, bg="white", fg="black") + self.username_entry.pack(pady=5) + + self.password_entry = Entry(self.root, bg="white", fg="black", show="*") + self.password_entry.pack(pady=5) + + login_button = Button(self.root, text="Zaloguj się", bg="lightblue", fg="black", command=self.login) + login_button.pack(pady=10) + + def login(self): + username = self.username_entry.get() + password = self.password_entry.get() + + # TODO połączyć z faktycznym logowaniem, tj tworzeniem Senderów i Readerów + # dane logowania czy są test + if username == "test" and password == "test": + self.root.destroy() + app = AppUI() + app.prepareInterface() + app.run() + else: + # TODO Może wystarczy pokazywać czerwony napis pod "Zaloguj się" + komunikat + messagebox.showerror("Błąd logowania", "Nieprawidłowa nazwa użytkownika lub hasło") class AppUI(): def __init__(self) -> None: - self.root = tk.Tk() + self.root = Tk() self.grupy = {} - self.szablony = [] + self.szablony: list[Template] = [] + self.template_window: TemplateEditor = None def prepareInterface(self) -> None: self.root.title("MailBuddy") self.root.configure(bg="black") self.root.minsize(width=800, height=470) - + self.root.protocol("WM_DELETE_WINDOW", self.__exit_clicked) + + self.__create_menu() self.__create_navigation() self.__create_notification_pane() self.__create_mailing_group_pane() self.__create_template_pane() self.__create_mail_input_pane() + def add_periodic_task(self, period: int, func: Callable): + # TODO można poprawić żeby się odpalało tylko przy dodaniu obiektu, przemyśleć + def wrapper(): + func() + self.root.after(period, wrapper) + wrapper() def run(self): self.root.mainloop() - def add_template(self, content: str | Iterable[str]): - if isinstance(content, str): - self.szablony.append(content) + def __exit_clicked(self) -> NoReturn | None: + # Wait for saving objects to DB + print("Exiting") + exit() + + def add_template(self, content: Template | Iterable[Template]): + if isinstance(content, Template): + if content not in self.szablony: + self.szablony.append(content) else: - [self.szablony.append(i) for i in content] + [self.szablony.append(i) for i in content if i not in self.szablony] self.__update_listbox(self.template_listbox, self.szablony) + + def add_group(self, name: str, emails: Iterable[Contact]): + self.grupy[name] = emails + self.__update_listbox(self.grupy_listbox, self.grupy) + + def __add_group_clicked(self): + self.show_group_window() - - # def usun_tekst(entry_text: tk.Text): - # entry_text.delete(1.0, tk.END) + def show_group_window(self, group_name: str | None = None, contacts: Iterable[Contact] | None = None): + group_editor = GroupEditor(self, group_name, contacts) + group_editor.prepareInterface() def __send_clicked() -> None: print("send mail") pass - def __add_group_clicked(self): - nazwa_grupy = simpledialog.askstring("Nazwa grupy", "Wpisz nazwę grupy:") - if nazwa_grupy: - adresy_email = simpledialog.askstring("Adresy email", "Wpisz adresy email oddzielone przecinkami:") - if adresy_email: - self.grupy[nazwa_grupy] = adresy_email.split(',') - self.update_grupy() - - def zapisz_grupy(self): - with open("grupy.txt", "w") as f: - for grupa, adresy in self.grupy.items(): - f.write(grupa + ':' + ','.join(adresy) + '\n') - - def update_grupy(self, btn_zapisz): - btn_zapisz.config(state=tk.NORMAL) - self.grupy_listbox.delete(0, tk.END) - for grupa in self.grupy.keys(): - self.grupy_listbox.insert(tk.END, grupa) - - def edytuj_grupe(self): - if self.grupy_listbox.curselection(): - indeks = self.grupy_listbox.curselection()[0] - nazwa_grupy = self.grupy_listbox.get(indeks) - adresy = ','.join(self.grupy[nazwa_grupy]) - nowe_adresy = simpledialog.askstring("Edytuj grupę", "Wpisz nowe adresy email oddzielone przecinkami:", initialvalue=adresy) - if nowe_adresy: - self.grupy[nazwa_grupy] = nowe_adresy.split(',') - self.update_grupy() - + def __importuj_clicked(self): + pass + + def __eksportuj_clicked(self): + pass + + def __template_selection_changed(self, _event): + selected = self.template_listbox.curselection() + if len(selected) > 0: + self.showTemplate(self.szablony[selected[0]]) + + def __group_doubleclicked(self, _event): + selected = self.grupy_listbox.curselection() + if len(selected) > 0: + elem = self.grupy_listbox.get(selected[0]) + self.show_group_window(elem, self.grupy[elem]) + + def __group_selection_changed(self, _event): + selected: int = self.template_listbox.curselection() + if len(selected) > 0: + self.showTemplate(self.szablony[selected[0]]) + + def __template_doubleclicked(self, _event): + selected = self.szablony[self.template_listbox.curselection()[0]] + self.show_template_window(selected) + + def showTemplate(self, selected: Template): + self.entry_text.delete('1.0', END) + self.entry_text.insert(END, selected.content) + @staticmethod - def __update_listbox(lb: tk.Listbox, content: Iterable[str]): - lb.delete(0, tk.END) - [lb.insert(tk.END, i) for i in content] + def __update_listbox(lb: Listbox, content: Iterable[str] | dict[IModel]): + if isinstance(content, list): + lb.delete(0, END) + [lb.insert(END, i) for i in content] + elif isinstance(content, dict): + lb.delete(0, END) + [lb.insert(END, k) for k in content.keys()] + else: + raise AttributeError(f"Wrong type of 'content', expected dict or Iterable, got {type(content)}") def __add_template_clicked(self): - # tresc_szablonu = simpledialog.askstring("Nowy szablon", "Wpisz treść szablonu:") - # if tresc_szablonu: - # self.add_template(tresc_szablonu) self.show_template_window() + + def __create_menu(self): + menubar = Menu(self.root) + + file_menu = Menu(menubar, tearoff=0) + file_menu.add_command(label="Import", command=self.__importuj_clicked) + file_menu.add_command(label="Export", command=self.__eksportuj_clicked) + menubar.add_cascade(label="File", menu=file_menu) + + edit_menu = Menu(menubar, tearoff=0) + add_menu = Menu(edit_menu, tearoff=0) + add_menu.add_command(label="Template", command=self.__add_template_clicked) + add_menu.add_command(label="Group", command=self.__add_group_clicked) + edit_menu.add_cascade(label="Add...", menu=add_menu) + menubar.add_cascade(label="Edit", menu=edit_menu) + self.root.config(menu=menubar) + def __create_navigation(self): - navigation_frame = tk.Frame(self.root, bg="lightblue") - btn_importuj = tk.Button(navigation_frame, text="Importuj", bg="lightblue", fg="black") - btn_eksportuj = tk.Button(navigation_frame, text="Eksportuj", bg="lightblue", fg="black") - btn_zaladuj = tk.Button(navigation_frame, text="Załaduj", bg="lightblue", fg="black") - btn_wyslij = tk.Button(navigation_frame, text="Wyślij", bg="lightblue", fg="black", + navigation_frame = Frame(self.root, bg="lightblue") + + + btn_plik = Menubutton( + navigation_frame, text="Plik", bg="lightblue", fg="black", relief=RAISED, bd=2) + plik_menu = Menu(btn_plik, tearoff=0) + plik_menu.add_command(label="Importuj", command=self.__importuj_clicked) + plik_menu.add_command(label="Eksportuj", command=self.__eksportuj_clicked) + btn_plik.configure(menu=plik_menu) + + + btn_plik = Menubutton( + navigation_frame, text="Plik", bg="lightblue", fg="black", relief=RAISED, bd=2) + plik_menu = Menu(btn_plik, tearoff=0) + plik_menu.add_command(label="Importuj", command=self.__importuj_clicked) + plik_menu.add_command(label="Eksportuj", command=self.__eksportuj_clicked) + btn_plik.configure(menu=plik_menu) + + btn_wyslij = Button(navigation_frame, text="Wyślij", bg="lightblue", fg="black", command=lambda: self.__send_clicked() ) - btn_usun = tk.Button(navigation_frame, text="Usuń", bg="lightblue", fg="black", - #command=lambda: self.usun_tekst(entry_text) + btn_usun = Button(navigation_frame, text="Usuń", bg="lightblue", fg="black", + # command=lambda: self.usun_tekst(entry_text) ) - btn_zapisz = tk.Button(navigation_frame, text="Zapisz", bg="lightblue", fg="black", - command=lambda: self.zapisz_grupy(btn_zapisz), state=tk.DISABLED) - btn_grupy = tk.Button(navigation_frame, text="Grupy", bg="lightblue", fg="black", - command=lambda: self.__add_group_clicked()) - btn_szablony = tk.Button(navigation_frame, text="Templates", bg="lightblue", fg="black", + btn_grupy = Button(navigation_frame, text="Grupy", bg="lightblue", fg="black", + command=lambda: self.__add_group_clicked()) + btn_szablony = Button(navigation_frame, text="Templates", bg="lightblue", fg="black", command=lambda: self.__add_template_clicked()) - - navigation_frame.pack(side=tk.TOP, fill=tk.X) - btn_importuj.pack(side=tk.LEFT, padx=5, pady=5) - btn_eksportuj.pack(side=tk.LEFT, padx=5, pady=5) - btn_zaladuj.pack(side=tk.LEFT, padx=5, pady=5) - btn_wyslij.pack(side=tk.LEFT, padx=5, pady=5) - btn_usun.pack(side=tk.LEFT, padx=5, pady=5) - btn_zapisz.pack(side=tk.LEFT, padx=5, pady=5) - btn_grupy.pack(side=tk.LEFT, padx=5, pady=5) - btn_szablony.pack(side=tk.LEFT, padx=5, pady=5) - + btn_settings = Button(navigation_frame, text="Ustawienia", bg="lightblue", fg="black", + command=self.logout) + + navigation_frame.pack(side=TOP, fill=X) + btn_wyslij.pack(side=LEFT, padx=5, pady=5) + btn_usun.pack(side=LEFT, padx=5, pady=5) + btn_grupy.pack(side=LEFT, padx=5, pady=5) + btn_szablony.pack(side=LEFT, padx=5, pady=5) + btn_settings.pack(side=RIGHT, padx=5, pady=5) + def __create_notification_pane(self): - notifications_frame = tk.Frame(self.root, bg="lightblue", width=200, height=100, relief=tk.RIDGE, borderwidth=2) - notifications_label = tk.Label(notifications_frame, text="Miejsce na powiadomienia", bg="lightblue") - - notifications_frame.pack(side=tk.LEFT, padx=10, pady=10, fill=tk.BOTH, expand=True, ipadx=5, ipady=5) - notifications_label.pack(fill=tk.BOTH, expand=True) + notifications_frame = Frame( + self.root, bg="lightblue", width=200, height=100, relief=RIDGE, borderwidth=2) + notifications_label = Label( + notifications_frame, text="Miejsce na powiadomienia", bg="lightblue") + notifications_frame.pack( + side=LEFT, padx=10, pady=10, fill=BOTH, expand=True, ipadx=5, ipady=5) + notifications_label.pack(fill=BOTH, expand=True) def __create_mailing_group_pane(self): - groups_frame = tk.Frame(self.root, bg="lightblue", width=200, height=100, relief=tk.RIDGE, borderwidth=2) - grupy_label = tk.Label(groups_frame, text="Grupy mailowe", bg="lightblue") - grupy_listbox = tk.Listbox(groups_frame, bg="lightblue", fg="black") - - groups_frame.pack(side=tk.LEFT, padx=10, pady=10, fill=tk.BOTH, expand=True, ipadx=5, ipady=5) - grupy_label.pack() - grupy_listbox.pack(fill=tk.BOTH, expand=True) - grupy_listbox.bind('', lambda event: self.edytuj_grupe()) + groups_frame = Frame( + self.root, bg="lightblue", width=200, height=100, relief=RIDGE, borderwidth=2) + grupy_label = Label( + groups_frame, text="Grupy mailowe", bg="lightblue") + self.grupy_listbox = Listbox(groups_frame, bg="lightblue", fg="black") + self.grupy_listbox.bind('<>', self.__group_selection_changed) + self.grupy_listbox.bind('', self.__group_doubleclicked) + + groups_frame.pack(side=LEFT, padx=10, pady=10, + fill=BOTH, expand=True, ipadx=5, ipady=5) + grupy_label.pack() + self.grupy_listbox.pack(fill=BOTH, expand=True) def __create_template_pane(self): - templates_frame = tk.Frame(self.root, bg="lightblue", width=200, height=100, relief=tk.RIDGE, borderwidth=2) - szablony_label = tk.Label(templates_frame, text="Szablony wiadomości", bg="lightblue") - self.template_listbox = tk.Listbox(templates_frame, bg="lightblue", fg="black") - - templates_frame.pack(side=tk.LEFT, padx=10, pady=10, fill=tk.BOTH, expand=True, ipadx=5, ipady=5) + templates_frame = Frame( + self.root, bg="lightblue", width=200, height=100, relief=RIDGE, borderwidth=2) + szablony_label = Label( + templates_frame, text="Szablony wiadomości", bg="lightblue") + self.template_listbox = Listbox( + templates_frame, bg="lightblue", fg="black") + self.template_listbox.bind('<>', self.__template_selection_changed) + self.template_listbox.bind('', self.__template_doubleclicked) + + templates_frame.pack(side=LEFT, padx=10, pady=10, + fill=BOTH, expand=True, ipadx=5, ipady=5) szablony_label.pack() - self.template_listbox.pack(fill=tk.BOTH, expand=True) + self.template_listbox.pack(fill=BOTH, expand=True) def __create_mail_input_pane(self): - entry_frame = tk.Frame(self.root, bg="lightblue", relief=tk.RIDGE, borderwidth=2) - entry_scrollbar = tk.Scrollbar(entry_frame) - self.entry_text = tk.Text(entry_frame, bg="lightblue", fg="black", wrap=tk.WORD, yscrollcommand=entry_scrollbar.set) - entry_scrollbar.config(command=self.entry_text.yview) - entry_adres_label = tk.Label(entry_frame, text="Wyślij do:", bg="lightblue", anchor="s") - entry_adres = tk.Entry(entry_frame, bg="white", fg="black") - - entry_frame.pack(side=tk.TOP, padx=10, pady=10, fill=tk.BOTH, expand=True, ipadx=5, ipady=5) - entry_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) - self.entry_text.pack(fill=tk.BOTH, expand=True) - entry_adres_label.pack(side=tk.TOP, padx=5, pady=5) - entry_adres.pack(side=tk.TOP, padx=5, pady=5, fill=tk.X) - - def show_template_window(self): - template_window = tk.Toplevel(self.root) - template_window.title("Stwórz szablon") - - name_label = tk.Label(template_window, text="Nazwa szablonu:", bg="lightblue") + entry_frame = Frame(self.root, bg="lightblue", + relief=RIDGE, borderwidth=2) + entry_scrollbar = Scrollbar(entry_frame) + self.entry_html_label = HTMLLabel( + entry_frame, html="", bg="lightblue") + entry_adres_label = Label( + entry_frame, text="Wyślij do:", bg="lightblue", anchor="s") + entry_adres = Entry(entry_frame, bg="white", fg="black") + + entry_frame.pack(side=TOP, padx=10, pady=10, + fill=BOTH, expand=True, ipadx=5, ipady=5) + entry_scrollbar.pack(side=RIGHT, fill=Y) + self.entry_html_label.pack(fill=BOTH, expand=True) + entry_adres_label.pack(side=TOP, padx=5, pady=5) + entry_adres.pack(side=TOP, padx=5, pady=5, fill=X) + + def showTemplate(self, selected: Template): + self.entry_html_label.set_html(selected.content) + + def show_template_window(self, obj: Template | None = None): + self.template_window = TemplateEditor(self, self.root, obj) + self.template_window.prepareInterface() + + def logout(self): + # TODO to jest do zmiany, okno powinno zostać przemianowane na ustawienia, gdzie + # logujemy się do providerów poczty, sam program nie ma blokady + # względem użytkownika + + self.root.destroy() # Zamknij główne okno aplikacji + root = Tk() # Otwórz ponownie okno logowania + login_window = LoginWindow(root) + login_window.prepareInterface() + root.mainloop() + +class TemplateEditor(Toplevel): + def __init__(self, parent: AppUI, master: Misc, obj: Template | None = None): + super().__init__(master) + self.parent = parent + self.current_combo = None + self.currentTemplate = obj + + def prepareInterface(self): + self.title("Stwórz szablon") + + name_label = Label(self, text="Nazwa szablonu:", bg="lightblue") name_label.grid(row=0, column=0, padx=5, pady=5) - - name_entry = tk.Entry(template_window, bg="white", fg="black") + + name_entry = Entry(self, bg="white", fg="black") name_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew") + if self.currentTemplate: + name_entry.insert(INSERT, self.currentTemplate.name if self.currentTemplate.name is not None else "") - template_text = tk.Text(template_window, bg="lightblue", fg="black", wrap=tk.WORD) - template_text.grid(row=1, column=0, columnspan=2, padx=5, pady=5, sticky="nsew") - - btn_save = tk.Button(template_window, text="Zapisz", bg="lightblue", fg="black", command=lambda: self.save_template(name_entry.get(), template_text.get(1.0, tk.END))) + template_text = Text(self, bg="lightblue", fg="black", wrap=WORD) + template_text.grid(row=1, column=0, columnspan=2, + padx=5, pady=5, sticky="nsew") + if self.currentTemplate: + template_text.insert(INSERT, self.currentTemplate.content if self.currentTemplate.content is not None else "") + + btn_save = Button(self, text="Zapisz", bg="lightblue", fg="black", command=lambda: self.__save_template_clicked( + name_entry.get(), template_text.get(1.0, END))) btn_save.grid(row=2, column=0, padx=5, pady=5, sticky="e") - btn_insert_placeholder = tk.Button(template_window, text="Wstaw luke", bg="lightblue", fg="black", command=lambda: self.insert_placeholder(template_text)) - btn_insert_placeholder.grid(row=2, column=1, padx=5, pady=5, sticky="w") + btn_insert_placeholder = Button(self, text="Wstaw luke", bg="lightblue", fg="black", + command=lambda: self.__template_window_insert_placeholder(template_text)) + btn_insert_placeholder.grid( + row=2, column=1, padx=5, pady=5, sticky="w") - def save_template(self, template_name, template_content): - # Tutaj możesz zapisać nazwę i zawartość szablonu, na przykład do pliku lub bazy danych - print("Nazwa szablonu:", template_name) - print("Zawartość szablonu:", template_content) + def __save_template_clicked(self, template_name: str, template_content: str) -> None: + if template_name != "" and template_content != "": + self.currentTemplate = Template(_name=template_name, _content=template_content) + self.parent.add_template(self.currentTemplate) + self.destroy() - def insert_placeholder(self, template_text): + def __template_window_insert_placeholder(self, template_text: str, placeholders: list[str] = []) -> None: placeholder_text = "_____" def on_placeholder_selection(event): selected_placeholder = self.current_combo.get() if selected_placeholder: - selected_text = template_text.tag_ranges(tk.SEL) + selected_text = template_text.tag_ranges(SEL) if selected_text: template_text.delete(selected_text[0], selected_text[1]) - template_text.insert(tk.INSERT, selected_placeholder) + template_text.insert(INSERT, selected_placeholder) def hide_combobox(): if self.current_combo: @@ -189,18 +336,20 @@ def hide_combobox(): def show_placeholder_menu(event): hide_combobox() - self.current_combo = ttk.Combobox(template_text, values=placeholders) - self.current_combo.bind("<>", on_placeholder_selection) + self.current_combo = Combobox( + template_text, values=placeholders) + self.current_combo.bind( + "<>", on_placeholder_selection) self.current_combo.place(x=event.x_root, y=event.y_root) self.current_combo.focus_set() # Dodanie przycisku "x" do zamknięcia comboboxa - close_button = tk.Button(self.current_combo, text="X", command=hide_combobox, bg="white") + close_button = Button( + self.current_combo, text="X", command=hide_combobox, bg="white") close_button.place(relx=0, rely=0, anchor="nw") - template_text.insert(tk.INSERT, placeholder_text) + template_text.insert(INSERT, placeholder_text) template_text.tag_configure("placeholder", background="lightgreen") - placeholders = ["pan", "pani", "adam", "zbigniew"] if not hasattr(self, 'current_combo'): self.current_combo = None @@ -208,15 +357,59 @@ def show_placeholder_menu(event): start_index = "1.0" while True: - start_index = template_text.search(placeholder_text, start_index, stopindex=tk.END) + start_index = template_text.search( + placeholder_text, start_index, stopindex=END) if not start_index: break - end_index = template_text.index(f"{start_index}+{len(placeholder_text)}c") + end_index = template_text.index( + f"{start_index}+{len(placeholder_text)}c") template_text.tag_add("placeholder", start_index, end_index) start_index = end_index - def dodaj_szablon(self): - tresc_szablonu = simpledialog.askstring("Nowy szablon", "Wpisz treść szablonu:") - if tresc_szablonu: - self.szablony.append(tresc_szablonu) +class GroupEditor(Toplevel): + def __init__(self, parent: AppUI, groupName: str | None = None, edited: Iterable[Contact] | None = None): + super().__init__(parent.root) + self.parent = parent + self.groupName = groupName + self.currentGroup = edited + + def prepareInterface(self): + self.title("Dodaj grupę") + + name_label = Label(self, text="Nazwa grupy:", bg="lightblue") + name_label.grid(row=0, column=0, padx=5, pady=5) + + self.name_entry = Entry(self, bg="white", fg="black") + self.name_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew") + + if self.groupName: + self.name_entry.insert(INSERT, self.groupName) + + email_label = Label(self, text="Adresy email:", bg="lightblue") + email_label.grid(row=1, column=0, padx=5, pady=5) + + self.email_text = Text(self, bg="lightblue", fg="black", wrap=WORD) + self.email_text.grid(row=1, column=1, padx=5, pady=5, sticky="nsew") + + if self.currentGroup: + [self.add_contact(c) for c in self.currentGroup] + + btn_save = Button(self, text="Zapisz", bg="lightblue", fg="black", command=self.__save_group_clicked) + btn_save.grid(row=2, column=0, columnspan=2, padx=5, pady=5, sticky="ew") + + def add_contact(self, c: Contact): + self.email_text.insert(INSERT, str(c)) + # self.email_text.update + def __save_group_clicked(self) -> None: + result = [] + group_name, email_addresses = self.name_entry.get(), self.email_text.get(1.0, END) + for mail in email_addresses.replace("\n", "").split(","): + # TODO jeżeli kontakt już istnieje, to nie tworzyć nowego, tylko zwrócić istniejący + try: + result.append(Contact("", "", mail)) + except AttributeError as e: + # print(e) + raise e + self.parent.add_group(group_name, result) + self.destroy() diff --git a/main.py b/main.py index 862a33d..4dbaad2 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,7 @@ from MessagingService.readers import * from UserInfo.LoginService import * # from sys import platform -import models as m +from models import IModel, Template, Attachment, Contact, Message from Triggers.triggers import ITrigger from interface import AppUI from DataSources.dataSources import DatabaseHandler, IDataSource @@ -10,18 +10,24 @@ dbname = "localSQLite.sqlite3" dbURL = f"sqlite:///{dbname}" -tables = [m.Template, m.Attachment, m.Contact, ITrigger, m.Message, MessageAttachment, SendAttempt] +tables = [Template, Attachment, Contact, ITrigger, Message, MessageAttachment, SendAttempt] +db: IDataSource = None def populateInterface(app: AppUI) -> None: modelType_func_mapper = { - m.Template: app.add_template, + Template: app.add_template, } for (modelType, ui_func) in modelType_func_mapper.items(): ui_func(modelType.all_instances) +def pushQueuedInstances(): + if len(IModel.saveQueued) > 0: + for o in IModel.saveQueued: + db.Save(o) + IModel.saveQueued.remove(o) if __name__ == "__main__": db = DatabaseHandler(dbURL, tables) @@ -30,12 +36,12 @@ def populateInterface(app: AppUI) -> None: if db.checkIntegrity(): print("Database intact, proceeding") - db.LoadSavedState() - populateInterface(ui) + db.LoadSavedState() + populateInterface(ui) # TODO win32 powidomienia # if 'win32' in platform: # enableWin32Integration() - + + ui.add_periodic_task(5000, pushQueuedInstances) ui.run() - diff --git a/models.py b/models.py index ebcddad..1d5517a 100644 --- a/models.py +++ b/models.py @@ -1,9 +1,10 @@ +from __future__ import annotations from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.application import MIMEApplication from sqlalchemy import Column, Integer, String, LargeBinary, TIMESTAMP, func -from sqlalchemy.orm import declarative_base -from sqlalchemy.orm import relationship +from sqlalchemy.orm import declarative_base, relationship +from sqlalchemy.ext.hybrid import hybrid_property from abc import ABCMeta, abstractmethod from pathlib import Path import re @@ -11,21 +12,66 @@ __all__ = ["Template", "Attachment", "Contact", "User", "Message"] + class IModel(declarative_base()): __abstract__ = True + run_loading = True + saveQueued: list[IModel] = [] + + @staticmethod + def queueSave(child): + if not IModel.run_loading: + IModel.saveQueued.append(child) + class Template(IModel): - all_instances = [] + all_instances: list[Template] = [] __tablename__ = "Templates" - - id = Column(Integer, primary_key=True) - name = Column(String(100), nullable=False) - content = Column(LargeBinary, nullable=False, default=b'') - def __init__(self, name: str, content: str) -> None: - self.name = name - self.content = content + _id = Column("id", Integer, primary_key=True) + _name = Column("name", String(100), nullable=True) + _content = Column("content", String, nullable=True) + + def __init__(self, **kwargs) -> None: + self.id = kwargs.pop('_id', None) + self.name = kwargs.pop('_name', None) + self.content = kwargs.pop('_content', None) Template.all_instances.append(self) + IModel.queueSave(child=self) + + + def __str__(self) -> str: + return self.name if self.name != None else "" + + def __repr__(self): + return f"Template(_name={self.name}, _content={self.content}, _id={self.id})" + + @hybrid_property + def id(self): + return self._id + + @hybrid_property + def name(self): + return self._name + + @hybrid_property + def content(self): + return self._content + + @id.setter + def id(self, newValue: int): + if newValue: + self._id = newValue + else: + self._id = max((i.id for i in Template.all_instances), default=0) + 1 + + @name.setter + def name(self, value: str | None): + self._name = value + + @content.setter + def content(self, value: str | None): + self._content = value class Attachment(IModel): @@ -37,11 +83,12 @@ class Attachment(IModel): file_path = Column(String(255), nullable=True) file = Column(LargeBinary, nullable=True) - - def __init__(self, path, type) -> None: + def __init__(self, path, type, attachment_id: int | None = None) -> None: + self.attachment_id = attachment_id self.path = path self.type = type Attachment.all_instances.append(self) + IModel.queueSave(child=self) # def prepareAttachment(self): # att = MIMEApplication(open(self.path, "rb").read(), _subtype=self.type) @@ -73,11 +120,11 @@ def __init__(self, first_name: str, last_name: str, email: str) -> None: raise AttributeError(f"{email} is not valid email") self.first_name = first_name self.last_name = last_name - Contact.all_instances.append(self) + IModel.queueSave(child=self) def __str__(self) -> str: - return f"Contact {self.first_name} {self.last_name}, {self.email}" + return f"{self.first_name} {self.last_name}, <{self.email}>" def __eq__(self, other) -> bool: if not isinstance(other, Contact): @@ -90,13 +137,16 @@ def isEmail(candidate: str) -> bool: return True return False + class User(): all_instances = [] - def __init__(self, first_name: str, last_name: str, email: str, password: str) -> None: + def __init__(self, first_name: str, last_name: str, + email: str, password: str) -> None: self.contact = Contact(first_name, last_name, email) self.password = password User.all_instances.append(self) + IModel.queueSave(child=self) class Message(IModel, MIMEMultipart): @@ -108,13 +158,15 @@ class Message(IModel, MIMEMultipart): email = Column(String(100), nullable=False) template_id = Column(Integer, nullable=False) sent_at = Column(TIMESTAMP, default=func.now()) - + # TODO # trigger = relationship("Trigger") # contact = relationship("Contact") # template = relationship("Template") - def __init__(self, recipient: Contact, att: list[Attachment] = None) -> None: + def __init__(self, recipient: Contact, + att: list[Attachment] = None) -> None: self.recipient = recipient self.att = att Message.all_instances.append(self) + IModel.queueSave(child=self)