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-01-27 05:44 +0100

1# -*- coding: utf-8 -*- 

2""" 

3@file 

4@brief Some automation helpers to grab mails from students about projects. 

5""" 

6 

7import time 

8import random 

9import numpy 

10from pyquickhelper.loghelper import noLOG 

11from pyquickhelper.texthelper.templating import apply_template 

12from pymmails.sender import send_email 

13 

14 

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""" 

22 

23template_mail_columns = "<p><b>{{ key }}</b></p><p>{{ value }}</p>\n" 

24 

25 

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. 

35 

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)* 

52 

53 Example of dataframe containing feedback: 

54 

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 +------+-----------+--------+---------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------+ 

72 

73 """ 

74 

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.") 

82 

83 def sums(spl): 

84 spl = [_ for _ in spl if isinstance(_, str) if "??" not in _] 

85 return ";".join(spl) 

86 

87 def sums2(spl): 

88 spl = [_ for _ in spl if isinstance(_, str)] 

89 return ", ".join(spl) 

90 

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) 

118 

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() 

129 

130 begin = begin.replace("\n", "<br />\n") 

131 end = end.replace("\n", "<br />\n") 

132 

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, " ") 

142 

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] 

157 

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) 

168 

169 # main mail 

170 context = common.copy() 

171 context["begin"] = begin 

172 context["end"] = end 

173 context["content"] = "\n".join(content) 

174 mail = None 

175 

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) 

184 

185 text = apply_template(template, context, engine=engine) 

186 

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) 

192 

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) 

198 

199 

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. 

207 

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 

223 

224 Short example (see algo :ref:`sphx_glr_automation_send_mails.py`):: 

225 

226 import pandas 

227 import sys 

228 import os 

229 

230 cc = ["cc@cc.org"] 

231 sujet = "Projet informatique, feedback sur le pitch" 

232 only = None # {28, 20, 19} 

233 

234 from pyquickhelper.loghelper import fLOG 

235 fLOG(OutputPrint=True) 

236 

237 from ensae_teaching_cs.automation_students import enumerate_feedback, enumerate_send_email 

238 import pymmails 

239 

240 df = pandas.read_excel("groupes_eleves_pitch.xlsx", sheet_name=0, engine='openpyxl') 

241 

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() 

252 

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