voting/bin/get_renewees.py

299 lines
12 KiB
Python
Executable file

#!/usr/bin/env python
"""
This program gets the members from the MySQL Database that need renewal.
By starting with --mode=day you can select only those members that need to
renew today.
With --mode=year you get the members that need to renew this year, i.e.
whole 2011.
One can send email, too. Use the --send-mail switch to send email.
If you want to test your setup, you can use --one-only=your@address.com to test
whether email actually works or what From address is used, which is
configurable with --from-address=other@address.com.
In order to work properly, you need to have your MySQL client configured
properly, i.e. ~/.my.cnf should be set up with the appropriate
credentials:
[client]
host=button-back
user=anonvoting
password=foobar
default-character-set=utf8
The reason to call MySQL client and not use the Python library is
mainly, that the MySQL bindings are not installed.
"""
import datetime
try:
from email.mime.text import MIMEText
from email.mime.nonmultipart import MIMENonMultipart
from email.charset import Charset
except ImportError:
from email.MIMEText import MIMEText
from email.Charset import Charset
from email.MIMENonMultipart import MIMENonMultipart
import logging
from optparse import OptionParser
import smtplib
import StringIO
import subprocess
import sys
import tempfile
__author__ = "Tobias Mueller"
__copyright__ = "Copyright 2011, The GNOME Project"
__credits__ = ["Tobias Mueller",]
__license__ = "GPLv3+"
__version__ = "1.0.0"
__maintainer__ = "Tobias Mueller"
__email__ = "tobiasmue@gnome.org"
TEMPLATE = '''
Dear %(firstname)s,
the last time, your GNOME Foundation Membership was renewed, was on
%(last_renewed_on)s.
The term of a membership is two years.
If you want to continue being a member, you have to renew your
membership.
Please see http://foundation.gnome.org/membership/ for details.
Thanks,
The GNOME Membership and Elections Committee
'''.strip()
log = logging.getLogger()
class MTText(MIMEText):
def __init__(self, _text, _subtype='plain', _charset='utf-8'):
if not isinstance(_charset, Charset):
_charset = Charset(_charset)
if isinstance(_text,unicode):
_text = _text.encode(_charset.input_charset)
MIMENonMultipart.__init__(self, 'text', _subtype,
**{'charset': _charset.input_charset})
self.set_payload(_text, _charset)
def send_email(to, subject, emailtext, from_address = None, smtp_server = 'localhost'):
log = logging.getLogger('eMail')
s = None
from_address = from_address or "GNOME Membership and Elections Committee <membership-committee@gnome.org>"
msg = MTText(emailtext)
msg['To'] = to
msg['From'] = from_address
msg['Subject'] = subject
msgstr = msg.as_string()
if s is None:
s = smtplib.SMTP()
s.connect(smtp_server)
try:
log.info('Trying to send to %s, %s', to, subject)
s.sendmail(from_address, [to,], msgstr)
except smtplib.SMTPException,e :
log.warning("Error: Could not send to %s!" % (to,))
raise e
#if s:
# s.quit()
class Member(object):
def __init__(self, firstname, lastname, email, token_or_last_renewed_on):
self.firstname = firstname
self.lastname = lastname
self.email = email
self.token_or_last_renewed_on = token_or_last_renewed_on
@classmethod
def from_csv(cls, csvstring):
firstname, lastname, email, token_or_last_renewed_on = csvstring.strip().split(';')
return Member(firstname, lastname, email, token_or_last_renewed_on)
def __str__(self):
if False: # string.format is too recent Python
fmt = "{firstname} {lastname} <{email}> (token_or_last_renewed_on)"
return fmt.format(self)
fmt = "%(firstname)s %(lastname)s <%(email)s> (%(token_or_last_renewed_on)s)"
return fmt % self.__dict__
def __repr__(self):
fmt = "<Member <%(email)s> (%(token_or_last_renewed_on)s)>"
return fmt % self.__dict__
def execute_query(query):
log.debug('MySQL Query: %s', query)
DATABASE = "foundation"
mysql_p = subprocess.Popen(['mysql', DATABASE], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
SQL_result, SQL_error = mysql_p.communicate(query)
if SQL_error:
sys.stderr.write('Error Executing SQL: %s' % SQL_error)
if not SQL_result:
log.info('NULL Result from SQL')
infile = StringIO.StringIO("")
else:
infile = StringIO.StringIO(SQL_result)
_ = infile.next() # The first line is garbage, I think it's MySQL output
return infile
def get_members_which_need_renewal(mode):
"""
Generates and executes a SQL Query which asks the foundation's database
for members that need to renew.
The base date is calculated from the mode argument:
election: Last years 07-01 until this years 07-01
year: Beginning of the year, i.e. 01.01. until 31.12.
month: From 1st of this month to end of this month
day: Members that need to renew this day
"""
ELECTION_FROM_DATE = """CONCAT(
YEAR(
DATE_SUB(CURDATE(), INTERVAL 1 YEAR)
),
'-07-01')
""".strip()# Last years July, 1st
ELECTION_TO_DATE = """CONCAT(YEAR(CURDATE()), '-07-01')""" # This years July, 1st
DAY_FROM_DATE = """DATE_FORMAT(NOW(),"%Y-%m-%d")"""
DAY_TO_DATE = DAY_FROM_DATE
MONTH_FROM_DATE = """DATE_FORMAT(NOW(),"%Y-%m-01")"""
MONTH_TO_DATE = """LAST_DAY(NOW())"""
YEAR_FROM_DATE = """DATE_FORMAT(NOW(),"%Y-01-01")"""
YEAR_TO_DATE = """DATE_FORMAT(NOW(),"%Y-12-31")"""
(mysql_from_date, mysql_to_date) = {
'election': (ELECTION_FROM_DATE, ELECTION_TO_DATE),
'day': (DAY_FROM_DATE,DAY_TO_DATE),
'month': (MONTH_FROM_DATE, MONTH_TO_DATE),
'year': (YEAR_FROM_DATE, YEAR_TO_DATE),
}[mode]
QUERY = '''
SET names 'utf8';
SELECT CONCAT(firstname, ';', lastname, ';', email, ';', last_renewed_on)
FROM foundationmembers
WHERE last_renewed_on >= DATE_SUB(
%(mysql_from_date)s ,
INTERVAL 2 YEAR
)
AND last_renewed_on <= DATE_SUB(
%(mysql_to_date)s ,
INTERVAL 2 YEAR
)
ORDER BY lastname;
'''.strip()
QUERY %= {'mysql_from_date': mysql_from_date,
'mysql_to_date': mysql_to_date}
infile = execute_query(QUERY)
memberlist = [Member.from_csv(line.strip()) for line in infile]
return memberlist
def get_members_election_token(election_id):
'''
Execute a SQL query to get a list of members with their temporary election
token.
The token need to be created first, i.e. using smth like
INSERT INTO election_tmp_tokens (election_id, member_id, tmp_token)
SELECT @election_id, id, SUBSTRING(MD5(RAND()) FROM 1 FOR 24) AS tmp_token
FROM electorate;
'''
QUERY = '''
SET names 'utf8';
SELECT CONCAT(firstname, ';', lastname, ';', email, ';', tmp_token)
FROM election_tmp_tokens, foundationmembers
WHERE election_id = %(election_id)s
AND member_id = id;
'''.strip()
QUERY %= {'election_id': election_id}
result = execute_query(QUERY)
memberlist = [Member.from_csv(line.strip()) for line in result]
return memberlist
if __name__ == "__main__":
parser = OptionParser()
parser.add_option("-l", "--loglevel", dest="loglevel",
help="Sets the loglevel to one of debug, info, warn, "
"error, critical", default="info")
parser.add_option("-m", "--mode", dest="mode",
help="Which members to get: one of year, month, day, election"
" [default: %default]", default="month")
parser.add_option("-s", "--send-mail", dest="sendmail",
help="Do indeed send mail [default: %default]",
action="store_true",
default=False)
parser.add_option("-t", "--token", dest="token_for_election",
help="Process temporary token, instead of renewals."
"Please give the election id, which you should know, as "
"argument to this parameter. A program argument is also required"
" to read the instructions as a template."
"[default: %default]",
default=False)
parser.add_option("-1", "--one-only", dest="oneonlyaddress",
help="Send one mail only to this address [default: %default]",
default=None)
parser.add_option("-f", "--from-address", dest="fromaddress",
help="Use that as sending address [default: %default]",
default="Tobias Mueller <tobiasmue@gnome.org>")
(options, args) = parser.parse_args()
loglevel = {'debug': logging.DEBUG, 'info': logging.INFO,
'warn': logging.WARN, 'error': logging.ERROR,
'critical': logging.CRITICAL}.get(options.loglevel, "warn")
LOGFORMAT = "%(asctime)s %(levelname)-8s %(name)s %(message)s"
DATEFORMAT = '%Y-%m-%d %H:%M:%S'
logging.basicConfig(level=loglevel, format=LOGFORMAT, datefmt=DATEFORMAT)
log = logging.getLogger('main')
if not options.token_for_election: # This is the default. We care about renewals
email_subject = 'Your GNOME Foundation Membership is about to expire'
logmsg = 'Needs Renewal: %s, %s, %s, %s'
template = TEMPLATE
members = get_members_which_need_renewal(options.mode)
else: # This is with -t option. We process election tokens.
email_subject = 'GNOME Foundation Board of Directors Elections %d - Voting Instructions' % datetime.date.today().year
logmsg = 'Sending Token: %s, %s, %s, %s'
template = open(args[0], "r").read()
members = get_members_election_token(options.token_for_election)
if options.oneonlyaddress:
members = [Member("firstname", "lastname",
options.oneonlyaddress, "2000-05-23")]
for member in members:
firstname, lastname, email, token_or_last_renewed_on = member.firstname, member.lastname, member.email, member.token_or_last_renewed_on
log.warn(logmsg, lastname, firstname, email, token_or_last_renewed_on)
emailtext = template % {'firstname':firstname, 'lastname':lastname,
'email': email, 'token_or_last_renewed_on': token_or_last_renewed_on,
'token': token_or_last_renewed_on,
'last_renewed_on': token_or_last_renewed_on,
}
log.debug('The email to be sent is: %s', emailtext)
to = email
fromaddress = options.fromaddress
subject = email_subject
try:
if options.sendmail or options.oneonlyaddress:
send_email(to, subject, emailtext, fromaddress)
except smtplib.SMTPException,e :
log.error('Error sending to %s: %s', to, e)
try:
tfile = tempfile.NamedTemporaryFile(delete=False)
except TypeError:
tfile = tempfile.NamedTemporaryFile()
tfile.writeline('To: %s' % to)
tfile.writeline('Subject: %s' % subject)
tfile.writeline('')
tfile.writeline('%s' % emailtext)
log.critical('eMail put in %s', tfile.name)