commit 16a203292dff6ba9533370dd2e82d2668a788704 Author: Cyril Novel Date: Thu Jun 11 20:10:49 2020 +0200 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4bf2a22 --- /dev/null +++ b/.gitignore @@ -0,0 +1,141 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +.idea/ \ No newline at end of file diff --git a/html_template/mail.html b/html_template/mail.html new file mode 100644 index 0000000..dd52247 --- /dev/null +++ b/html_template/mail.html @@ -0,0 +1,253 @@ + + + + Paturlettre + + + + + + + +
+ Paturlettre +
+ + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ Logo +
+ +
+ + + + + +
+

{Main_Title}

+

{Sub_Title}

+
+ +
+ + + + + + + + + + + + + + + + + + +
+

Semaine précédente

+ + {EVENTS_PREVIOUS} +
+
+

Cette semaine

+ + {EVENTS_CURRENT} +
+
+

Semaine prochaine

+ + {EVENTS_NEXT} +
+
+ + + + +
+ + + + + + + +
Citation de la semaine
+ {Quote_Text}
+ — {Quote_Author} +
+
+
+ +
+ + + + + + +
+

+ Cet email vous est envoyé par Cyril NOVEL
+ parce que vous êtes inscrit sur la liste de diffusion de la Paturlettre.
+ Des questions ou des commentaires ? Envoyez un email à Paturlettre. +

+
+ +
+ + diff --git a/src/email_sender.py b/src/email_sender.py new file mode 100644 index 0000000..382b8f8 --- /dev/null +++ b/src/email_sender.py @@ -0,0 +1,195 @@ +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText + +from smtplib import SMTP_SSL +from argparse import ArgumentParser +from datetime import date, timedelta +import wikiquote + +from src.event import create_events_from_span + + +week_days = ['Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi', 'Dimanche'] +months = ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', + 'octobre', 'novembre', 'décembre'] + + +class EmailSender: + def __init__(self, smtp_server, user, password): + self._smtp_server = smtp_server + self._user = user + self._pwd = password + print("Setup OK") + + def send_paturlettre(self, recipients): + msg = MIMEMultipart('alternative') + msg['Subject'] = f'[Paturlettre] Semaine {get_week_number_and_span()[0]}' + msg['From'] = 'Paturlettre ' + msg['To'] = 'Diffusion Paturlettre ' + msg.preamble = f'La Paturlettre de la semaine {get_week_number_and_span()[0]} !' + msg.attach(MIMEText(create_plain_email(), 'plain')) + msg.attach(MIMEText(create_email_from_template(), 'html')) + print("Msg OK") + + conn = SMTP_SSL(self._smtp_server, port=465) + print("Conn OK") + conn.login(self._user, self._pwd) + print("Login OK") + try: + conn.sendmail(msg['From'], 'diffusion_paturlettre@kosmon.fr', msg.as_string()) + print("Send OK") + finally: + conn.quit() + + +def create_email_from_template(): + events_previous, events_current, events_next = get_events() + with open('../html_template/mail.html', 'r', encoding='utf-8') as tmp: + mail = tmp.read() + mail = mail.replace('{LogoAddress}', + 'https://tof.cx/images/2020/05/27/448fd7d507778064b39ba95c381cd3e8.png') + wk_and_span = get_week_number_and_span() + mail = mail.replace('{Main_Title}', f'Semaine {wk_and_span[0]}') + mail = mail.replace('{Sub_Title}', + f'du {wk_and_span[1].day} {months[wk_and_span[1].month - 1]}' + f' au {wk_and_span[2].day} {months[wk_and_span[2].month - 1]}') + + mail = mail.replace('{EVENTS_PREVIOUS}', create_html_string_for_week(events_previous)) + mail = mail.replace('{EVENTS_CURRENT}', create_html_string_for_week(events_current)) + mail = mail.replace('{EVENTS_NEXT}', create_html_string_for_week(events_next)) + + quote = wikiquote.qotd(lang='fr') + mail = mail.replace('{Quote_Text}', quote[0]) + mail = mail.replace('{Quote_Author}', quote[1]) + + return mail + + +def create_plain_email(): + events_previous, events_current, events_next = get_events() + wk_and_span = get_week_number_and_span() + mail_plain = f'Paturlettre - Semaine {wk_and_span[0]}\n' + mail_plain += f'du {wk_and_span[1].day} {months[wk_and_span[1].month - 1]}' \ + f' au {wk_and_span[2].day} {months[wk_and_span[2].month - 1]}\n' + mail_plain += '\n\n' + mail_plain += 'Semaine précédente :\n' + mail_plain += create_plain_string_for_week(events_previous) + mail_plain += '\n' + mail_plain += 'Cette semaine :\n' + mail_plain += create_plain_string_for_week(events_current) + mail_plain += '\n' + mail_plain += 'Semaine suivante :\n' + mail_plain += create_plain_string_for_week(events_next) + mail_plain += '\n\n' + + quote = wikiquote.qotd(lang='fr') + mail_plain += 'Citation de la semaine :\n' + mail_plain += quote[0] + '\n' + mail_plain += f'— {quote[1]}\n\n' + mail_plain += 'Cet email vous est envoyé par Cyril NOVEL parce que vous êtes inscrit sur la ' \ + 'liste de diffusion de la Paturlettre.\n' + mail_plain += 'Des questions ou des commentaires ? Envoyez un email à paturlettre@kosmon.fr\n' + + return mail_plain + + +def create_html_string_for_day(day_name: str, events: list, last_day: bool) -> str: + html_str = ' \n' + html_str += f' {day_name}\n' + html_str += ' \n' + + for i in range(0, len(events)): + e = events[i] + style = "width: 100%;" + if not last_day and i == len(events) - 1: + style = "width: 100%; border-bottom: 1px dashed #9ba5ae;" + html_str += f' \n' + html_str += f' {e.type_str()} · {e.event_date_as_str()}
{e.event_description()}\n' + html_str += ' ' + + return html_str + + +def create_html_string_for_no_event() -> str: + html_str = ' \n' + html_str += f' Pas d\'événements\n' + html_str += ' \n' + + return html_str + + +def create_plain_string_for_no_event() -> str: + return 'Pas d\'événements\n' + + +def create_html_string_for_week(events: list): + if not events: + return create_html_string_for_no_event() + d = {} + last_day = -1 + for i in range(0, 7): + d[i] = [e for e in events if e.get_actual_weekday() == i] + if d[i]: + last_day = i + + html_str = '' + for i in range(0, 7): + if d[i]: + html_str += create_html_string_for_day(week_days[i], d[i], i == last_day) + return html_str + + +def create_plain_string_for_week(events: list): + if not events: + return create_plain_string_for_no_event() + plain_str = '' + d = {} + for i in range(0, 7): + d[i] = [e for e in events if e.get_actual_weekday() == i] + + for i in range(0, 7): + if d[i]: + plain_str += f' - {week_days[i]} :\n' + for e in d[i]: + plain_str += f' * {e.type_str()} · {e.event_date_as_str()} · ' \ + f'{e.event_description()}\n' + return plain_str + + +def get_events(): + today = date.today() + monday = today - timedelta(days=today.weekday()) + previous_monday = monday - timedelta(days=7) + next_monday = monday + timedelta(days=7) + previous_week_events = create_events_from_span(previous_monday, + previous_monday + timedelta(days=6)) + this_week_events = create_events_from_span(monday, + monday + timedelta(days=6)) + next_week_events = create_events_from_span(next_monday, + next_monday + timedelta(days=6)) + return previous_week_events, this_week_events, next_week_events + + +def get_week_number_and_span(): + today = date.today() + monday = today - timedelta(days=today.weekday()) + return today.isocalendar()[1], monday, monday + timedelta(days=6) + + +def main(): + parser = ArgumentParser(description='Send mail') + parser.add_argument('-s', '--smtp', dest='smtp_server', + help='Json files to process, multiple values accepted') + parser.add_argument('-u', '--user', dest='user', + help='Json files to process, multiple values accepted') + parser.add_argument('-p', '--password', dest='password', + help='Json files to process, multiple values accepted') + args = parser.parse_args() + + email_sender = EmailSender(args.smtp_server, args.user, args.password) + email_sender.send_paturlettre(None) + print("Done") + + +if __name__ == '__main__': + main() diff --git a/src/event.py b/src/event.py new file mode 100644 index 0000000..d2fa7ce --- /dev/null +++ b/src/event.py @@ -0,0 +1,182 @@ +from datetime import date +from src.person import Person +from src.mariage import Mariage +from os.path import abspath, join, dirname + + +event_types = ['N', 'M', 'D'] + +members_file = abspath(join(dirname(__file__), 'data', 'members.csv')) +mariages_file = abspath(join(dirname(__file__), 'data', 'mariages.csv')) + + +class Event: + def __init__(self, event_type: str, event_date: date, current_date: date, + person_a: Person, person_b: Person = None): + if event_type not in event_types: + raise ValueError('{} is not in {}'.format(event_type, event_types)) + self._type = event_type + self._date = event_date + self._current_date = current_date + self._p_a = person_a + self._p_b = person_b + + def get_year_span(self) -> int: + return self._current_date.year - self._date.year + + def event_description(self) -> str: + y = self.get_year_span() + if self._type == 'N': + return '{}{} né{} il y a {} an{}'.format( + self._p_a.formatted_name(), + ' (a épousé {})'.format(self._p_b.formatted_name()) if self._p_b else '', + '' if self._p_a.is_male() else 'e', + y, + 's' if y > 1 else '' + ) + if self._type == 'M': + return '{} a épousé {} il y a {} an{}'.format( + self._p_a.formatted_name(), + self._p_b.formatted_name(), + y, + 's' if y > 1 else '' + ) + return '{}{} décédé{} il y a {} ans{}'.format( + self._p_a.formatted_name(), + ' (a épousé {})'.format(self._p_b.formatted_name()) if self._p_b else '', + '' if self._p_a.is_male() else 'e', + y, + 's' if y > 1 else '' + ) + + def event_date_as_str(self) -> str: + day = str(self._date.day) if self._date.day > 9 else '0' + str(self._date.day) + month = str(self._date.month) if self._date.month > 9 else '0' + str(self._date.month) + return '{}/{}/{}'.format(day, month, self._date.year) + + def get_actual_weekday(self) -> int: + return self._current_date.weekday() + + def type_str(self): + return self._type + + +def load_persons_to_dict() -> dict: + id_to_person = {} + first_line = True + with open(members_file, 'r', encoding='utf-8') as members: + for line in members: + if first_line: + first_line = False + continue + line.strip() + infos = line.split(';') + birth = infos[5].split('-') + death = None if infos[6] == 'NULL' else infos[6].split('-') + p = Person(int(infos[0]), + 'M' if infos[3] == '1' else 'F', + infos[2], + infos[1], + date(int(birth[0]), int(birth[1]), int(birth[2])), + None if not death else date(int(death[0]), int(death[1]), int(death[2])), + infos[4] == '1', + infos[7] == '1') + id_to_person[int(infos[0])] = p + return id_to_person + + +def load_mariage_to_dict() -> dict: + id_to_mariage = {} + first_line = True + with open(mariages_file, 'r', encoding='utf-8') as mariages: + for line in mariages: + if first_line: + first_line = False + continue + line.strip() + infos = line.split(';') + if infos[3] == "NULL": # Union libre, not an event + continue + m_type = 'M' + if infos[6] == '0': + m_type = 'D' + if infos[6] == '2': + m_type = 'L' + m_date = infos[3].split('-') + m = Mariage( + int(infos[0]), + int(infos[1]), + int(infos[2]), + date(int(m_date[0]), int(m_date[1]), int(m_date[2])), + infos[4] == '1', + infos[5] == '1', + m_type + ) + + if m.show_mariage(): + id_to_mariage[int(infos[0])] = m + return id_to_mariage + + +def find_significant_other(id_to_mariage: dict, uid: int) -> int: + for m in id_to_mariage.values(): + if m.in_this_union(uid) and m.show_mariage(): + return m.get_so(uid) + return -1 + + +def create_events_from_span(start: date, end: date) -> list: + id_to_person = load_persons_to_dict() + id_to_mariage = load_mariage_to_dict() + cur_year = start.year + + events = [] + + for p in id_to_person.values(): + so = find_significant_other(id_to_mariage, p.uid()) + if so == p.uid(): + print(f"What ??? {so}") + if not p.is_dead(): + birth = p.birth() + birth_in_year = date(cur_year, birth.month, birth.day) + if start <= birth_in_year <= end: + e = Event('N', birth, birth_in_year, + p, None if so == -1 else id_to_person[so]) + events.append(e) + + birth_in_year = date(cur_year + 1, birth.month, birth.day) + if start <= birth_in_year <= end: + e = Event('N', birth, birth_in_year, + p, None if so == -1 else id_to_person[so]) + events.append(e) + else: + death = p.death() + death_in_year = date(cur_year, death.month, death.day) + if start <= death_in_year <= end: + e = Event('D', death, death_in_year, + p, None if so == -1 else id_to_person[so]) + events.append(e) + + death_in_year = date(cur_year + 1, death.month, death.day) + if start <= death_in_year <= end: + e = Event('D', death, death_in_year, + p, None if so == -1 else id_to_person[so]) + events.append(e) + + for m in id_to_mariage.values(): + if not m.show_mariage(): + continue + m_date = m.event_date() + date_in_year = date(cur_year, m_date.month, m_date.day) + if start <= date_in_year <= end: + e = Event('M', m_date, date_in_year, + id_to_person[m.id_a()], id_to_person[m.id_b()]) + events.append(e) + + date_in_year = date(cur_year + 1, m_date.month, m_date.day) + if start <= date_in_year <= end: + e = Event('M', m_date, date_in_year, + id_to_person[m.id_a()], id_to_person[m.id_b()]) + events.append(e) + + return events diff --git a/src/mariage.py b/src/mariage.py new file mode 100644 index 0000000..a00bd18 --- /dev/null +++ b/src/mariage.py @@ -0,0 +1,37 @@ +from datetime import date + +unions = ['D', 'M', 'L'] # Divorce (0), Mariage ou Pacs (1), Union Libre (2) + + +class Mariage: + def __init__(self, uid: int, id_a: int, id_b: int, event_date: date, last_mariage: bool, + hide: bool, union: str): + self._uid = uid + self._id_a = id_a + self._id_b = id_b + self._date = event_date + self._last_mariage = last_mariage + self._hide = hide + if union not in unions: + raise ValueError('{} is not in {}'.format(union, unions)) + self._union = union + + def show_mariage(self): + return not self._hide and self._union != 'D' and self._last_mariage + + def event_date(self): + return self._date + + def in_this_union(self, uid: int) -> bool: + return self._id_a == uid or self._id_b == uid + + def get_so(self, uid: int) -> int: + if uid == self._id_a: + return self._id_b + return self._id_a + + def id_a(self): + return self._id_a + + def id_b(self): + return self._id_b diff --git a/src/person.py b/src/person.py new file mode 100644 index 0000000..01949dc --- /dev/null +++ b/src/person.py @@ -0,0 +1,45 @@ +from datetime import date + +genders = ['M', 'F'] + + +class Person: + def __init__(self, uid: int, gender: str, first_name: str, last_name: str, + birth: date, death: date, in_law: bool, hide: bool): + if gender not in genders: + raise ValueError('{} is not in {}'.format(gender, genders)) + self._gender = gender + self._first = first_name + self._last = last_name + self._birth = birth + self._death = death + self._in_law = in_law + self._uid = uid + self._hide = hide + + def uid(self): + return self._uid + + def is_dead(self): + return self._death is not None + + def hide(self): + return self._hide + + def death(self, death: date = None) -> date: + if death is not None: + self._death = death + return self._death + + def birth(self, birth: date = None) -> date: + if birth is not None: + self._birth = birth + return self._birth + + def formatted_name(self): + return '{}{} {}'.format('† ' if self.is_dead() else '', + self._first.title(), + self._last.upper()) + + def is_male(self): + return self._gender == 'M' diff --git a/utils/rewrite_data.py b/utils/rewrite_data.py new file mode 100644 index 0000000..56772f3 --- /dev/null +++ b/utils/rewrite_data.py @@ -0,0 +1,67 @@ +id_paturle = [] +member_lines = [] +with open('E:/Documents/GitHub/GeneaLetter/src/data/members.csv', 'r', encoding='utf-8') as members: + first_line = True + for line in members: + if first_line: + first_line = False + continue + line = line.strip() + info = line.split(';') + id_paturle.append(int(info[0])) + member_lines.append(line) +id_translation = {} + +id_paturle = sorted(id_paturle) +for i in range(0, len(id_paturle)): + id_translation[id_paturle[i]] = i + +with open('E:/Documents/GitHub/GeneaLetter/src/data/members.csv', 'r', encoding='utf-8') as ori_members: + with open('E:/Documents/GitHub/GeneaLetter/src/data/members_new.csv', 'w', encoding='utf-8') as new_members: + first_line = True + for line in ori_members: + if first_line: + new_members.write(line) + first_line = False + continue + line = line.strip() + info = line.split(';') + info[0] = str(id_translation[int(info[0])]) + line = ";".join(info) + "\n" + new_members.write(line) + +id_mariage = [] +with open('E:/Documents/GitHub/GeneaLetter/src/data/mariages.csv', 'r', encoding='utf-8') as old_mariage: + with open('E:/Documents/GitHub/GeneaLetter/src/data/mariages_new.csv', 'w', encoding='utf-8') as new_mariage: + first_line = True + for line in old_mariage: + if first_line: + new_mariage.write(line) + first_line = False + continue + line = line.strip() + info = line.split(';') + id_mariage.append(int(info[0])) + info[1] = str(id_translation[int(info[1])]) + info[2] = str(id_translation[int(info[2])]) + line = ";".join(info) + "\n" + new_mariage.write(line) + +id_translation = {} +id_mariage = sorted(id_mariage) +for i in range(0, len(id_mariage)): + id_translation[id_mariage[i]] = i + +with open('E:/Documents/GitHub/GeneaLetter/src/data/mariages_new.csv', 'r', encoding='utf-8') as old_mariage: + with open('E:/Documents/GitHub/GeneaLetter/src/data/mariages_ok.csv', 'w', encoding='utf-8') as new_mariage: + first_line = True + for line in old_mariage: + if first_line: + new_mariage.write(line) + first_line = False + continue + line = line.strip() + info = line.split(';') + info[0] = str(id_translation[int(info[0])]) + line = ";".join(info) + "\n" + new_mariage.write(line)