diff --git a/DataSources/dataSourceAbs.py b/DataSources/dataSourceAbs.py new file mode 100644 index 0000000..c9c83ae --- /dev/null +++ b/DataSources/dataSourceAbs.py @@ -0,0 +1,29 @@ +from abc import abstractmethod + + +class IDataSource(): + current_instance = None + + @classmethod + def __subclasshook__(cls, subclass: type) -> bool: + return (hasattr(subclass, 'GetData') and + callable(subclass.GetData) and + hasattr(subclass, 'checkIntegrity') and + callable(subclass.checkIntegrity) and + hasattr(subclass, 'LoadSavedState') and + callable(subclass.LoadSavedState) + ) + + @abstractmethod + def GetData(self): + raise RuntimeError + + @abstractmethod + def checkIntegrity(self): + raise RuntimeError + + @abstractmethod + def LoadSavedState(self): + """Collect all data saved in data source and instantiate adjacent model objects + """ + raise RuntimeError \ No newline at end of file diff --git a/DataSources/dataSources.py b/DataSources/dataSources.py index 57d09b4..6164db5 100644 --- a/DataSources/dataSources.py +++ b/DataSources/dataSources.py @@ -1,10 +1,11 @@ from __future__ import annotations -from abc import ABCMeta, abstractmethod from collections.abc import Iterable from enum import Enum +from .dataSourceAbs import IDataSource from pandas import read_csv, read_excel, DataFrame from additionalTableSetup import GroupContacts -from models import DataImport, IModel, Contact +from group_controller import GroupController +from models import DataImport, Group, IModel, Contact, Template import sqlalchemy as alchem import sqlalchemy.orm as orm from sqlalchemy.exc import IntegrityError @@ -13,33 +14,6 @@ class SupportedDbEngines(Enum): SQLite=1 -class IDataSource(): - current_instance = None - - @classmethod - def __subclasshook__(cls, subclass: type) -> bool: - return (hasattr(subclass, 'GetData') and - callable(subclass.GetData) and - hasattr(subclass, 'checkIntegrity') and - callable(subclass.checkIntegrity) and - hasattr(subclass, 'LoadSavedState') and - callable(subclass.LoadSavedState) - ) - - @abstractmethod - def GetData(self): - raise RuntimeError - - @abstractmethod - def checkIntegrity(self): - raise RuntimeError - - @abstractmethod - def LoadSavedState(self): - """Collect all data saved in data source and instantiate adjacent model objects - """ - raise RuntimeError - class DatabaseHandler(IDataSource): def __init__(self, connectionString: str, tableCreators: Iterable[IModel], engine: SupportedDbEngines = SupportedDbEngines.SQLite) -> None: @@ -118,6 +92,20 @@ def LoadSavedState(self) -> None: print(e) continue IModel.run_loading = False + self.runAdditionalBindings() + + + def runAdditionalBindings(self): + for g in Group.all_instances: + g.contacts = GroupController.get_contacts(g) + + for t in Template.all_instances: + if t.dataimport_id != None: + for di in DataImport.all_instances: + if t.dataimport_id == di.id: + t.dataimport = di + break # TODO w razie dodanie większej ilości di - templatek + def Update(self, obj: IModel): Session = orm.sessionmaker(bind=self.dbEngineInstance) @@ -164,7 +152,6 @@ def GetData(self) -> DataFrame: print("Błąd podczas wczytywania pliku XLSX:", e) return None - class CSVHandler(IDataSource): def __init__(self, path: str) -> None: self.file_path = path @@ -187,9 +174,10 @@ class GapFillSource(): all_instances: list[GapFillSource] = [] def __init__(self, source: IDataSource | IModel = Contact) -> None: - if isinstance(source, IDataSource): - self.iData: IDataSource = source - elif isinstance(source, DataImport) or isinstance(source, list): + # if isinstance(source, IDataSource): + # self.iData: IDataSource = source + # el + if isinstance(source, DataImport) or isinstance(source, list): self.model_source: IModel = source elif issubclass(source, IModel): self.model_source: IModel = source @@ -200,25 +188,28 @@ def __init__(self, source: IDataSource | IModel = Contact) -> None: self.get_possible_values() def get_possible_values(self): - if hasattr(self, "iData"): - idata_type = type(self.iData) - match(idata_type): - case type(DatabaseHandler): - # openTablePicker() - pass - case type(XLSXHandler): - # openSheetPicker() - pass - case type(CSVHandler): - # openCsvPicker() - pass - elif hasattr(self, "model_source"): + # if hasattr(self, "iData"): + # idata_type = type(self.iData) + # match(idata_type): + # case type(DatabaseHandler): + # # openTablePicker() + # pass + # case type(XLSXHandler): + # # openSheetPicker() + # pass + # case type(CSVHandler): + # # openCsvPicker() + # pass + # el + if hasattr(self, "model_source"): if self.model_source == Contact: self.possible_values = { name: attr for name, attr in Contact.__dict__.items() if isinstance(attr, hybrid_property) and attr != "all_instances" } elif isinstance(self.model_source, DataImport): self.possible_values = self.model_source.getColumnPreview() else: raise AttributeError(f"{type(self.model_source)} isn't supported") + else: + raise AttributeError(f"Incorrectly created GapFillSource, expected 'model_source'={self.model_source} to be present.") @staticmethod def getPreviewText(searched: str) -> str | None: diff --git a/Interface/AddContactWindow.py b/Interface/AddContactWindow.py index 8648394..9a54a0e 100644 --- a/Interface/AddContactWindow.py +++ b/Interface/AddContactWindow.py @@ -1,16 +1,5 @@ -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 tkinter import Button, Label, Entry, Toplevel, messagebox +from models import Contact class AddContactWindow(Toplevel): diff --git a/Interface/AppUI.py b/Interface/AppUI.py index 5fdc127..61b017d 100644 --- a/Interface/AppUI.py +++ b/Interface/AppUI.py @@ -80,21 +80,10 @@ def __exit_clicked(self) -> NoReturn | None: exit() def update_templates(self): - # 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.szablony = Template.all_instances self.__update_listbox(self.template_listbox, self.szablony) def update_groups(self): - # 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.grupy = Group.all_instances self.__update_listbox(self.grupy_listbox, self.grupy) @@ -124,7 +113,8 @@ def __send_clicked(self) -> None: u = User.GetCurrentUser() self.sender.Send(self.selected_mailing_group, self.selected_template_group, u) - #send_email() + messagebox.showinfo("Zakończono wysyłanie", "Ukończono wysyłanie maili") + def __template_selection_changed(self, _event): selected = self.template_listbox.curselection() diff --git a/Interface/ContactList.py b/Interface/ContactList.py index c7f1859..3315977 100644 --- a/Interface/ContactList.py +++ b/Interface/ContactList.py @@ -1,16 +1,8 @@ -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 tkinter import Button, Frame, Label, Entry, Scrollbar, Toplevel, Canvas, Checkbutton, BooleanVar, VERTICAL +from tkinter.constants import BOTH, LEFT, RIGHT, X, Y from group_controller import GroupController -from models import Contact, IModel, Template, Group -from tkhtmlview import HTMLLabel, HTMLText -from DataSources.dataSources import GapFillSource +from models import Contact, Group from .AddContactWindow import AddContactWindow class ContactList(Toplevel): diff --git a/Interface/Settings.py b/Interface/Settings.py index 6428791..8ff0c05 100644 --- a/Interface/Settings.py +++ b/Interface/Settings.py @@ -1,18 +1,7 @@ -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 import simpledialog, Tk, Button, Label, Entry, Toplevel, messagebox 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, User -from tkhtmlview import HTMLLabel, HTMLText -from DataSources.dataSources import GapFillSource -#from main import ui -#import MessagingService.smtp_data +from models import User class Settings(Toplevel): diff --git a/Interface/TemplateEditor.py b/Interface/TemplateEditor.py index 51ca9ec..f453683 100644 --- a/Interface/TemplateEditor.py +++ b/Interface/TemplateEditor.py @@ -104,7 +104,8 @@ def update_preview(): 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.name = template_name + self.currentTemplate.content = template_content self.parent.update_templates() self.destroy() diff --git a/MessagingService/senders.py b/MessagingService/senders.py index 3f3d5cd..0684f3c 100644 --- a/MessagingService/senders.py +++ b/MessagingService/senders.py @@ -1,4 +1,6 @@ from abc import ABCMeta, abstractmethod +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText from smtplib import * from models import Group, Template, User, Message @@ -30,10 +32,17 @@ def Send(self, g: Group, t: Template, u: User) -> None: server.login(u._email, u.password) print("logged in successfully") emailsSent = 0 + print(f"Sending {t.name} to {len(g.contacts)} contacts") for contact in g.contacts: + print(f"Sending do {contact.email}") message = Message(t, contact) - #server.sendmail(u._email, contact.email, message.getParsedBody()) - server.sendmail(u._email, contact.email, message.prepareMail()) + mimemsg = MIMEMultipart("alternative") + mimemsg["From"] = u._email + mimemsg["To"] = contact.email + mimemsg["Subject"] = "MailBuddy mailing" # TODO Temat maila w TemplateEditor, aby dało się go parametryzować + xd = MIMEText(message.getParsedBody(), "html") + mimemsg.attach(xd) + server.send_message(mimemsg) emailsSent += 1 server.quit() print(f"Sent {emailsSent}") diff --git a/group_controller.py b/group_controller.py index 12347ff..8dcf9a8 100644 --- a/group_controller.py +++ b/group_controller.py @@ -1,11 +1,11 @@ from additionalTableSetup import GroupContacts from models import Group, Contact -from DataSources.dataSources import DatabaseHandler +from DataSources.dataSourceAbs import IDataSource class GroupController: - dbh: DatabaseHandler = None + dbh: IDataSource = None @classmethod - def setDbHandler(cls, handler: DatabaseHandler) -> None: + def setDbHandler(cls, handler: IDataSource) -> None: cls.dbh = handler @classmethod diff --git a/models.py b/models.py index 482c88b..71a15a8 100644 --- a/models.py +++ b/models.py @@ -63,8 +63,6 @@ def pushQueuedInstances(): IModel.retrieveAdditionalQueued.remove(o) - - class DataImport(IModel): all_instances: list[DataImport] = [] __tablename__ = "DataImport" @@ -81,6 +79,7 @@ def __init__(self, **kwargs) -> None: self.content = kwargs.pop('_content', None) DataImport.all_instances.append(self) IModel.queueSave(child=self) + print(f"Utworzono {type(self)}") def getColumnPreview(self) -> dict | None: workbook = load_workbook(self.localPath, read_only=True) @@ -97,23 +96,23 @@ def getColumnPreview(self) -> dict | None: return result if len(result) > 0 else None def GetRow(self, email: str) -> dict[str, str]: - workbook = load_workbook(self.localPath, read_only=True) - result = dict() - for sheet in workbook: - first_row = next(sheet.iter_rows(values_only=True)) - if "Email" not in first_row: - continue - emailColumnIdx = first_row.index("Email") - - for row in sheet.iter_rows(min_row=2, values_only=True): - if row[emailColumnIdx] == email: - for idx, column in enumerate(first_row): - result[column] = row[idx] + with open(self.localPath) as r: + workbook = load_workbook(r, read_only=True) + result = dict() + for sheet in workbook: + first_row = next(sheet.iter_rows(values_only=True)) + if "Email" not in first_row: + continue + emailColumnIdx = first_row.index("Email") + + for row in sheet.iter_rows(min_row=2, values_only=True): + if row[emailColumnIdx] == email: + for idx, column in enumerate(first_row): + result[column] = row[idx] break - break - break - if len(result) == 0: - raise AttributeError("Nie znaleziono odpowiadającej linijki w pliku z danymi do uzupełnienia") + break + if len(result) == 0: + raise AttributeError("Nie znaleziono odpowiadającej linijki w pliku z danymi do uzupełnienia") return result @@ -168,6 +167,7 @@ class Template(IModel): # dataImportRel = relationship(DataImport, foreign_keys=[DataImport._id]) def __init__(self, **kwargs) -> None: + self.obj_creation = True self.id: int = kwargs.pop('_id', None) self.name: str = kwargs.pop('_name', None) self.content: object = kwargs.pop('_content', None) @@ -175,6 +175,8 @@ def __init__(self, **kwargs) -> None: self.dataimport_id: int = kwargs.pop("_dataimport_id", None) Template.all_instances.append(self) IModel.queueSave(child=self) + self.obj_creation = False + print(f"Utworzono {type(self)}") def __str__(self) -> str: @@ -225,17 +227,20 @@ def id(self, newValue: int): @name.setter def name(self, value: str | None): self._name = value - IModel.queueToUpdate(self) + if not self.obj_creation: + IModel.queueToUpdate(self) @content.setter def content(self, value: str | None): self._content = value - IModel.queueToUpdate(self) + if not self.obj_creation: + IModel.queueToUpdate(self) @dataimport_id.setter def dataimport_id(self, value: int | None): self._dataimport_id = value - IModel.queueToUpdate(self) + if not self.obj_creation: + IModel.queueToUpdate(self) IModel.retrieveAdditionalData(self) #endregion @@ -255,6 +260,7 @@ def __init__(self, path, type, attachment_id: int | None = None) -> None: self.type = type Attachment.all_instances.append(self) IModel.queueSave(child=self) + print(f"Utworzono {type(self)}") # def prepareAttachment(self): # att = MIMEApplication(open(self.path, "rb").read(), _subtype=self.type) @@ -285,6 +291,7 @@ def __init__(self, **kwargs) -> None: self.last_name = kwargs.pop("_last_name", "") Contact.all_instances.append(self) IModel.queueSave(child=self) + print(f"Utworzono {type(self)}") def __str__(self) -> str: return f"{self.first_name} {self.last_name}, <{self.email}>" @@ -404,6 +411,7 @@ def __init__(self, **kwargs) -> None: self._smtp_socket_type = "SSL" User.all_instances.append(self) IModel.queueSave(child=self) + print(f"Utworzono {type(self)}") # region Properties @hybrid_property @@ -587,29 +595,21 @@ def __init__(self, template: Template, recipient: Contact, self.template_id = template.id Message.all_instances.append(self) IModel.queueSave(child=self) + print(f"Utworzono {type(self)}") def getParsedBody(self) -> str: data = dict() - if self.template.dataimport: + if self.template.dataimport_id != None: data = self.template.dataimport.GetRow(self.email) body: str = self.template.FillGaps(data) + print(f"Mail: {body}") return body - - #def prepareMail(self) -> str: - #pathToXlsx = self.template.dataimport._localPath - #print(pathToXlsx) - - #template = self.template._content - - #return body - - class Group(IModel): all_instances: list[Group] = [] __tablename__ = "Groups" - _id = Column("id", Integer, primary_key=True) + _id = Column("id", Integer, primary_key=True, autoincrement="auto") _name = Column("name", String(100), nullable=True) def __init__(self, **kwargs): @@ -655,10 +655,7 @@ def name(self): @id.setter def id(self, newValue: int): - if newValue: - self._id = newValue - else: - self._id = max((i.id for i in Group.all_instances), default=-1) + 1 + self._id = newValue @name.setter def name(self, value: str | None):