Coverage for src/ensae_teaching_cs/automation_students/send_feedback.py: 72%
144 statements
« prev ^ index » next coverage.py v7.1.0, created at 2023-04-28 06:23 +0200
« prev ^ index » next coverage.py v7.1.0, created at 2023-04-28 06:23 +0200
1# -*- coding: utf-8 -*-
2"""
3@file
4@brief Some automation helpers to grab mails from students about projects.
5"""
7import time
8import random
9import numpy
10from pyquickhelper.loghelper import noLOG
11from pyquickhelper.texthelper.templating import apply_template
12from pymmails.sender import send_email
15template_mail_feedback = """
16<p>{{ begin }}</p>
17<p><b>{{ col_name }}</b></p>
18<p>{{ name }}</p>
19{{ content }}
20<p>{{ end }}</p>
21"""
23template_mail_columns = "<p><b>{{ key }}</b></p><p>{{ value }}</p>\n"
26def enumerate_feedback(df1, col_group="Groupe",
27 col_mail="Mail", col_name="Name",
28 cols=["Sujet", "Rapport", "Code", "Soutenance"],
29 subject=None, begin=None, end=None,
30 template=template_mail_feedback,
31 template_col=template_mail_columns,
32 engine="jinja2", exc=True, fLOG=noLOG):
33 """
34 Sends feedback to students.
36 @param df1 dataframe
37 @param col_group name of the column which contains the group definition
38 @param col_mail name of the column which contains the mail of the members
39 @param col_name name of the column which contains the names of the members
40 @param cols list of columns to add to the mails, if there are multiple values
41 per group, they will be joined by space or another separator
42 if an element in this list is a tuple ``(col_name, sep)``
43 @param subject subject of the mail
44 @param begin beginning of the mail
45 @param end end of the mail (signature)
46 @param template template of the mail
47 @param template_col template for additional columns, the outcome will be joined
48 to fill ``{{ content }}`` in the other template
49 @param engine engine for the template
50 @param exc raise an exception if there is no mail
51 @return enumerate mails content as tuple *(mail, html, text)*
53 Example of dataframe containing feedback:
55 +------+-----------+--------+---------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------+
56 | Mail | Name | Groupe | Sujet | Pitch | Code |
57 +======+===========+========+=======================================+===================================================================================================================================================================================================================================================================+==================================================================================================================================================+
58 | | AAA bbb | 1 | | | |
59 +------+-----------+--------+---------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------+
60 | | ABA ccc | 1 | jeu de hex | ok | ok |
61 +------+-----------+--------+---------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------+
62 | | VVV uuu | 2 | | | |
63 +------+-----------+--------+---------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------+
64 | | ZZZZ xxxx | 2 | élections US, twitter, nuages de mots | ok | ok |
65 +------+-----------+--------+---------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------+
66 | | GGG ffff | 3 | distribution des sièges dans un avion | ok | Les print peuvent être remplacés par une autre fonction afin de désactiver les print qui ne servent qu'à la mise au point. |
67 +------+-----------+--------+---------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------+
68 | | ?? | 31 | | | |
69 +------+-----------+--------+---------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------+
70 | | RRRR yyyy | 31 | analyse de texte / nuage de mots | Il faut éviter le code dans le contenu du pitch. Le pitch est un peu flou quant aux raisons qui vous poussent à développer votre propre tokenizer. A bien justifier avant de vous lancer dans ce type de travail et ne pas oublier la question de son évaluation. | L'interface graphique est-elle indispensable ? Le code alterne fonction, lecture de texte. N'hésitez pas à séparer les deux pour le rendu final. |
71 +------+-----------+--------+---------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------+
73 """
75 if begin is None:
76 raise ValueError("begin cannot be None, it should be string.")
77 if end is None:
78 raise ValueError("end cannot be None, it should be your signature.")
79 if subject is None:
80 raise ValueError(
81 "subject cannot be None, it should be the subject of the mail.")
83 def sums(spl):
84 spl = [_ for _ in spl if isinstance(_, str) if "??" not in _]
85 return ";".join(spl)
87 def sums2(spl):
88 spl = [_ for _ in spl if isinstance(_, str)]
89 return ", ".join(spl)
91 def sums3(spl, sep):
92 res = []
93 for _ in spl:
94 if isinstance(_, str):
95 try:
96 i = int(_)
97 _ = i
98 except ValueError:
99 try:
100 i = float(_)
101 _ = i
102 except ValueError:
103 pass
104 if isinstance(_, float):
105 if numpy.isnan(_):
106 continue
107 if int(_) == _:
108 s_ = str(int(_))
109 else:
110 s_ = str(_)
111 else:
112 s_ = str(_)
113 if s_ not in res:
114 res.append(s_)
115 # pandas seems to change the type of the value
116 # if extra characters are not added
117 return sep.join(res)
119 def clean_value(s):
120 if isinstance(s, float):
121 if numpy.isnan(s):
122 return ""
123 elif int(s) == s:
124 return str(int(s))
125 else:
126 return str(s)
127 else:
128 return str(s).strip()
130 begin = begin.replace("\n", "<br />\n")
131 end = end.replace("\n", "<br />\n")
133 aggs = {col_mail: sums}
134 if col_name is not None:
135 aggs[col_name] = sums2
136 for c in cols:
137 if isinstance(c, tuple):
138 aggs[c[0]] = lambda s, sep=c[1]: sums3( # pylint: disable=W0631,W0640
139 s, sep) # pylint: disable=W0631,W0640
140 else:
141 aggs[c] = lambda s: sums3(s, " ")
143 if col_group is not None:
144 group = df1.groupby(col_group).agg(aggs)
145 common = dict(col_group=col_group,
146 col_name=col_name, col_mail=col_mail)
147 common_rev = {v: k for k, v in common.items()}
148 lc = list(group.columns)
149 colsi = [lc.index(c[0] if isinstance(c, tuple) else c) for c in cols]
150 else:
151 # already aggregated by group
152 group = df1.groupby(col_mail).agg(aggs)
153 common = dict(col_name=col_name, col_mail=col_mail)
154 common_rev = {v: k for k, v in common.items()}
155 lc = list(group.columns)
156 colsi = [lc.index(c[0] if isinstance(c, tuple) else c) for c in cols]
158 for row in group.itertuples(index=False):
159 # key, value pairs
160 content = []
161 for c, i in zip(cols, colsi):
162 cn = c[0] if isinstance(c, tuple) else c
163 v = clean_value(row[i])
164 if v:
165 ct = dict(key=cn, value=v)
166 text = apply_template(template_col, ct, engine=engine)
167 content.append(text)
169 # main mail
170 context = common.copy()
171 context["begin"] = begin
172 context["end"] = end
173 context["content"] = "\n".join(content)
174 mail = None
176 # rest of columns add to the context
177 for k, v in zip(group.columns, row):
178 if k == col_mail:
179 mail = v
180 k = common_rev.get(k, k)
181 if k.startswith("col_"):
182 k = k[4:]
183 context[k] = clean_value(v)
185 text = apply_template(template, context, engine=engine)
187 if mail is None or "@" not in mail:
188 if exc:
189 raise ValueError("No mail for:\n" + text)
190 else:
191 fLOG("No mail for:\n" + text)
193 html = ('<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body>\n' +
194 text + "\n</body></html>\n")
195 text = text.replace("<b>", "").replace(
196 "</b>", "").replace("<br />", "\n")
197 yield (mail, html, text)
200def enumerate_send_email(mailbox, subject, fr, df1, cc=None, delay=(1000, 1500),
201 delay_sending=False, exc=True, skip=0, only=None,
202 **params):
203 """
204 Sends feedback to students.
205 Sets mailbox to None to see what the first
206 mail looks like before going through the whole list.
208 @param mailbox mailbox, see `create_smtp_server
209 <http://www.xavierdupre.fr/app/pymmails/helpsphinx/pymmails/sender/
210 email_sender.html?pymmails.sender.email_sender.create_smtp_server>`_,
211 if mailbox is None, the function displays the message and fails
212 @param subject subject
213 @param fr from
214 @param df1 first dataframe
215 @param cc additional receivers
216 @param delay random delay between two mails
217 @param delay_sending returns functions
218 @param exc raise exception when mail is empty
219 @param skip skip the first mails
220 @param only send only to these groups (group id)
221 @param params see @see fn enumerate_feedback
222 @return enumerate mails
224 Short example (see algo :ref:`sphx_glr_automation_send_mails.py`)::
226 import pandas
227 import sys
228 import os
230 cc = ["cc@cc.org"]
231 sujet = "Projet informatique, feedback sur le pitch"
232 only = None # {28, 20, 19}
234 from pyquickhelper.loghelper import fLOG
235 fLOG(OutputPrint=True)
237 from ensae_teaching_cs.automation_students import enumerate_feedback, enumerate_send_email
238 import pymmails
240 df = pandas.read_excel("groupes_eleves_pitch.xlsx", sheet_name=0, engine='openpyxl')
242 mailbox = pymmails.sender.create_smtp_server("gmail", "xavier.dupre", "****")
243 mails = enumerate_send_email(mailbox, sujet, "xavier.dupre@gmail.com",
244 df, exc=True, fLOG=fLOG, delay_sending=False,
245 begin=begin, end=end,
246 cc=cc, only=only, col_group="Groupe",
247 col_name="Nom", col_mail="Mail",
248 cols=["Sujet", "Rapport", "Code", "Soutenance", "Question code",
249 "Note rapport / 5", "Note code / 7", "Note soutenance / 5",
250 "Suivi et Bonus / 3 ou 4", "Bonus raison"])
251 mailbox.close()
253 """
254 loop = 0
255 for mails, html, text in enumerate_feedback(df1, exc=exc, subject=subject, **params):
256 if loop < skip:
257 loop += 1
258 continue
259 if only is not None and loop not in only:
260 loop += 1
261 continue
262 if mails is None or "@" not in mails:
263 # if there is an issue, it should been cautch by the previous
264 # function (we skip)
265 loop += 1
266 continue
267 if not delay_sending and "fLOG" in params:
268 params["fLOG"](loop, "send mail to ", mails)
269 if isinstance(mails, str) and ";" in mails:
270 mails = ";".join(_.strip() for _ in mails.split(";"))
271 if mailbox is None:
272 if "fLOG" not in params:
273 raise KeyError(
274 "fLOG should be send to the function as a parameter, it is used to display the message when mailbox is None")
275 fLOG = params["fLOG"]
276 fLOG("***********************")
277 fLOG(f"fr={fr}")
278 fLOG(f"to={mails}")
279 fLOG(f"cc={cc}")
280 fLOG(f"subject={subject}")
281 fLOG(f"body\n{html}")
282 with open("enumerate_send_email_debug.html", "w", encoding="utf-8") as f:
283 f.write(html)
284 raise ValueError(
285 "mailbox is None, first mail is display and the process is stopped, see enumerate_send_email_debug.html.")
286 res = send_email(mailbox, fr=fr, to=mails.split(";"), cc=cc, delay_sending=delay_sending,
287 body_html=html, body_text=text, subject=subject)
288 if delay_sending:
289 def delay_send(params, loop, mails, res):
290 if "fLOG" in params:
291 params["fLOG"](loop, "send mail to ", mails)
292 res()
293 rnd = random.randint(*delay)
294 time.sleep(rnd / 1000.0)
295 yield lambda params=params, loop=loop, mails=mails, res=res: delay_send(params, loop, mails, res)
296 else:
297 yield res
298 rnd = random.randint(*delay)
299 time.sleep(rnd / 1000.0)
300 loop += 1