#!/usr/bin/env python
# -*- coding: utf-8 -*-
# TOMUSS: The Online Multi User Simple Spreadsheet
# Copyright (C) 2008-2014 Thierry EXCOFFIER, Universite Claude Bernard
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Contact: Thierry.EXCOFFIER@bat710.univ-lyon1.fr
"""Management of the justification for missing courses."""
import os
import time
import cgi
import glob
from . import utilities
from . import configuration
from . import inscrits
from . import document
from . import teacher
js = utilities.js
def a_date(date):
"""Check if the date (DD/MM/YYYY/[MA]) seems fine"""
if date[-1] not in "MA":
raise ValueError("Bad date:" + date)
int_list = [int(v) for v in date[:-1].split('/')]
if len(int_list) != 3:
raise ValueError("Bad date:" + date)
if (int_list[0] >= 1 and int_list[0] <= 31 and
int_list[1] >= 1 and int_list[1] <= 12 and
int_list[2] > 2000):
return '%d/%d/%d%s' % (int_list[0], int_list[1], int_list[2], date[-1])
raise ValueError("Bad date:" + date)
class Abj(object):
"""ABJ for a given student
Functions: add, rem, add_da, rem_da
are stored in the key file of the student (per university year).
The parameters are yet validated.
The same functions beginning with 'store_' do the check and the storage.
"""
def __init__(self, year, semester, login):
year, semester = utilities.university_year_semester(year, semester)
self.login = utilities.the_login(login)
self.filename = os.path.join(self.login, 'abj_%d' % year)
self.abjs = []
self.da = []
f = utilities.manage_key('LOGINS', self.filename)
if f:
locales = self.get_locales()
for line in f.strip().split('\n'):
eval(line.strip(), {}, locales)
def get_locales(self):
return {'add' : self.add,
'add_da': self.add_da,
'rem' : self.rem,
'rem2' : self.rem2,
'rem_da': self.rem_da,
}
def store(self, data):
utilities.manage_key('LOGINS',self.filename, content=data, append=True)
eval(data, {}, self.get_locales())
def add(self, from_date, to_date, author, dummy_date, comment):
self.abjs.append((from_date, to_date, author, comment))
def store_add(self, from_date, to_date, user_name, date, comment):
from_date = a_date(from_date)
to_date = a_date(to_date)
for abj in self.abjs:
if (from_date, to_date) == abj[:2]:
print 'Do no add twice the same ABJ for', self.login
print from_date, to_date, user_name, date, comment
for i in self.abjs:
print '\t', i
return
if not date:
date = time.strftime('%Y%m%d%H%M%S')
self.store('add(%s,%s,%s,%s,%s)\n' % (
repr(from_date), repr(to_date), repr(user_name),
repr(date), repr(comment)))
def add_da(self, ue_code, date, author, dummy_fdate, comment):
self.da.append((ue_code, date, author, comment))
def store_add_da(self, ue_code, date, user_name, fdate, comment):
for i in self.da:
if i[0] == ue_code and i[1] == date:
return
if not fdate:
fdate = time.strftime('%Y%m%d%H%M%S')
self.store('add_da(%s,%s,%s,%s,%s)\n' % (
repr(ue_code), repr(date), repr(user_name),
repr(fdate), repr(comment)))
def rem(self, from_date, to_date, dummy_username, dummy_date):
"""BUGGED: Do not use. It is here to allow reading <2010 files"""
self.abjs = [abj
for abj in self.abjs
if abj[0] != from_date and abj[1] != to_date]
def store_rem(self, from_date, to_date, user_name, date=''):
"""BUGGED: Do not use. It is here to allow reading <2010 files"""
from_date = a_date(from_date)
to_date = a_date(to_date)
if not date:
date = time.strftime('%Y%m%d%H%M%S')
self.store('rem(%s,%s,%s,%s)\n' % (
repr(from_date), repr(to_date), repr(user_name), repr(date)))
def rem2(self, from_date, to_date, dummy_username, dummy_date):
self.abjs = [abj
for abj in self.abjs
if abj[0] != from_date or abj[1] != to_date]
def store_rem2(self, from_date, to_date, user_name, date=''):
from_date = a_date(from_date)
to_date = a_date(to_date)
if not date:
date = time.strftime('%Y%m%d%H%M%S')
self.store('rem2(%s,%s,%s,%s)\n' % (
repr(from_date), repr(to_date), repr(user_name), repr(date)))
def rem_da(self, ue_code, dummy_username, dummy_date):
self.da = [da for da in self.da if da[0] != ue_code]
def store_rem_da(self, ue_code, user_name, date):
if not date:
date = time.strftime('%Y%m%d%H%M%S')
self.store('rem_da(%s,%s,%s)\n' % (
repr(ue_code), repr(user_name), repr(date)))
def js(self):
"""All the student ABJ data as a JavaScript fragment"""
abj_list = []
for from_date, to_date, author, comment in self.abjs:
abj_list.append('[%s,%s,%s,%s]' % (repr(from_date), repr(to_date),
repr(author), js(comment)))
return '[' + ','.join(abj_list) + ']'
def js_da(self):
"""All the student DA data as a JavaScript fragment"""
da_list = []
for ue_code, date, author, comment in self.da:
da_list.append('[%s,%s,%s,%s]' % (repr(ue_code), repr(date),
repr(author), js(comment)))
return '[' + ','.join(da_list) + ']'
def ues_without_da(self):
"""List of the UE_CODE registered by the student, but without DA"""
ues = inscrits.L_fast.ues_of_a_student_short(self.login)
if len(self.da) == 0:
return ues
da = zip(*self.da)[0]
return [ue_code for ue_code in ues if ue_code not in da]
def current_abjs(self, year, semester):
span = configuration.semester_span(year, semester).split(' ')
begin = configuration.date_to_time(span[0])
end = configuration.date_to_time(span[1])
for abj in self.abjs:
abj_begin = configuration.date_to_time(abj[0].rstrip('AM'))
abj_end = configuration.date_to_time(abj[1].rstrip('AM'))
if max(begin, abj_begin) <= min(end, abj_end):
yield abj
def current_das(self, year, semester):
span = configuration.semester_span(year, semester).split(' ')
begin = configuration.date_to_time(span[0])
end = configuration.date_to_time(span[1])
for da in self.da:
date = configuration.date_to_time(da[1])
if begin < date < end:
yield da
class Abjs(object):
"""ABJ for a set of students"""
def __init__(self, year, semester):
self.year, self.semester = utilities.university_year_semester(
year, semester)
# This code is here to translate old data format to the new one.
if configuration.read_only:
return
filename = os.path.join(configuration.db, 'Y'+str(year), 'S'+semester)
utilities.mkpath_safe(filename)
filename = os.path.join(filename, 'abjs.py')
if not os.path.exists(filename):
return
utilities.warn('Start translation from old file format')
locales = {'add' : self.add ,
'add_da': self.add_da,
'rem' : self.rem ,
'rem2' : self.rem ,
'rem_da': self.rem_da,
}
f = open(filename, "r")
for line in f:
if '(' not in line:
continue
eval(line, {}, locales)
f.close()
utilities.unlink_safe(filename)
utilities.warn('Done translation from old file format')
def add(self, login, from_date, to_date, user_name='', date='',comment=''):
Abj(self.year, self.semester, login).store_add(
from_date, to_date, user_name, date, comment)
def add_da(self, login, ue_code, date=None, user_name='', fdate='',
comment=''):
Abj(self.year, self.semester, login).store_add_da(
ue_code, date, user_name, fdate, comment)
def rem(self, login, from_date, to_date, user_name='', date=''):
"""BUGGED: Do not use. It is here to allow reading <2010 files"""
Abj(self.year, self.semester, login).store_rem(
from_date, to_date, user_name, date)
def rem2(self, login, from_date, to_date, user_name='', date=''):
Abj(self.year, self.semester, login).store_rem2(
from_date, to_date, user_name, date)
def rem_da(self, login, ue_code, user_name='', date=''):
Abj(self.year, self.semester, login).store_rem_da(
ue_code, user_name, date)
def students(self):
for filename in glob.glob(
os.path.join(configuration.db, 'LOGINS','*','*',
'abj_%s' % self.year)):
yield filename.split(os.path.sep)[-2]
def add_abjs(year, semester, ticket, student, from_date, to_date, comment):
"""Helper function"""
Abjs(year, semester).add(student, from_date, to_date, ticket.user_name,
comment=comment)
def rem_abjs(year, semester, ticket, student, from_date, to_date):
"""Helper function"""
Abjs(year, semester).rem(student, from_date, to_date,
ticket.user_name)
def add_abjs_da(year, semester, ticket, student, ue_code, date, comment):
"""Helper function"""
Abjs(year, semester).add_da(student, ue_code, date, ticket.user_name,
comment=comment)
def rem_abjs_da(year, semester, ticket, student, ue_code):
"""Helper function"""
Abjs(year, semester).rem_da(student, ue_code, ticket.user_name)
def html_abjs(year, semester, student, full=False):
"""Get all the ABJS/DA informations has HTML"""
return unicode(Abj(year, semester, student).html(year, semester, full),
'utf-8')
def a_student(browser, year, semester, ticket, student):
"""Send student abj with the data to_date the navigator."""
student = inscrits.login_to_student_id(student)
html = """
<link rel="stylesheet" href="%s/style.css" type="text/css">
<body class="abj_body">
<div id="student_display"><IMG src="%s"></div>
""" % (configuration.server_url, configuration.picture(student, ticket))
html += "<A HREF=\"%s/%s\">%s</A>, <small>%s</small><SCRIPT>" % (
configuration.suivi.url(year,semester,ticket.ticket),
student.replace("'","\\'"),
' '.join(inscrits.L_fast.firstname_and_surname(student)).replace("'","\\'"),
', '.join(inscrits.L_fast.portail(student)).replace("'","\\'")
)
abj = Abj(year, semester, student)
html += "document.write(window.parent.display_abjs(%s));" % unicode(
abj.js(), 'utf-8')
html += "document.write(window.parent.display_da(%s));" % unicode(
abj.js_da(), 'utf-8')
ue_list = abj.ues_without_da()
ue_list.sort()
html += "window.parent.ues_without_da(%s);" % js(ue_list) + '</SCRIPT>'
browser.write(html.encode('utf8'))
def tierstemps(student_id, table_tt=None, only_current=True):
"""Returns a strings containing all tiers-temps informations about
the student from the TT table given."""
student_id = inscrits.login_to_student_id(student_id)
if table_tt == None:
# Get TT for current year
table_tt = get_table_tt(*configuration.year_semester)
_ = utilities.__
tt = table_tt.the_current_tt(table_tt).get(student_id, None)
if tt and (not only_current or tt.current()):
html = ""
if tt.begin:
html += _("MSG_abj_tt_from") + tt.begin + '\n'
if tt.end:
html += _('TH_until') + tt.end + '\n'
if tt.written_exam:
html += _("COL_COMMENT_+write") + " : %s.\n" % tt.written_exam
if tt.spoken_exam:
html += _("COL_COMMENT_+speech") + " : %s.\n" % tt.spoken_exam
if tt.practical_exam:
html += _("COL_COMMENT_+practical") + " : %s.\n" % tt.practical_exam
if tt.assistant:
html += _("COL_COMMENT_+assistant") + ".\n"
if tt.room:
html += _("COL_COMMENT_+room") + ".\n"
if tt.remarks:
html += tt.remarks + '.\n'
return html
return ''
def title(name, sort_fct):
"""Columns title with the link to sort the HTML table"""
return (name +
' <span class="x">(<a href="javascript:sort(' +
sort_fct +
')">Trier</a>)</span>')
def alpha_html(browser, year, semester, ue_name_endswith=None,
ue_name_startswith=None, author=None):
"""Returns ABJ/DA information for all the students in HTML format"""
_ = utilities._
browser.write('''<html>
<head>
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
</head>
<body>
<div id="x">%s</div>
<table style="table-layout: fixed" border><tbody id="t">
''' % _("MSG_suivi_student_wait") )
line = '<tr>' + '<td>%s</td>' * 8 + '</tr>\n'
browser.write(line % (
_("COL_TITLE_0_1"),
title(_("COL_TITLE_0_2"), 'cmp_name'),
title(_("COL_TITLE_0_0"), 'cmp_id'),
_("TH_what"),
title(_("B_Date"), 'cmp_ue'),
title(_("TH_end_or_ue"), 'cmp_ue2'),
title(_("TH_comment"), 'cmp_comment'),
title(_("TH_abj_author"), 'cmp_comment'),
))
for login in Abjs(year, semester).students():
if ue_name_endswith:
for ue_code in inscrits.L_batch.ues_of_a_student_short(login):
if ue_code.endswith(ue_name_endswith):
break
else:
# No UE ended by the required character
continue
if ue_name_startswith:
for ue_code in inscrits.L_batch.ues_of_a_student_short(login):
if ue_code.startswith(ue_name_startswith):
break
else:
# No UE start by the required character
continue
def write(a, b, c, d, e):
fn, sn = inscrits.L_slow.firstname_and_surname(login)
fn = fn.encode('utf8')
sn = sn.encode('utf8')
browser.write( line % (fn, sn, login, a, b, c ,d, e))
student = Abj(year, semester, login)
for from_date, to_date, author2, comment in student.abjs:
if author is None or author == author2:
write('ABJ', from_date, to_date, cgi.escape(comment), author2)
for ue_code, date, author2, comment in student.da:
if author is None or author == author2:
write('DAS', date, ue_code, cgi.escape(comment), author2)
browser.write('</tbody></table>'
+ '<script>abj_messages = [%s,%s,%s,%s] ; </script>' % (
utilities.js(utilities._("MSG_abj_choose_action")),
utilities.js(utilities._("MSG_abj_hide_abj")),
utilities.js(utilities._("MSG_abj_hide_da")),
utilities.js(utilities._("MSG_abj_display_all")))
+ '</script>')
browser.write(utilities.read_file(os.path.join('FILES', 'abj_recap.html')))
def underline(txt, char='='):
"""Returns the text preceded with a line of '=' of same length"""
return '\n' + char * len(txt) + '\n' + txt + '\n' + char * len(txt) + '\n'
def nice_date(date):
"""Returns a DD/MM/YYYY/[AM] nicely formatted"""
return date.replace('M',' ' + configuration.ampms_full[0]
).replace('A', ' ' + configuration.ampms_full[1])
def get_table_tt(year, semester):
"""Returns the current database for TT"""
return document.table(utilities.university_year(year, semester),
'Dossiers', 'tt')
def feedback(browser, letter, nr_letters):
"""This function displays a feedback in the browser while
loading students informations"""
if browser:
nr_letters += 1
if nr_letters % 80 == 0:
letter += '\n'
browser.write(letter)
browser.flush()
return nr_letters
#REDEFINE
# When a student has an ABJ it may be outside of UE schedule.
# The function returns the ABJs without ABJ outside of UE planning.
def prune_abjs(abj_list, group, sequence, ue_code):
"""Trim unecessary ABJS"""
return abj_list
def do_prune(abj_list, first_day, last_day, group, sequence, ue_code):
"""Trim unecessary ABJS using first and last day of the course."""
abjs_pruned = []
messages = []
for abjj in abj_list:
try:
seconds = configuration.date_to_time(abjj[0][:-1])
except OverflowError:
seconds = 0
if seconds >= last_day:
continue
try:
seconds = configuration.date_to_time(abjj[1][:-1])
except OverflowError:
seconds = 8000000000
if seconds < first_day:
continue
if abjj[3].startswith('{{{MESSAGE}}}'):
messages.append(abjj)
continue
abjs_pruned.append(abjj)
return prune_abjs(abjs_pruned, group, sequence, ue_code) + messages
def ue_mails_and_comments(ue_code):
"""Returns the mails of the master of the UE and a comment
about the origin of the mail address"""
mails = []
text = []
the_ue = teacher.all_ues().get(ue_code[3:], None)
if the_ue:
for teacher_login in the_ue.responsables_login():
fn, sn, mail = inscrits.L_slow.firstname_and_surname_and_mail(
teacher_login)
text.append(" * " + fn.title() + ' ' + sn.upper() + ' : ')
if mail == None:
text.append(utilities._("MSG_abj_unknown_mail"))
else:
mails.append(mail.lower())
text.append(mail)
text.append('\n')
# Add other mails
other_mails = []
for an_other_mail in teacher.other_mails(ue_code[3:]):
an_other_mail = an_other_mail.lower().encode('utf-8')
if an_other_mail not in mails:
other_mails.append(an_other_mail)
if other_mails:
text.append(" " + utilities._("MSG_other_mail") +
', '.join(other_mails) + '\n')
mails += other_mails
# Add other mails from SPIRAL
if the_ue:
other_mails = []
for an_other_mail in the_ue.mails():
an_other_mail = an_other_mail.lower()
if an_other_mail not in mails:
other_mails.append(unicode(an_other_mail, 'utf-8'))
if other_mails:
text.append(" " + utilities._("MSG_other_mail2") +
', '.join(other_mails) + '\n')
mails += other_mails
if len(mails) == 0:
text.append(utilities._("MSG_no_master") + '\n')
return mails, text
def ue_resume(ue_code, year, semester, browser=None):
"""Returns all the ABJ/DA/TT informations about the all the UE students"""
nr_letters = 0
table_tt = get_table_tt(year, semester)
#
# The UE title
#
the_ue = teacher.all_ues().get(ue_code[3:], None)
if browser:
text = []
browser.write(utilities._("MSG_suivi_student_wait") + '\n')
else:
text = [utilities.__("MSG_abj_mail_header") % configuration.server_url
+ '\n\n']
if not the_ue:
text.append(underline(ue_code + utilities.__("MSG_abj_no_title")))
else:
text.append(underline(ue_code + ' : ' + the_ue.intitule()))
text.append('\n' + utilities.__("MSG_abj_link") + '\n\n')
text.append(' %s/%s/%s/%s/resume\n' % (
configuration.server_url, year, semester, ue_code))
#
# The UE managers
#
text.append(underline(utilities.__("MSG_abj_master"), char='-'))
mails, infos = ue_mails_and_comments(ue_code)
text += infos
the_students = []
first_day = 0
last_day = 8000000000
if len(the_students) == 0:
# Fast process but may be incomplete
table_ue = document.table(year, semester, ue_code, create=False)
if table_ue:
the_students = [(line[0].value, line[3].value, line[4].value)
for line in table_ue.lines.values()
if line[0].value]
first_day = table_ue.dates[0]
last_day = table_ue.dates[1] + 86400 # End of last day
table_ue.unload()
if first_day == 0:
ss = configuration.semester_span(year, semester)
if ss:
fd, ld = ss.split(' ')
first_day = configuration.date_to_time(fd)
last_day = configuration.date_to_time(ld)
utilities.warn('first_day=%s %s' % (first_day, last_day))
#
# The ABJ
#
first = True
infos = []
for student_login, group, sequence in the_students:
student_id = inscrits.login_to_student_id(student_login)
student = Abj(year, semester, student_id)
abjs_pruned = do_prune(student.abjs, first_day, last_day,
group, sequence, ue_code)
if len(abjs_pruned) == 0:
continue
nr_letters = feedback(browser, 'A', nr_letters)
if first:
first = False
text.append(underline(utilities.__("TH_ABJ_list"), char='-'))
fs = inscrits.L_slow.firstname_and_surname(student.login)
the_abjs = (' * ' + student.login + ' ' + fs[1].upper() + ' ' +
fs[0].title() + '\n')
for abj in abjs_pruned:
if abj[0] == abj[1]:
an_abj = ' - ' + utilities.__("MSG_abj_the") \
+ ' ' + nice_date(abj[0])
elif abj[0][:-1] == abj[1][:-1]:
an_abj = ' - ' + utilities.__("MSG_abj_the") \
+ ' ' + abj[0][:-1]
else:
an_abj = " - " + utilities.__("MSG_abjtt_from_before") \
+ " " + nice_date(abj[0]) + ' ' \
+ utilities.__("TH_until") + ' ' + nice_date(abj[1])
if abj[3]:
an_abj += ' (' + unicode(abj[3],'utf8') + ')'
the_abjs += an_abj + '\n'
infos.append( (utilities.flat(fs[1]).lower(),
utilities.flat(fs[0]).lower(),
the_abjs) )
if infos:
infos.sort()
text += zip(*infos)[2]
#
# The DA
#
first = True
infos = []
for student_login, group, sequence in the_students:
student_id = inscrits.login_to_student_id(student_login)
student = Abj(year, semester, student_id)
dates = [d for d in student.da if d[0] == ue_code]
if dates:
nr_letters = feedback(browser, 'D', nr_letters)
if first:
first = False
text.append(underline(utilities.__("MSG_abj_da_list"),
char='-'))
fs = inscrits.L_slow.firstname_and_surname(student_login)
an_abj = (' * ' + student_login + ' '
+ fs[1].upper() + ' ' + fs[0].title()
+ ' ' + utilities.__("MSG_abj_tt_from") + dates[0][1])
if dates[0][3]:
an_abj += ' (' + unicode(dates[0][3],'utf-8') + ')'
an_abj += '\n'
infos.append((utilities.flat(fs[1]).lower(),
utilities.flat(fs[0]).lower(),
an_abj))
if infos:
infos.sort()
text += zip(*infos)[2]
#
# The TT
#
first = True
tt_logins = list(table_tt.logins())
infos = []
for student_login, group, sequence in the_students:
infos_tt = tierstemps(student_login, table_tt = table_tt)
if not infos_tt:
continue
nr_letters = feedback(browser, 'T', nr_letters)
if first:
first = False
text.append(underline(utilities.__("MSG_abj_tt_list"),
char='-'))
fs = inscrits.L_slow.firstname_and_surname(student_login)
a_tt = (' * ' + student_login + ' ' + fs[1].upper() + ' '
+ fs[0].title() + '\n')
for line in infos_tt.split('\n')[:-1]:
a_tt += ' - ' + line + '\n'
infos.append((utilities.flat(fs[1]).lower(),
utilities.flat(fs[0]).lower(),
a_tt))
if infos:
infos.sort()
text += zip(*infos)[2]
return text, mails
to_send_ok = [] # (Recipent, Title, message) list
def list_mail(browser, year, semester, only_licence=True):
"""Compute and display for all the UE the 'ue_resume'.
Store the result in order to send all the mails after human check"""
to_send = []
sender = configuration.abj_sender
browser.write(utilities._("MSG_abj_sender") + sender)
browser.write('<pre>')
ues = {}
for login in Abjs(year, semester).students():
student = Abj(year, semester, login)
for a_da in student.da:
ues[a_da[0]] = True
if student.abjs:
for ue_code in inscrits.L_batch.ues_of_a_student_short(student.login):
ues[ue_code] = True
table_tt = get_table_tt(year, semester)
tt_logins = list(table_tt.logins())
for student_login in tt_logins:
for ue_code in inscrits.L_batch.ues_of_a_student_short(student_login):
ues[ue_code] = True
if only_licence:
ues = [ue_code for ue_code in ues if ue_code[-1] == 'L']
for ue_code in ues:
lines, mails = ue_resume(ue_code, year, semester)
browser.write(''.join(lines).encode('utf8'))
if mails:
to_send.append( (mails,
'ABJ + DA + TT pour l\'UE ' + ue_code,
''.join(lines)) )
global to_send_ok
to_send_ok = to_send
browser.write('</pre>' + utilities._("MSG_abj_send") % len(to_send))
browser.close()
to_send_ok_example = [
(['exco@bat710.univ-lyon1.fr', 'thierry.excoffier@bat710.univ-lyon1.fr'],
'UE_CODE x',
'blabla'),
(['exco@liris.univ-lyon1.fr', u'excé@www710.univ-lyon1.fr'],
'UE_CODE y',
'blaBLA'),
(['exco@bat710.univ-lyon1.fr', 'thierry.excoffier@bat710.univ-lyon1.fr'],
'UE_CODE z',
'blabla'),
]
def send_mail(browser):
"""Send all the mails generated by 'list_mail'"""
for recipients, mail_title, text in to_send_ok:
try:
error = utilities.send_mail(recipients,
mail_title,
text,
configuration.abj_sender)
if error:
browser.write("<pre>" + error + "</pre>")
else:
browser.write("<p>" + repr(recipients)
+ utilities._("MSG_abj_sent"))
except UnicodeEncodeError:
browser.write("<pre>BUG dans abj.send_mail</pre>")