First commit
This commit is contained in:
commit
16a203292d
7 changed files with 920 additions and 0 deletions
141
.gitignore
vendored
Normal file
141
.gitignore
vendored
Normal file
|
|
@ -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/
|
||||||
253
html_template/mail.html
Normal file
253
html_template/mail.html
Normal file
|
|
@ -0,0 +1,253 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Paturlettre</title>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<style type="text/css">
|
||||||
|
/* FONTS */
|
||||||
|
@media screen {
|
||||||
|
@font-face {
|
||||||
|
font-family: "Open Sans";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local("Open Sans"), local("OpenSans"), url(https://fonts.gstatic.com/s/opensans/v13/cJZKeOuBrn4kERxqtaUH3ZBw1xU1rKptJj_0jans920.woff2) format("woff2");
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: "Open Sans";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local("Open Sans Bold"), local("OpenSans-Bold"), url(https://fonts.gstatic.com/s/opensans/v13/k3k702ZOKiLJc3WVjuplzBampu5_7CjHW5spxoeN3Vs.woff2) format("woff2");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CLIENT-SPECIFIC STYLES */
|
||||||
|
body,
|
||||||
|
table,
|
||||||
|
td,
|
||||||
|
a {
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
table,
|
||||||
|
td {
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RESET STYLES */
|
||||||
|
img {
|
||||||
|
border: 0;
|
||||||
|
height: auto;
|
||||||
|
line-height: 100%;
|
||||||
|
outline: none;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse !important;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
height: 100% !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* iOS BLUE LINKS */
|
||||||
|
a[x-apple-data-detectors] {
|
||||||
|
color: inherit !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-weight: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MOBILE STYLES */
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
h1 {
|
||||||
|
font-size: 32px !important;
|
||||||
|
line-height: 32px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td.genea_row {
|
||||||
|
padding: 2px 5px 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.genea_header {
|
||||||
|
padding: 3px 5px 0px;
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.min_info {
|
||||||
|
font-size: 90%;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.day_row {
|
||||||
|
padding: 3px 5px 0px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 110%;
|
||||||
|
color: #229027;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ANDROID CENTER FIX */
|
||||||
|
div[style*="margin: 16px 0;"] {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body style="background-color: #eef3ee; margin: 0 !important; padding: 0 !important;">
|
||||||
|
<!-- HIDDEN PREHEADER TEXT -->
|
||||||
|
<div style="display: none; font-size: 1px; color: #fefefe; line-height: 1px; font-family: 'Open Sans', 'OpenSans', Segoe UI, Helvetica, Arial, sans-serif; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden;">
|
||||||
|
Paturlettre
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="font-family: 'Open Sans', 'OpenSans', Segoe UI, Helvetica, Arial, sans-serif;">
|
||||||
|
<!-- LOGO -->
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#34ce3b" align="center">
|
||||||
|
<!--[if (gte mso 9)|(IE)]>
|
||||||
|
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="600">
|
||||||
|
<![endif]-->
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" style="padding: 15px 10px 15px 10px;">
|
||||||
|
<img alt="Logo" src="{LogoAddress}" width="450" height="75" style="display: block; width: 300px; max-width: 450px; min-width: 75px; border="0">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!--[if (gte mso 9)|(IE)]>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- HERO -->
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#34ce3b" align="center" style="padding: 0px 10px 0px 10px;">
|
||||||
|
<!--[if (gte mso 9)|(IE)]>
|
||||||
|
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="600">
|
||||||
|
<![endif]-->
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#ffffff" align="center" valign="top" style="padding: 20px 20px 20px 20px; border-radius: 20px 20px 0px 0px; color: #2a2f34; font-weight: 400; letter-spacing: 0px;">
|
||||||
|
<h1 style="font-size: 32px; font-weight: 400; margin: 0;">{Main_Title}</h1>
|
||||||
|
<h2 style="font-size: 18px; font-weight: 200; margin-top: 5px; margin-bottom: 0px;">{Sub_Title}</h2>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!--[if (gte mso 9)|(IE)]>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- COPY BLOCK -->
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#eef3ee" align="center" style="padding: 0px 10px 0px 10px;">
|
||||||
|
<!--[if (gte mso 9)|(IE)]>
|
||||||
|
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="600">
|
||||||
|
<![endif]-->
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
|
||||||
|
<!-- COPY -->
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#ffffff" align="center" style="padding: 0px 30px 20px 30px; color: #808980; font-size: 13px; font-weight: 400; text-align: center;">
|
||||||
|
<h3>Semaine précédente</h3>
|
||||||
|
<table style="width: 100%;">
|
||||||
|
{EVENTS_PREVIOUS}
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- COPY -->
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#ffffff" align="center" style="padding: 0px 30px 20px 30px; color: #2b342a; font-size: 16px; font-weight: 400; text-align: center;">
|
||||||
|
<h3>Cette semaine</h3>
|
||||||
|
<table style="width: 100%;">
|
||||||
|
{EVENTS_CURRENT}
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- COPY -->
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#ffffff" align="center" style="padding: 0px 30px 20px 30px; color: #808980; font-size: 13px; font-weight: 400; text-align: center;">
|
||||||
|
<h3>Semaine prochaine</h3>
|
||||||
|
<table style="width: 100%;">
|
||||||
|
{EVENTS_NEXT}
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- QUOTE -->
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#ffffff" align="left" style="border-radius: 0px 0px 20px 20px;">
|
||||||
|
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding: 10px 30px 40px 30px;">
|
||||||
|
<table border="0" cellspacing="0" cellpadding="0">
|
||||||
|
<tr>
|
||||||
|
<td width="100%" align="center" style="font-size: 13px; color: #2a2f34; text-decoration: none; display: inline-block; text-align: center; margin-bottom: 3px;"><i>Citation de la semaine</i></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="font-size: 16px; color: #2a2f34; text-decoration: none; padding: 12px 16px; border-radius: 10px; border: 1px solid #dce0e3; display: inline-block; text-align: left;" bgcolor="#fff">
|
||||||
|
<i>{Quote_Text}</i><br />
|
||||||
|
— {Quote_Author}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!--[if (gte mso 9)|(IE)]>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- FOOTER -->
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#eef3ee" align="center" style="padding: 0px 10px 0px 10px;">
|
||||||
|
<!--[if (gte mso 9)|(IE)]>
|
||||||
|
<table align="center" border="0" cellspacing="0" cellpadding="0" width="600">
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="600">
|
||||||
|
<![endif]-->
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 600px;">
|
||||||
|
<!-- NAVIGATION -->
|
||||||
|
<tr>
|
||||||
|
<td bgcolor="#eef3ee" align="center" style="padding: 30px 30px 30px 30px; color: #9ba5ae; font-size: 14px; font-weight: 400; line-height: 18px;">
|
||||||
|
<p style="margin: 0;">
|
||||||
|
Cet email vous est envoyé par Cyril NOVEL<br />
|
||||||
|
parce que vous êtes inscrit sur la liste de diffusion de la Paturlettre.<br />
|
||||||
|
Des questions ou des commentaires ? Envoyez un email à <a href="mailto:paturlettre@kosmon.fr" style="color: #34ce3b;">Paturlettre</a>.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<!--[if (gte mso 9)|(IE)]>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
195
src/email_sender.py
Normal file
195
src/email_sender.py
Normal file
|
|
@ -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 <paturlettre@kosmon.fr>'
|
||||||
|
msg['To'] = 'Diffusion Paturlettre <no_reply_paturlettre@kosmon.fr>'
|
||||||
|
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 = ' <tr style="width: 100%;">\n'
|
||||||
|
html_str += f' <td class="day_row">{day_name}</td>\n'
|
||||||
|
html_str += ' </tr>\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' <tr style="{style}">\n'
|
||||||
|
html_str += f' <td class="genea_row"><span class="min_info"><b>{e.type_str()}</b> · {e.event_date_as_str()} </span><br />{e.event_description()}</td>\n'
|
||||||
|
html_str += ' </tr>'
|
||||||
|
|
||||||
|
return html_str
|
||||||
|
|
||||||
|
|
||||||
|
def create_html_string_for_no_event() -> str:
|
||||||
|
html_str = ' <tr style="width: 100%;">\n'
|
||||||
|
html_str += f' <td class="day_row">Pas d\'événements</td>\n'
|
||||||
|
html_str += ' </tr>\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()
|
||||||
182
src/event.py
Normal file
182
src/event.py
Normal file
|
|
@ -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
|
||||||
37
src/mariage.py
Normal file
37
src/mariage.py
Normal file
|
|
@ -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
|
||||||
45
src/person.py
Normal file
45
src/person.py
Normal file
|
|
@ -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'
|
||||||
67
utils/rewrite_data.py
Normal file
67
utils/rewrite_data.py
Normal file
|
|
@ -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)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue