From 6bf62e6faebb6fa7eda2778158b766728ba5132f Mon Sep 17 00:00:00 2001 From: dittko Date: Sun, 26 May 2024 11:55:49 +0200 Subject: [PATCH 1/2] usuniete miejsce na powiadomienia zmieniony navbar na menu dodanie skalowania okienka Templates --- interface.py | 142 ++++++++++++++------------------------------------- 1 file changed, 37 insertions(+), 105 deletions(-) diff --git a/interface.py b/interface.py index 40efef1..3491378 100644 --- a/interface.py +++ b/interface.py @@ -95,8 +95,6 @@ def prepareInterface(self) -> None: 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() @@ -213,64 +211,12 @@ def __create_menu(self): edit_menu.add_cascade(label="Add...", menu=add_menu) menubar.add_cascade(label="Edit", menu=edit_menu) menubar.add_command(label="Open Settings", command=self.logout) + menubar.add_command(label="Send", command=lambda: self.__send_clicked()) + self.root.config(menu=menubar) - - def __create_navigation(self): - 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 = Button(navigation_frame, text="Usuń", bg="lightblue", fg="black", - # command=lambda: self.usun_tekst(entry_text) - ) - 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()) - 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 = 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 = Frame( @@ -337,49 +283,46 @@ def logout(self): root.mainloop() class TemplateEditor(Toplevel): - def __init__(self, parent: AppUI, master: Misc, - obj: Template | None = None): + def __init__(self, parent: AppUI, master: Misc, obj: Template | None = None): super().__init__(master) self.parent = parent self.current_combo: Combobox = None self.currentTemplate = obj + self.prepareInterface() + def prepareInterface(self): self.title("Stwórz szablon") + self.grid_rowconfigure(1, weight=1) + self.grid_columnconfigure(1, weight=1) + self.grid_columnconfigure(3, weight=1) + name_label = Label(self, text="Nazwa szablonu:", bg="lightblue") name_entry = Entry(self, bg="white", fg="black") - + self.template_text = HTMLText(self, bg="lightblue", fg="black", wrap=WORD) self.template_text.bind("", self.__on_html_key_clicked) self.template_text.bind("<>", self.__on_text_changed) - + self.template_preview = HTMLLabel(self, bg="lightblue", fg="black", wrap=WORD) - btn_save = Button(self, text="Zapisz", bg="lightblue", fg="black", command=lambda: self.__save_template_clicked( - name_entry.get(), self.template_text.get(1.0, END))) - btn_insert_placeholder = Button(self, text="Wstaw luke", bg="lightblue", fg="black", + btn_save = Button(self, text="Zapisz", bg="lightblue", fg="black", + command=lambda: self.__save_template_clicked(name_entry.get(), self.template_text.get(1.0, END))) + btn_insert_placeholder = Button(self, text="Wstaw lukę", bg="lightblue", fg="black", command=self.__template_window_insert_placeholder) - - + name_label.grid(row=0, column=0, padx=5, pady=5) name_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew") - self.template_text.grid(row=1, column=0, columnspan=2, - padx=5, pady=5, sticky="nsew") + self.template_text.grid(row=1, column=0, columnspan=2, padx=5, pady=5, sticky="nsew") self.template_preview.grid(row=1, column=3, columnspan=5, padx=5, pady=5, sticky="nsew") - btn_save.grid(row=2, column=0, padx=5, pady=5, sticky="e") - btn_insert_placeholder.grid( - row=2, column=1, padx=5, pady=5, sticky="w") - - if self.currentTemplate: # if anything is present in template - name_entry.insert( - INSERT, - self.currentTemplate.name if self.currentTemplate.name is not None else "") - - self.template_text.insert( - INSERT, - self.currentTemplate.content if self.currentTemplate.content is not None else "") - self.template_text.event_generate("<>") # Initial render - + btn_save.grid(row=2, column=2, padx=(50, 5), pady=5, sticky="e") + btn_insert_placeholder.grid(row=2, column=3, padx=(5, 50), pady=5, sticky="w") + + if self.currentTemplate: + name_entry.insert(INSERT, self.currentTemplate.name if self.currentTemplate.name is not None else "") + self.template_text.insert(INSERT, self.currentTemplate.content if self.currentTemplate.content is not None else "") + self.template_text.event_generate("<>") + def __on_html_key_clicked(self, event: Event): if event.keycode not in NonAlteringKeyCodes: self.template_text.event_generate("<>") @@ -388,18 +331,15 @@ def __on_text_changed(self, event): html_text = self.template_text.get("1.0", END) mb_tag = "MailBuddyGap>" replacement_text = '' - - # Only preview change, original text remains intact - contains mb_tag + html_text = html_text.replace("<" + mb_tag, replacement_text) html_text = html_text.replace("") - + self.template_preview.set_html(html_text) - def __save_template_clicked( - self, template_name: str, template_content: str) -> None: + 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.currentTemplate = Template(_name=template_name, _content=template_content) self.parent.add_template(self.currentTemplate) self.destroy() @@ -407,13 +347,11 @@ def hide_combobox(self): if self.current_combo: self.current_combo.destroy() - def __template_window_insert_placeholder( - self, placeholders: list[GapFillSource] = GapFillSource.all_instances) -> None: + def __template_window_insert_placeholder(self, placeholders: list[GapFillSource] = GapFillSource.all_instances) -> None: placeholder_text = " " combo_values = [key for placeholder in placeholders for key in placeholder.possible_values] - + def on_placeholder_selection(event): - # TODO: Debug - usuwa tylko zaznaczony tekst, może niechcąco usunąć inny fragment selected_placeholder = self.current_combo.get() if selected_placeholder: selected_text = self.template_text.tag_ranges(SEL) @@ -424,16 +362,12 @@ def on_placeholder_selection(event): def show_placeholder_menu(event): self.hide_combobox() - self.current_combo = Combobox( - self.template_text, values=combo_values) - #TODO: Debug populating combobox - self.current_combo.bind( - "<>", on_placeholder_selection) + self.current_combo = Combobox(self.template_text, values=combo_values) + self.current_combo.bind("<>", on_placeholder_selection) self.current_combo.place(x=event.x_root, y=event.y_root) self.current_combo.focus_set() - - close_button = Button( - self.current_combo, text="X", command=self.hide_combobox, bg="white") + + close_button = Button(self.current_combo, text="X", command=self.hide_combobox, bg="white") close_button.place(relx=0.90, rely=0, anchor="ne") self.template_text.insert(INSERT, placeholder_text) @@ -443,12 +377,10 @@ def show_placeholder_menu(event): start_index = "1.0" while True: - start_index = self.template_text.search( - placeholder_text, start_index, stopindex=END) + start_index = self.template_text.search(placeholder_text, start_index, stopindex=END) if not start_index: break - end_index = self.template_text.index( - f"{start_index}+{len(placeholder_text)}c") + end_index = self.template_text.index(f"{start_index}+{len(placeholder_text)}c") self.template_text.tag_add("placeholder", start_index, end_index) start_index = end_index From 38d60bc43687a07f3defce6db781b97a1b19375b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Ha=C5=82aczkiewicz?= <54312917+ikarmus2001@users.noreply.github.com> Date: Mon, 27 May 2024 22:22:43 +0200 Subject: [PATCH 2/2] Dodanie VS2022, przygotowanie do merge --- .gitignore | 6 +- Interface/AddContactWindow.py | 51 +++ Interface/AppUI.py | 224 +++++++++++++ Interface/ContactList.py | 114 +++++++ Interface/GroupEditor.py | 67 ++++ Interface/Settings.py | 74 +++++ Interface/TemplateEditor.py | 138 ++++++++ MailBuddy.pyproj | 70 ++++ MailBuddy.sln | 23 ++ interface.py | 609 ---------------------------------- main.py | 31 +- models.py | 14 +- 12 files changed, 800 insertions(+), 621 deletions(-) create mode 100644 Interface/AddContactWindow.py create mode 100644 Interface/AppUI.py create mode 100644 Interface/ContactList.py create mode 100644 Interface/GroupEditor.py create mode 100644 Interface/Settings.py create mode 100644 Interface/TemplateEditor.py create mode 100644 MailBuddy.pyproj create mode 100644 MailBuddy.sln delete mode 100644 interface.py diff --git a/.gitignore b/.gitignore index f167d5c..8fbca22 100644 --- a/.gitignore +++ b/.gitignore @@ -163,4 +163,8 @@ cython_debug/ .vscode/ *.sqlite3 # Used for testing purposes -personalSecrets.py \ No newline at end of file +personalSecrets.py +Tests/ethereal_mock.py +*.exe +/MailbuddyEnv +/.vs \ No newline at end of file diff --git a/Interface/AddContactWindow.py b/Interface/AddContactWindow.py new file mode 100644 index 0000000..c36d854 --- /dev/null +++ b/Interface/AddContactWindow.py @@ -0,0 +1,51 @@ +from collections.abc import Callable, Iterable +from enum import Enum +from sqlalchemy.exc import IntegrityError +from types import TracebackType +from traceback import print_tb +from typing import Literal, Any, NoReturn +from tkinter import Event, Menu, simpledialog, ttk, Listbox, Tk, Text, Button, Frame, Label, Entry, Scrollbar, Toplevel, Misc, messagebox, Menubutton, Canvas,Checkbutton,BooleanVar, VERTICAL, RAISED +from tkinter.ttk import Combobox +from tkinter.constants import NORMAL, DISABLED, BOTH, RIDGE, END, LEFT, RIGHT, TOP, X, Y, INSERT, SEL, WORD +from group_controller import GroupController +from models import Contact, IModel, Template, Group +from tkhtmlview import HTMLLabel, HTMLText +from DataSources.dataSources import GapFillSource + + +class AddContactWindow(Toplevel): + def __init__(self, parent: Toplevel | ContactList) -> None: + super().__init__(parent) + self.parent = parent + + def prepareInterface(self): + self.title("Dodaj Kontakt") + + email_label = Label(self, text="Adres email:", bg="lightblue") + self.email_entry = Entry(self, bg="white", fg="black") + name_label = Label(self, text="Imię:", bg="lightblue") + self.name_entry = Entry(self, bg="white", fg="black") + surname_label = Label(self, text="Nazwisko:", bg="lightblue") + self.surname_entry = Entry(self, bg="white", fg="black") + btn_add_contact = Button(self, text="Dodaj kontakt", bg="lightblue", fg="black", command=self.add_manual_contact) + + email_label.grid(row=0, column=0, padx=5, pady=5) + self.email_entry.grid(row=0, column=1, padx=5, pady=5) + name_label.grid(row=1, column=0, padx=5, pady=5) + self.name_entry.grid(row=1, column=1, padx=5, pady=5) + surname_label.grid(row=2, column=0, padx=5, pady=5) + self.surname_entry.grid(row=2, column=1, padx=5, pady=5) + btn_add_contact.grid(row=3, column=0, columnspan=2, padx=5, pady=5, sticky="ew") + + def add_manual_contact(self): + email = self.email_entry.get() + name = self.name_entry.get() + surname = self.surname_entry.get() + if email: + newContact = Contact(_email=email, _first_name=name, _last_name=surname) + self.parent.update() + self.destroy() + # TODO: Jakiś sygnał do parenta żeby się zaktualizował? + else: + messagebox.showerror("Błąd", "Podaj adres e-mail") + diff --git a/Interface/AppUI.py b/Interface/AppUI.py new file mode 100644 index 0000000..5812df0 --- /dev/null +++ b/Interface/AppUI.py @@ -0,0 +1,224 @@ +from collections.abc import Callable, Iterable +from types import TracebackType +from traceback import print_tb +from typing import NoReturn +from tkinter import Menu, simpledialog, Listbox, Tk, Frame, Label, Entry, Scrollbar +from tkinter.constants import BOTH, RIDGE, END, LEFT, RIGHT, TOP, X, Y, INSERT +from models import IModel, Template, Group +from tkhtmlview import HTMLLabel +from .GroupEditor import GroupEditor +from .Settings import Settings +from .TemplateEditor import TemplateEditor + + +def errorHandler(xd, exctype: type, excvalue: Exception, tb: TracebackType): + msg = f"{exctype}: {excvalue}, {print_tb(tb)}" + print(msg) + simpledialog.messagebox.showerror("Error", msg) + +Tk.report_callback_exception = errorHandler + +class AppUI(): + def __init__(self) -> None: + self.root = Tk() + self.grupy: list[Group] = [] + 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_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 __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 if i not in self.szablony] + self.__update_listbox(self.template_listbox, self.szablony) + + def add_group(self, g: Group | Iterable[Group]): + if isinstance(g, Group): + if g not in self.grupy: + self.grupy.append(g) + else: + [self.grupy.append(i) for i in g if i not in self.grupy] + self.__update_listbox(self.grupy_listbox, self.grupy) + + def __add_group_clicked(self): + self.show_group_window() + + def show_group_window(self, g: Group | None = None): + group_editor = GroupEditor(self, g) + group_editor.prepareInterface() + + def __send_clicked(event) -> None: + print("send mail") + pass + + 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 = int(self.grupy_listbox.get(selected[0]).split(':')[0]) + self.show_group_window(self.grupy[elem]) + + def __group_selection_changed(self, _event): + selected: int = self.grupy_listbox.curselection() + if len(selected) > 0: + g: Group = self.grupy[selected[0]] + mails = [", ".join(x.email) for x in g.contacts] + self.entry_adres.delete(0, END) + self.entry_adres.insert(INSERT, mails) + + def __template_doubleclicked(self, _event): + ui_selection = self.template_listbox.curselection() + if len(ui_selection) > 0: + selected = self.szablony[ui_selection[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: Listbox, content: Iterable[IModel] | dict[IModel]): + if isinstance(content, Iterable): + 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): + 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) + menubar.add_command(label="Open Settings", command=self.logout) + menubar.add_command(label="Send", command=lambda: self.__send_clicked()) + + + self.root.config(menu=menubar) + + + + def __create_mailing_group_pane(self): + 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 = 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=BOTH, expand=True) + + def __create_mail_input_pane(self): + 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") + self.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) + self.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): + + root = Tk() # Otwórz ponownie okno logowania + settings = Settings(root) + settings.prepareInterface() + root.mainloop() + + + + diff --git a/Interface/ContactList.py b/Interface/ContactList.py new file mode 100644 index 0000000..7d7d4d9 --- /dev/null +++ b/Interface/ContactList.py @@ -0,0 +1,114 @@ +from collections.abc import Callable, Iterable +from enum import Enum +from sqlalchemy.exc import IntegrityError +from types import TracebackType +from traceback import print_tb +from typing import Literal, Any, NoReturn +from tkinter import Event, Menu, simpledialog, ttk, Listbox, Tk, Text, Button, Frame, Label, Entry, Scrollbar, Toplevel, Misc, messagebox, Menubutton, Canvas,Checkbutton,BooleanVar, VERTICAL, RAISED +from tkinter.ttk import Combobox +from tkinter.constants import NORMAL, DISABLED, BOTH, RIDGE, END, LEFT, RIGHT, TOP, X, Y, INSERT, SEL, WORD +from group_controller import GroupController +from models import Contact, IModel, Template, Group +from tkhtmlview import HTMLLabel, HTMLText +from DataSources.dataSources import GapFillSource +from GroupEditor import GroupEditor +from AddContactWindow import AddContactWindow + +class ContactList(Toplevel): + def __init__(self, parent: Toplevel | GroupEditor, group: Group | None = None) -> None: + super().__init__(parent) + self.group = group + self.parent = parent + + def prepareInterface(self): + self.title("Dodaj kontakt z listy") + + group_editor_geometry = self.parent.winfo_geometry() + self.geometry(group_editor_geometry) + + contact_frame = Frame(self) + search_frame = Frame(contact_frame, bg="lightblue") + search_label = Label(search_frame, text="Wyszukaj:", bg="lightblue") + self.search_entry = Entry(search_frame, bg="white", fg="black") + search_button = Button(search_frame, text="Szukaj", bg="lightblue", fg="black", command=self.search_contact) + add_contact_button = Button(search_frame, text="Dodaj nowy kontakt", bg="lightblue", fg="black", command=self.add_manual_contact_window) + scrollbar = Scrollbar(contact_frame, orient=VERTICAL) + self.contact_canvas = Canvas(contact_frame, yscrollcommand=scrollbar.set) + self.contact_inner_frame = Frame(self.contact_canvas) + + scrollbar.config(command=self.contact_canvas.yview) + + contact_frame.pack(fill=BOTH, expand=True) + search_frame.pack(fill=X, padx=5, pady=5) + search_label.pack(side=LEFT, padx=5, pady=5) + self.search_entry.pack(side=LEFT, padx=5, pady=5, expand=True, fill=X) + search_button.pack(side=LEFT, padx=5, pady=5) + add_contact_button.pack(side=LEFT, padx=5, pady=5) + scrollbar.pack(side=RIGHT, fill=Y) + self.contact_canvas.pack(side=LEFT, fill=BOTH, expand=True) + self.contact_canvas.create_window((0, 0), window=self.contact_inner_frame, anchor='nw') + + self.contact_canvas.configure(scrollregion=self.contact_canvas.bbox("all")) + self.update() + + def clearEntries(self): + for widget in self.contact_inner_frame.winfo_children(): + widget.destroy() + + def update(self): + self.clearEntries() + self.populateWindow() + + def populateWindow(self): + shouldAddButton = self.parent != None and isinstance(self.parent, GroupEditor) + for idx, c in enumerate(Contact.all_instances): + self.create_contact_widget(c, idx, addBtn=shouldAddButton) + + if self.group: + group_contacts = GroupController.get_contacts(self.group) + group_emails = {contact.email for contact in group_contacts} + for idx, c in enumerate(Contact.all_instances): + added_to_group = c.email in group_emails + self.create_contact_widget(c, idx, added_to_group, addBtn=shouldAddButton) + + def create_contact_widget(self, c: Contact, idx: int, added_to_group: bool = False, addBtn: bool = True): + def toggle_checkbox(): + if checkbox_var.get(): + self.add_contact_to_group(c) + else: + self.remove_contact_from_group(c) + + checkbox_var = BooleanVar(value=added_to_group) + checkbox = Checkbutton(self.contact_inner_frame, variable=checkbox_var, command=toggle_checkbox) #bg="lightblue") + checkbox.grid(row=idx, column=2, padx=4, pady=4) + + Label(self.contact_inner_frame, text=f"Mail {idx+1}:").grid(row=idx, column=0, padx=5, pady=5) + Label(self.contact_inner_frame, text=f"{c.email} - {c.first_name} {c.last_name}").grid(row=idx, column=1, padx=5, pady=5) + + def add_contact_to_group(self, c: Contact): + if self.group == None: + return + + try: + GroupController.add_contact(self.group, c) + if isinstance(self.parent, GroupEditor): + self.parent.update() + except IntegrityError: + pass + + def remove_contact_from_group(self, c: Contact): + GroupController.delete_connection(self.group, c) + if isinstance(self.parent, GroupEditor): + self.parent.update() + + def search_contact(self): + search_criteria = self.search_entry.get().strip() + self.clearEntries() + + for idx, c in enumerate(Contact.all_instances): + if search_criteria in c.first_name or search_criteria in c.last_name or search_criteria in c.email: + self.create_contact_widget(c, idx) + + def add_manual_contact_window(self): + acw = AddContactWindow(self) + acw.prepareInterface() diff --git a/Interface/GroupEditor.py b/Interface/GroupEditor.py new file mode 100644 index 0000000..221ca9f --- /dev/null +++ b/Interface/GroupEditor.py @@ -0,0 +1,67 @@ +from tkinter import Text, Button, Label, Entry, Tk, Toplevel +from tkinter.constants import END, INSERT, WORD +from group_controller import GroupController +from models import Contact, Group + + +class GroupEditor(Toplevel): + def __init__(self, parent: Toplevel | Tk, edited: Group | None = None): + super().__init__(parent.root) + self.parent = parent + self.currentGroup = edited + + def prepareInterface(self): + name_label = Label(self, text="Nazwa grupy:", bg="lightblue") + self.name_entry = Entry(self, bg="white", fg="black") + email_label = Label(self, text="Adresy email:", bg="lightblue") + self.email_text = Text(self, bg="lightblue", fg="black", wrap=WORD) + btn_add_list_contact = Button(self, text="Dodaj z listy", bg="lightblue", fg="black", command=self.add_contact_from_list_window) + btn_save = Button(self, text="Zapisz", bg="lightblue", fg="black", command=self.__save_group_clicked) + + name_label.grid(row=0, column=0, padx=5, pady=5, sticky="w") + self.name_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew") + email_label.grid(row=1, column=0, padx=5, pady=5, sticky="w") + self.email_text.grid(row=1, column=1, padx=5, pady=5, sticky="nsew") + btn_add_list_contact.grid(row=2, column=0, padx=5, pady=5, sticky="ew") + btn_save.grid(row=3, column=0, columnspan=2, padx=5, pady=5, sticky="ew") + + self.grid_columnconfigure(1, weight=1) + self.grid_rowconfigure(1, weight=1) + + self.update() + + + def update(self): + if self.currentGroup: + self.title(f"Edytuj grupę {self.currentGroup.name}") + self.name_entry.delete(0, END) + self.name_entry.insert(0, self.currentGroup.name) + self.currentGroup.contacts = GroupController.get_contacts(self.currentGroup) + self.email_text.delete('1.0', END) # Clear current content + [self.add_contact(c) for c in self.currentGroup.contacts] + else: + self.title("Dodaj grupę") + + def add_contact(self, c: Contact): + self.email_text.insert(INSERT, str(c.email) + "\n") + + def add_contact_from_list_window(self): + contact_list_window = ContactList(self, self.currentGroup) + contact_list_window.prepareInterface() + # TODO: Odebrać info o dodawanych kontaktach, wywoływać add_contact + + def __save_group_clicked(self) -> None: + if not self.currentGroup: + self.currentGroup = Group(_name = self.name_entry.get()) + else: + self.currentGroup.name = self.name_entry.get() + txt = self.email_text.get(1.0, END).strip() + email_addresses = [address for address in txt.replace("\n", "").split(",") if address.strip()] + # TODO: Przy zmianie kontrolek w grupie będzie trzeba zmienić wywoływanie konstruktora - te kontakty powinny być zapisane wcześniej, bez możliwości dodawania ich od tak z palca + for mail in email_addresses: + try: + self.currentGroup._add_contact(Contact(_email=mail)) + except AttributeError as e: + raise e + self.parent.add_group(self.currentGroup) + self.destroy() diff --git a/Interface/Settings.py b/Interface/Settings.py new file mode 100644 index 0000000..127d827 --- /dev/null +++ b/Interface/Settings.py @@ -0,0 +1,74 @@ +from collections.abc import Callable, Iterable +from enum import Enum +from sqlalchemy.exc import IntegrityError +from types import TracebackType +from traceback import print_tb +from typing import Literal, Any, NoReturn +from tkinter import Event, Menu, simpledialog, ttk, Listbox, Tk, Text, Button, Frame, Label, Entry, Scrollbar, Toplevel, Misc, messagebox, Menubutton, Canvas,Checkbutton,BooleanVar, VERTICAL, RAISED +from tkinter.ttk import Combobox +from tkinter.constants import NORMAL, DISABLED, BOTH, RIDGE, END, LEFT, RIGHT, TOP, X, Y, INSERT, SEL, WORD +from group_controller import GroupController +from models import Contact, IModel, Template, Group +from tkhtmlview import HTMLLabel, HTMLText +from DataSources.dataSources import GapFillSource + + +class Settings: + def __init__(self, root): + self.root = root + self.root.title("Ustawienia") + self.root.configure(bg="lightblue") + self.root.geometry("400x400") + + def prepareInterface(self): + # TODO: tutaj powinniśmy ładować wartości z User + example_emails = ["example1@example.com", "example2@example.com", "example3@example.com"] + + label = Label( + self.root, + text="MailBuddy", + bg="lightblue", + font=("Helvetica", 24)) + + self.email_combobox = Combobox(self.root, values=example_emails) + + connect_button = Button( + self.root, + text="Połącz", + bg="lightblue", + fg="black", + command=self.connect) + + change_email_button = Button( + self.root, + text="Dodaj nowy adres mailowy", + bg="lightblue", + fg="black", + command=self.change_email) + + close_button = Button( + self.root, + text="Wyłącz ustawienia", + bg="lightblue", + fg="black", + command=self.close) + + label.pack(pady=20) + self.email_combobox.pack(pady=5) + connect_button.pack(pady=5) + change_email_button.pack(pady=5) + close_button.pack(pady=5) + + def connect(self): + email = self.email_combobox.get() + # TODO: połączenie z pocztą + messagebox.showinfo("Połączenie", f"Połączono z {email}") + + def change_email(self): + new_email = simpledialog.askstring( + "Zmień adres e-mail", "Dodaj nowy adres e-mail") + if new_email: + self.email_combobox.set(new_email) + + def close(self): + self.root.destroy() \ No newline at end of file diff --git a/Interface/TemplateEditor.py b/Interface/TemplateEditor.py new file mode 100644 index 0000000..83fcff3 --- /dev/null +++ b/Interface/TemplateEditor.py @@ -0,0 +1,138 @@ +from enum import Enum +from tkinter import Event, Tk, Button, Label, Entry, Toplevel, Misc +from tkinter.ttk import Combobox +from tkinter.constants import END, INSERT, SEL, WORD +from models import Template +from tkhtmlview import HTMLLabel, HTMLText +from DataSources.dataSources import GapFillSource + + +class TemplateEditor(Toplevel): + def __init__(self, parent: Toplevel | Tk, master: Misc, obj: Template | None = None): + super().__init__(master) + self.parent = parent + self.current_combo: Combobox = None + self.currentTemplate = obj + + self.prepareInterface() + + def prepareInterface(self): + self.title("Stwórz szablon") + + self.grid_rowconfigure(1, weight=1) + self.grid_columnconfigure(1, weight=1) + self.grid_columnconfigure(3, weight=1) + + name_label = Label(self, text="Nazwa szablonu:", bg="lightblue") + name_entry = Entry(self, bg="white", fg="black") + + self.template_text = HTMLText(self, bg="lightblue", fg="black", wrap=WORD) + self.template_text.bind("", self.__on_html_key_clicked) + self.template_text.bind("<>", self.__on_text_changed) + + self.template_preview = HTMLLabel(self, bg="lightblue", fg="black", wrap=WORD) + btn_save = Button(self, text="Zapisz", bg="lightblue", fg="black", + command=lambda: self.__save_template_clicked(name_entry.get(), self.template_text.get(1.0, END))) + btn_insert_placeholder = Button(self, text="Wstaw lukę", bg="lightblue", fg="black", + command=self.__template_window_insert_placeholder) + + name_label.grid(row=0, column=0, padx=5, pady=5) + name_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew") + self.template_text.grid(row=1, column=0, columnspan=2, padx=5, pady=5, sticky="nsew") + self.template_preview.grid(row=1, column=3, columnspan=5, padx=5, pady=5, sticky="nsew") + btn_save.grid(row=2, column=2, padx=(50, 5), pady=5, sticky="e") + btn_insert_placeholder.grid(row=2, column=3, padx=(5, 50), pady=5, sticky="w") + + if self.currentTemplate: + name_entry.insert(INSERT, self.currentTemplate.name if self.currentTemplate.name is not None else "") + self.template_text.insert(INSERT, self.currentTemplate.content if self.currentTemplate.content is not None else "") + self.template_text.event_generate("<>") + + def __on_html_key_clicked(self, event: Event): + if event.keycode not in NonAlteringKeyCodes: + self.template_text.event_generate("<>") + + def __on_text_changed(self, event): + html_text = self.template_text.get("1.0", END) + mb_tag = "MailBuddyGap>" + replacement_text = '' + + html_text = html_text.replace("<" + mb_tag, replacement_text) + html_text = html_text.replace("") + + self.template_preview.set_html(html_text) + + 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 hide_combobox(self): + if self.current_combo: + self.current_combo.destroy() + + def __template_window_insert_placeholder(self, placeholders: list[GapFillSource] = GapFillSource.all_instances) -> None: + placeholder_text = " " + combo_values = [key for placeholder in placeholders for key in placeholder.possible_values] + + def on_placeholder_selection(event): + selected_placeholder = self.current_combo.get() + if selected_placeholder: + selected_text = self.template_text.tag_ranges(SEL) + if selected_text: + self.template_text.delete(selected_text[0], selected_text[1]) + self.template_text.insert(INSERT, placeholder_text.replace(" ", selected_placeholder)) + self.template_text.event_generate("<>") + + def show_placeholder_menu(event): + self.hide_combobox() + self.current_combo = Combobox(self.template_text, values=combo_values) + self.current_combo.bind("<>", on_placeholder_selection) + self.current_combo.place(x=event.x_root, y=event.y_root) + self.current_combo.focus_set() + + close_button = Button(self.current_combo, text="X", command=self.hide_combobox, bg="white") + close_button.place(relx=0.90, rely=0, anchor="ne") + + self.template_text.insert(INSERT, placeholder_text) + self.template_text.tag_configure("placeholder", background="lightgreen") + + self.template_text.bind("", show_placeholder_menu) + + start_index = "1.0" + while True: + start_index = self.template_text.search(placeholder_text, start_index, stopindex=END) + if not start_index: + break + end_index = self.template_text.index(f"{start_index}+{len(placeholder_text)}c") + self.template_text.tag_add("placeholder", start_index, end_index) + start_index = end_index + + +class NonAlteringKeyCodes(Enum): + # List is non-exhaustive, should be tested + # via https://asawicki.info/nosense/doc/devices/keyboard/key_codes.html + + # Backspace = 8 + # Tab = 9 + Num_Key_5 = 12 + # Enter = 13 + Shift = 16 + Ctrl = 17 + Alt = 18 + Pause_Break = 19 + Caps_Lock = 20 + Esc = 27 + # Space = 32 + Page_Up = 33 + Page_Down = 34 + End = 35 + Home = 36 + Left_Arrow = 37 + Up_Arrow = 38 + Right_Arrow = 39 + Down_Arrow = 40 + Print_Screen = 44 + Insert = 45 + # Delete = 46 diff --git a/MailBuddy.pyproj b/MailBuddy.pyproj new file mode 100644 index 0000000..b399d8d --- /dev/null +++ b/MailBuddy.pyproj @@ -0,0 +1,70 @@ + + + + Debug + 2.0 + {42e8f18f-8e4d-4e59-baad-d241dd028a13} + + main.py + + . + . + {888888a0-9f3d-457c-b088-3a5042f75d52} + Standard Python launcher + Global|PythonCore|3.12 + Pytest + + + + + 10.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MailBuddy.sln b/MailBuddy.sln new file mode 100644 index 0000000..46af93b --- /dev/null +++ b/MailBuddy.sln @@ -0,0 +1,23 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "MailBuddy", "MailBuddy.pyproj", "{42E8F18F-8E4D-4E59-BAAD-D241DD028A13}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {42E8F18F-8E4D-4E59-BAAD-D241DD028A13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42E8F18F-8E4D-4E59-BAAD-D241DD028A13}.Release|Any CPU.ActiveCfg = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {540E65FD-46AF-4A50-AFC8-C0D3EE06BE25} + EndGlobalSection +EndGlobal diff --git a/interface.py b/interface.py deleted file mode 100644 index 3491378..0000000 --- a/interface.py +++ /dev/null @@ -1,609 +0,0 @@ -from collections.abc import Callable, Iterable -from enum import Enum -from sqlalchemy.exc import IntegrityError -from types import TracebackType -from traceback import print_tb -from typing import Literal, Any, NoReturn -from tkinter import Event, Menu, simpledialog, ttk, Listbox, Tk, Text, Button, Frame, Label, Entry, Scrollbar, Toplevel, Misc, messagebox, Menubutton, Canvas,Checkbutton,BooleanVar, VERTICAL, RAISED -from tkinter.ttk import Combobox -from tkinter.constants import NORMAL, DISABLED, BOTH, RIDGE, END, LEFT, RIGHT, TOP, X, Y, INSERT, SEL, WORD -from group_controller import GroupController -from models import Contact, IModel, Template, Group -from tkhtmlview import HTMLLabel, HTMLText -from DataSources.dataSources import GapFillSource - -def errorHandler(xd, exctype: type, excvalue: Exception, tb: TracebackType): - msg = f"{exctype}: {excvalue}, {print_tb(tb)}" - print(msg) - simpledialog.messagebox.showerror("Error", msg) - - -Tk.report_callback_exception = errorHandler - - -class Settings: - def __init__(self, root): - self.root = root - self.root.title("Ustawienia") - self.root.configure(bg="lightblue") - self.root.geometry("400x400") - - def prepareInterface(self): - # TODO: tutaj powinniśmy ładować wartości z User - example_emails = ["example1@example.com", "example2@example.com", "example3@example.com"] - - label = Label( - self.root, - text="MailBuddy", - bg="lightblue", - font=("Helvetica", 24)) - - self.email_combobox = Combobox(self.root, values=example_emails) - - connect_button = Button( - self.root, - text="Połącz", - bg="lightblue", - fg="black", - command=self.connect) - - change_email_button = Button( - self.root, - text="Dodaj nowy adres mailowy", - bg="lightblue", - fg="black", - command=self.change_email) - - close_button = Button( - self.root, - text="Wyłącz ustawienia", - bg="lightblue", - fg="black", - command=self.close) - - label.pack(pady=20) - self.email_combobox.pack(pady=5) - connect_button.pack(pady=5) - change_email_button.pack(pady=5) - close_button.pack(pady=5) - - def connect(self): - email = self.email_combobox.get() - # TODO: połączenie z pocztą - messagebox.showinfo("Połączenie", f"Połączono z {email}") - - def change_email(self): - new_email = simpledialog.askstring( - "Zmień adres e-mail", "Dodaj nowy adres e-mail") - if new_email: - self.email_combobox.set(new_email) - - def close(self): - self.root.destroy() - -class AppUI(): - def __init__(self) -> None: - self.root = Tk() - self.grupy: list[Group] = [] - 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_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 __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 if i not in self.szablony] - self.__update_listbox(self.template_listbox, self.szablony) - - def add_group(self, g: Group | Iterable[Group]): - if isinstance(g, Group): - if g not in self.grupy: - self.grupy.append(g) - else: - [self.grupy.append(i) for i in g if i not in self.grupy] - self.__update_listbox(self.grupy_listbox, self.grupy) - - def __add_group_clicked(self): - self.show_group_window() - - def show_group_window(self, g: Group | None = None): - group_editor = GroupEditor(self, g) - group_editor.prepareInterface() - - def __send_clicked(event) -> None: - print("send mail") - pass - - 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 = int(self.grupy_listbox.get(selected[0]).split(':')[0]) - self.show_group_window(self.grupy[elem]) - - def __group_selection_changed(self, _event): - selected: int = self.grupy_listbox.curselection() - if len(selected) > 0: - g: Group = self.grupy[selected[0]] - mails = [", ".join(x.email) for x in g.contacts] - self.entry_adres.delete(0, END) - self.entry_adres.insert(INSERT, mails) - - def __template_doubleclicked(self, _event): - ui_selection = self.template_listbox.curselection() - if len(ui_selection) > 0: - selected = self.szablony[ui_selection[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: Listbox, content: Iterable[IModel] | dict[IModel]): - if isinstance(content, Iterable): - 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): - 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) - menubar.add_command(label="Open Settings", command=self.logout) - menubar.add_command(label="Send", command=lambda: self.__send_clicked()) - - - self.root.config(menu=menubar) - - - - def __create_mailing_group_pane(self): - 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 = 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=BOTH, expand=True) - - def __create_mail_input_pane(self): - 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") - self.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) - self.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): - - root = Tk() # Otwórz ponownie okno logowania - settings = Settings(root) - settings.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: Combobox = None - self.currentTemplate = obj - - self.prepareInterface() - - def prepareInterface(self): - self.title("Stwórz szablon") - - self.grid_rowconfigure(1, weight=1) - self.grid_columnconfigure(1, weight=1) - self.grid_columnconfigure(3, weight=1) - - name_label = Label(self, text="Nazwa szablonu:", bg="lightblue") - name_entry = Entry(self, bg="white", fg="black") - - self.template_text = HTMLText(self, bg="lightblue", fg="black", wrap=WORD) - self.template_text.bind("", self.__on_html_key_clicked) - self.template_text.bind("<>", self.__on_text_changed) - - self.template_preview = HTMLLabel(self, bg="lightblue", fg="black", wrap=WORD) - btn_save = Button(self, text="Zapisz", bg="lightblue", fg="black", - command=lambda: self.__save_template_clicked(name_entry.get(), self.template_text.get(1.0, END))) - btn_insert_placeholder = Button(self, text="Wstaw lukę", bg="lightblue", fg="black", - command=self.__template_window_insert_placeholder) - - name_label.grid(row=0, column=0, padx=5, pady=5) - name_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew") - self.template_text.grid(row=1, column=0, columnspan=2, padx=5, pady=5, sticky="nsew") - self.template_preview.grid(row=1, column=3, columnspan=5, padx=5, pady=5, sticky="nsew") - btn_save.grid(row=2, column=2, padx=(50, 5), pady=5, sticky="e") - btn_insert_placeholder.grid(row=2, column=3, padx=(5, 50), pady=5, sticky="w") - - if self.currentTemplate: - name_entry.insert(INSERT, self.currentTemplate.name if self.currentTemplate.name is not None else "") - self.template_text.insert(INSERT, self.currentTemplate.content if self.currentTemplate.content is not None else "") - self.template_text.event_generate("<>") - - def __on_html_key_clicked(self, event: Event): - if event.keycode not in NonAlteringKeyCodes: - self.template_text.event_generate("<>") - - def __on_text_changed(self, event): - html_text = self.template_text.get("1.0", END) - mb_tag = "MailBuddyGap>" - replacement_text = '' - - html_text = html_text.replace("<" + mb_tag, replacement_text) - html_text = html_text.replace("") - - self.template_preview.set_html(html_text) - - 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 hide_combobox(self): - if self.current_combo: - self.current_combo.destroy() - - def __template_window_insert_placeholder(self, placeholders: list[GapFillSource] = GapFillSource.all_instances) -> None: - placeholder_text = " " - combo_values = [key for placeholder in placeholders for key in placeholder.possible_values] - - def on_placeholder_selection(event): - selected_placeholder = self.current_combo.get() - if selected_placeholder: - selected_text = self.template_text.tag_ranges(SEL) - if selected_text: - self.template_text.delete(selected_text[0], selected_text[1]) - self.template_text.insert(INSERT, placeholder_text.replace(" ", selected_placeholder)) - self.template_text.event_generate("<>") - - def show_placeholder_menu(event): - self.hide_combobox() - self.current_combo = Combobox(self.template_text, values=combo_values) - self.current_combo.bind("<>", on_placeholder_selection) - self.current_combo.place(x=event.x_root, y=event.y_root) - self.current_combo.focus_set() - - close_button = Button(self.current_combo, text="X", command=self.hide_combobox, bg="white") - close_button.place(relx=0.90, rely=0, anchor="ne") - - self.template_text.insert(INSERT, placeholder_text) - self.template_text.tag_configure("placeholder", background="lightgreen") - - self.template_text.bind("", show_placeholder_menu) - - start_index = "1.0" - while True: - start_index = self.template_text.search(placeholder_text, start_index, stopindex=END) - if not start_index: - break - end_index = self.template_text.index(f"{start_index}+{len(placeholder_text)}c") - self.template_text.tag_add("placeholder", start_index, end_index) - start_index = end_index - -class GroupEditor(Toplevel): - def __init__(self, parent: AppUI, edited: Group | None = None): - super().__init__(parent.root) - self.parent = parent - self.currentGroup = edited - - def prepareInterface(self): - name_label = Label(self, text="Nazwa grupy:", bg="lightblue") - self.name_entry = Entry(self, bg="white", fg="black") - email_label = Label(self, text="Adresy email:", bg="lightblue") - self.email_text = Text(self, bg="lightblue", fg="black", wrap=WORD) - btn_add_list_contact = Button(self, text="Dodaj z listy", bg="lightblue", fg="black", command=self.add_contact_from_list_window) - btn_save = Button(self, text="Zapisz", bg="lightblue", fg="black", command=self.__save_group_clicked) - - name_label.grid(row=0, column=0, padx=5, pady=5, sticky="w") - self.name_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew") - email_label.grid(row=1, column=0, padx=5, pady=5, sticky="w") - self.email_text.grid(row=1, column=1, padx=5, pady=5, sticky="nsew") - btn_add_list_contact.grid(row=2, column=0, padx=5, pady=5, sticky="ew") - btn_save.grid(row=3, column=0, columnspan=2, padx=5, pady=5, sticky="ew") - - self.grid_columnconfigure(1, weight=1) - self.grid_rowconfigure(1, weight=1) - - self.update() - - def update(self): - if self.currentGroup: - self.title(f"Edytuj grupę {self.currentGroup.name}") - self.name_entry.delete(0, END) - self.name_entry.insert(0, self.currentGroup.name) - self.currentGroup.contacts = GroupController.get_contacts(self.currentGroup) - self.email_text.delete('1.0', END) # Clear current content - [self.add_contact(c) for c in self.currentGroup.contacts] - else: - self.title("Dodaj grupę") - - def add_contact(self, c: Contact): - self.email_text.insert(INSERT, str(c.email) + "\n") - - def add_contact_from_list_window(self): - contact_list_window = ContactList(self, self.currentGroup) - contact_list_window.prepareInterface() - # TODO: Odebrać info o dodawanych kontaktach, wywoływać add_contact - - def __save_group_clicked(self) -> None: - if not self.currentGroup: - self.currentGroup = Group(_name = self.name_entry.get()) - else: - self.currentGroup.name = self.name_entry.get() - txt = self.email_text.get(1.0, END).strip() - email_addresses = [address for address in txt.replace("\n", "").split(",") if address.strip()] - # TODO: Przy zmianie kontrolek w grupie będzie trzeba zmienić wywoływanie konstruktora - te kontakty powinny być zapisane wcześniej, bez możliwości dodawania ich od tak z palca - for mail in email_addresses: - try: - self.currentGroup._add_contact(Contact(_email=mail)) - except AttributeError as e: - raise e - self.parent.add_group(self.currentGroup) - self.destroy() - -class ContactList(Toplevel): - def __init__(self, parent: Toplevel | GroupEditor, group: Group | None = None) -> None: - super().__init__(parent) - self.group = group - self.parent = parent - - def prepareInterface(self): - self.title("Dodaj kontakt z listy") - - group_editor_geometry = self.parent.winfo_geometry() - self.geometry(group_editor_geometry) - - contact_frame = Frame(self) - search_frame = Frame(contact_frame, bg="lightblue") - search_label = Label(search_frame, text="Wyszukaj:", bg="lightblue") - self.search_entry = Entry(search_frame, bg="white", fg="black") - search_button = Button(search_frame, text="Szukaj", bg="lightblue", fg="black", command=self.search_contact) - add_contact_button = Button(search_frame, text="Dodaj nowy kontakt", bg="lightblue", fg="black", command=self.add_manual_contact_window) - scrollbar = Scrollbar(contact_frame, orient=VERTICAL) - self.contact_canvas = Canvas(contact_frame, yscrollcommand=scrollbar.set) - self.contact_inner_frame = Frame(self.contact_canvas) - - scrollbar.config(command=self.contact_canvas.yview) - - contact_frame.pack(fill=BOTH, expand=True) - search_frame.pack(fill=X, padx=5, pady=5) - search_label.pack(side=LEFT, padx=5, pady=5) - self.search_entry.pack(side=LEFT, padx=5, pady=5, expand=True, fill=X) - search_button.pack(side=LEFT, padx=5, pady=5) - add_contact_button.pack(side=LEFT, padx=5, pady=5) - scrollbar.pack(side=RIGHT, fill=Y) - self.contact_canvas.pack(side=LEFT, fill=BOTH, expand=True) - self.contact_canvas.create_window((0, 0), window=self.contact_inner_frame, anchor='nw') - - self.contact_canvas.configure(scrollregion=self.contact_canvas.bbox("all")) - self.update() - - def clearEntries(self): - for widget in self.contact_inner_frame.winfo_children(): - widget.destroy() - - def update(self): - self.clearEntries() - self.populateWindow() - - def populateWindow(self): - shouldAddButton = self.parent != None and isinstance(self.parent, GroupEditor) - for idx, c in enumerate(Contact.all_instances): - self.create_contact_widget(c, idx, addBtn=shouldAddButton) - - if self.group: - group_contacts = GroupController.get_contacts(self.group) - group_emails = {contact.email for contact in group_contacts} - for idx, c in enumerate(Contact.all_instances): - added_to_group = c.email in group_emails - self.create_contact_widget(c, idx, added_to_group, addBtn=shouldAddButton) - - def create_contact_widget(self, c: Contact, idx: int, added_to_group: bool = False, addBtn: bool = True): - def toggle_checkbox(): - if checkbox_var.get(): - self.add_contact_to_group(c) - else: - self.remove_contact_from_group(c) - - checkbox_var = BooleanVar(value=added_to_group) - checkbox = Checkbutton(self.contact_inner_frame, variable=checkbox_var, command=toggle_checkbox) #bg="lightblue") - checkbox.grid(row=idx, column=2, padx=4, pady=4) - - Label(self.contact_inner_frame, text=f"Mail {idx+1}:").grid(row=idx, column=0, padx=5, pady=5) - Label(self.contact_inner_frame, text=f"{c.email} - {c.first_name} {c.last_name}").grid(row=idx, column=1, padx=5, pady=5) - - def add_contact_to_group(self, c: Contact): - if self.group == None: - return - - try: - GroupController.add_contact(self.group, c) - if isinstance(self.parent, GroupEditor): - self.parent.update() - except IntegrityError: - pass - - def remove_contact_from_group(self, c: Contact): - GroupController.delete_connection(self.group, c) - if isinstance(self.parent, GroupEditor): - self.parent.update() - - def search_contact(self): - search_criteria = self.search_entry.get().strip() - self.clearEntries() - - for idx, c in enumerate(Contact.all_instances): - if search_criteria in c.first_name or search_criteria in c.last_name or search_criteria in c.email: - self.create_contact_widget(c, idx) - - def add_manual_contact_window(self): - acw = AddContactWindow(self) - acw.prepareInterface() - -class AddContactWindow(Toplevel): - def __init__(self, parent: Toplevel | ContactList) -> None: - super().__init__(parent) - self.parent = parent - - def prepareInterface(self): - self.title("Dodaj Kontakt") - - email_label = Label(self, text="Adres email:", bg="lightblue") - self.email_entry = Entry(self, bg="white", fg="black") - name_label = Label(self, text="Imię:", bg="lightblue") - self.name_entry = Entry(self, bg="white", fg="black") - surname_label = Label(self, text="Nazwisko:", bg="lightblue") - self.surname_entry = Entry(self, bg="white", fg="black") - btn_add_contact = Button(self, text="Dodaj kontakt", bg="lightblue", fg="black", command=self.add_manual_contact) - - email_label.grid(row=0, column=0, padx=5, pady=5) - self.email_entry.grid(row=0, column=1, padx=5, pady=5) - name_label.grid(row=1, column=0, padx=5, pady=5) - self.name_entry.grid(row=1, column=1, padx=5, pady=5) - surname_label.grid(row=2, column=0, padx=5, pady=5) - self.surname_entry.grid(row=2, column=1, padx=5, pady=5) - btn_add_contact.grid(row=3, column=0, columnspan=2, padx=5, pady=5, sticky="ew") - - def add_manual_contact(self): - email = self.email_entry.get() - name = self.name_entry.get() - surname = self.surname_entry.get() - if email: - newContact = Contact(_email=email, _first_name=name, _last_name=surname) - self.parent.update() - self.destroy() - # TODO: Jakiś sygnał do parenta żeby się zaktualizował? - else: - messagebox.showerror("Błąd", "Podaj adres e-mail") - -class NonAlteringKeyCodes(Enum): - # List is non-exhaustive, should be tested - # via https://asawicki.info/nosense/doc/devices/keyboard/key_codes.html - - # Backspace = 8 - # Tab = 9 - Num_Key_5 = 12 - # Enter = 13 - Shift = 16 - Ctrl = 17 - Alt = 18 - Pause_Break = 19 - Caps_Lock = 20 - Esc = 27 - # Space = 32 - Page_Up = 33 - Page_Down = 34 - End = 35 - Home = 36 - Left_Arrow = 37 - Up_Arrow = 38 - Right_Arrow = 39 - Down_Arrow = 40 - Print_Screen = 44 - Insert = 45 - # Delete = 46 - diff --git a/main.py b/main.py index f94b3da..581ad72 100644 --- a/main.py +++ b/main.py @@ -3,12 +3,25 @@ from UserInfo.LoginService import * # from sys import platform from group_controller import GroupController -from models import IModel, Template, Attachment, Contact, Message, Group +from models import IModel, Template, Attachment, Contact, Message, Group, User from Triggers.triggers import ITrigger -from interface import AppUI +from Interface.AppUI import AppUI from DataSources.dataSources import DatabaseHandler, GapFillSource, IDataSource from additionalTableSetup import GroupContacts, MessageAttachment, SendAttempt + +mocking_enabled = True +mock_name = "Russ" +mock_lastname = "Connelly" +mock_login = "russ.connelly30@ethereal.email" +mock_pwd = "QQcGx1RmfVkaEMjzqZ" + +smtp_host = "smtp.ethereal.email" +smtp_port = 587 +smtp_security = "tls" + + + dbname = "localSQLite.sqlite3" dbURL = f"sqlite:///{dbname}" tables = [Template, Attachment, Contact, ITrigger, Message, Group, MessageAttachment, SendAttempt, GroupContacts] @@ -47,9 +60,15 @@ def pushQueuedInstances(): _contact_fields = GapFillSource() - # TODO win32 powidomienia - # if 'win32' in platform: - # enableWin32Integration() - + if (mocking_enabled): + try: + mock_user = User(email=mock_login, + first_name=mock_name, + last_name=mock_lastname, + password=mock_pwd) + sender = SMTPSender(mock_user) + except AttributeError as ae: + print(ae) + ui.add_periodic_task(5000, pushQueuedInstances) ui.run() diff --git a/models.py b/models.py index e58f5cc..77e4384 100644 --- a/models.py +++ b/models.py @@ -138,9 +138,13 @@ def get_by_id(cls, searched_id: int) -> Contact | None: @staticmethod def isEmail(candidate: str) -> bool: - if re.match(r"^(?!.*@.*@.*$)[^@]+@[^@]+\.[^@]+$", candidate): - return True - return False + if candidate == None: + return False + + if re.match(r"^(?!.*@.*@.*$)[^@]+@[^@]+\.[^@]+$/g", candidate): + return False + + return True # region Properties @hybrid_property @@ -158,7 +162,7 @@ def last_name(self): @email.setter def email(self, newValue: str): if not Contact.isEmail(newValue): - raise AttributeError("Value is not an email") + raise AttributeError(f"{newValue} is not an email") self._email = newValue @first_name.setter @@ -176,7 +180,7 @@ class User(): def __init__(self, first_name: str, last_name: str, email: str, password: str) -> None: - self.contact = Contact(first_name, last_name, email) + self.contact = Contact(first_name=first_name, last_name=last_name, email=email) self.password = password User.all_instances.append(self) IModel.queueSave(child=self)