Commits

martin.v.loewis  committed a059141 Draft

Add our standard extensions and detectors.

  • Participants
  • Parent commits ac0b15b

Comments (0)

Files changed (6)

File detectors/config.ini.template

+#This configuration file controls the behavior of busybody.py and tellteam.py
+#The two definitions can be comma-delimited lists of email addresses.
+#Be sure these addresses will accept mail from the tracker's email address.
+[main]
+triage_email =  triage@example.com
+busybody_email= busybody@example.com
+
+# URI to XMLRPC server doing the actual spam check.
+spambayes_uri = http://www.webfast.com:80/sbrpc
+# These must match the {ham,spam}_cutoff setting in the SpamBayes server
+# config.
+spambayes_ham_cutoff = 0.2
+spambayes_spam_cutoff = 0.85
+
+spambayes_may_view_spam = User,Coordinator,Developer
+spambayes_may_classify = Coordinator
+spambayes_may_report_misclassified = User,Coordinator,Developer

File detectors/sendmail.py

+from roundup import roundupdb
+
+def determineNewMessages(cl, nodeid, oldvalues):
+    ''' Figure a list of the messages that are being added to the given
+        node in this transaction.
+    '''
+    messages = []
+    if oldvalues is None:
+        # the action was a create, so use all the messages in the create
+        messages = cl.get(nodeid, 'messages')
+    elif oldvalues.has_key('messages'):
+        # the action was a set (so adding new messages to an existing issue)
+        m = {}
+        for msgid in oldvalues['messages']:
+            m[msgid] = 1
+        messages = []
+        # figure which of the messages now on the issue weren't there before
+        for msgid in cl.get(nodeid, 'messages'):
+            if not m.has_key(msgid):
+                messages.append(msgid)
+    return messages
+
+
+def is_spam(db, msgid):
+    """Return true if message has a spambayes score above
+    db.config.detectors['SPAMBAYES_SPAM_CUTOFF']. Also return true if
+    msgid is None, which happens when there are no messages (i.e., a
+    property-only change)"""
+    if not msgid:
+        return False
+    cutoff_score = float(db.config.detectors['SPAMBAYES_SPAM_CUTOFF'])    
+
+    msg = db.getnode("msg", msgid)
+    if msg.has_key('spambayes_score') and \
+           msg['spambayes_score'] > cutoff_score:
+        return True
+    return False
+
+
+def sendmail(db, cl, nodeid, oldvalues):
+    """Send mail to various recipients, when changes occur:
+
+    * For all changes (property-only, or with new message), send mail
+      to all e-mail addresses defined in
+      db.config.detectors['BUSYBODY_EMAIL']
+
+    * For all changes (property-only, or with new message), send mail
+      to all members of the nosy list.
+
+    * For new issues, and only for new issue, send mail to
+      db.config.detectors['TRIAGE_EMAIL']
+
+    """
+
+    sendto = []
+
+    # The busybody addresses always get mail.
+    try:
+        sendto += db.config.detectors['BUSYBODY_EMAIL'].split(",")
+    except KeyError:
+        pass
+
+    # New submission? 
+    if None == oldvalues:
+        changenote = cl.generateCreateNote(nodeid)
+        try:
+            # Add triage addresses
+            sendto += db.config.detectors['TRIAGE_EMAIL'].split(",")
+        except KeyError:
+            pass
+        oldfiles = []
+    else:
+        changenote = cl.generateChangeNote(nodeid, oldvalues)
+        oldfiles = oldvalues.get('files', [])        
+
+    newfiles = db.issue.get(nodeid, 'files', [])
+    if oldfiles != newfiles:
+        added = [fid for fid in newfiles if fid not in oldfiles]
+        removed = [fid for fid in oldfiles if fid not in newfiles]
+        filemsg = ""
+
+        for fid in added:
+            url = db.config.TRACKER_WEB + "file%s/%s" % \
+                  (fid, db.file.get(fid, "name"))
+            changenote+="\nAdded file: %s" % url
+        for fid in removed:
+            url = db.config.TRACKER_WEB + "file%s/%s" % \
+                  (fid, db.file.get(fid, "name"))            
+            changenote+="\nRemoved file: %s" % url
+
+
+    authid = db.getuid()
+
+    new_messages = determineNewMessages(cl, nodeid, oldvalues)
+
+    # Make sure we send a nosy mail even for property-only
+    # changes. 
+    if not new_messages:
+        new_messages = [None]
+
+    for msgid in [msgid for msgid in new_messages if not is_spam(db, msgid)]:
+        try:
+            cl.send_message(nodeid, msgid, changenote, sendto,
+                            authid=authid)
+            nosymessage(db, nodeid, msgid, oldvalues, changenote)
+        except roundupdb.MessageSendError, message:
+            raise roundupdb.DetectorError, message
+
+def nosymessage(db, nodeid, msgid, oldvalues, note,
+                whichnosy='nosy',
+                from_address=None, cc=[], bcc=[]):
+    """Send a message to the members of an issue's nosy list.
+
+    The message is sent only to users on the nosy list who are not
+    already on the "recipients" list for the message.
+
+    These users are then added to the message's "recipients" list.
+
+    If 'msgid' is None, the message gets sent only to the nosy
+    list, and it's called a 'System Message'.
+
+    The "cc" argument indicates additional recipients to send the
+    message to that may not be specified in the message's recipients
+    list.
+
+    The "bcc" argument also indicates additional recipients to send the
+    message to that may not be specified in the message's recipients
+    list. These recipients will not be included in the To: or Cc:
+    address lists.
+    """
+    if msgid:
+        authid = db.msg.get(msgid, 'author')
+        recipients = db.msg.get(msgid, 'recipients', [])
+    else:
+        # "system message"
+        authid = None
+        recipients = []
+
+    sendto = []
+    bcc_sendto = []
+    seen_message = {}
+    for recipient in recipients:
+        seen_message[recipient] = 1
+
+    def add_recipient(userid, to):
+        # make sure they have an address
+        address = db.user.get(userid, 'address')
+        if address:
+            to.append(address)
+            recipients.append(userid)
+
+    def good_recipient(userid):
+        # Make sure we don't send mail to either the anonymous
+        # user or a user who has already seen the message.
+        return (userid and
+                (db.user.get(userid, 'username') != 'anonymous') and
+                not seen_message.has_key(userid))
+
+    # possibly send the message to the author, as long as they aren't
+    # anonymous
+    if (good_recipient(authid) and
+        (db.config.MESSAGES_TO_AUTHOR == 'yes' or
+         (db.config.MESSAGES_TO_AUTHOR == 'new' and not oldvalues))):
+        add_recipient(authid, sendto)
+
+    if authid:
+        seen_message[authid] = 1
+
+    # now deal with the nosy and cc people who weren't recipients.
+    for userid in cc + db.issue.get(nodeid, whichnosy):
+        if good_recipient(userid):
+            add_recipient(userid, sendto)
+
+    # now deal with bcc people.
+    for userid in bcc:
+        if good_recipient(userid):
+            add_recipient(userid, bcc_sendto)
+
+    # If we have new recipients, update the message's recipients
+    # and send the mail.
+    if sendto or bcc_sendto:
+        if msgid is not None:
+            db.msg.set(msgid, recipients=recipients)
+        db.issue.send_message(nodeid, msgid, note, sendto, from_address,
+                              bcc_sendto)
+
+
+def init(db):
+    db.issue.react('set', sendmail)
+    db.issue.react('create', sendmail)

File extensions/timestamp.py

+import time, struct, base64
+from roundup.cgi.actions import RegisterAction
+from roundup.cgi.exceptions import *
+
+def timestamp():
+    return base64.encodestring(struct.pack("i", time.time())).strip()
+
+def unpack_timestamp(s):
+    return struct.unpack("i",base64.decodestring(s))[0]
+
+class Timestamped:
+    def check(self):
+        try:
+            created = unpack_timestamp(self.form['opaque'].value)
+        except KeyError:
+            raise FormError, "somebody tampered with the form"
+        if time.time() - created < 4:
+            raise FormError, "responding to the form too quickly"
+        return True
+
+class TimestampedRegister(Timestamped, RegisterAction):
+    def permission(self):
+        self.check()
+        RegisterAction.permission(self)
+
+def init(instance):
+    instance.registerUtil('timestamp', timestamp)
+    instance.registerAction('register', TimestampedRegister)

File extensions/timezone.py

+# Utility for replacing the simple input field for the timezone with
+# a select-field that lists the available values.
+
+import cgi
+
+try:
+    import pytz
+except ImportError:
+    pytz = None
+
+
+def tzfield(prop, name, default):
+    if pytz:
+        value = prop.plain()        
+        if '' == value:
+            value = default
+        else:
+            try:
+                value = "Etc/GMT%+d" % int(value)
+            except ValueError:
+                pass
+
+        l = ['<select name="%s"' % name]
+        for zone in pytz.all_timezones:
+            s = ' '
+            if zone == value:
+                s = 'selected=selected '
+            z = cgi.escape(zone)
+            l.append('<option %svalue="%s">%s</option>' % (s, z, z))
+        l.append('</select>')
+        return '\n'.join(l)
+        
+    else:
+        return prop.field()
+
+def init(instance):
+    instance.registerUtil('tzfield', tzfield)

File html/user.item.html

  <tr tal:condition="python:edit_ok or context.timezone"
      tal:define="name string:timezone; label string:Timezone; value context/timezone">
   <th metal:use-macro="th_label">Timezone</th>
-  <td><input name="timezone" metal:use-macro="normal_input">
-   <tal:block tal:condition="edit_ok" i18n:translate="">(this is a numeric hour offset, the default is
-    <span tal:replace="db/config/DEFAULT_TIMEZONE" i18n:name="zone"
-    />)</tal:block>
+  <td><input tal:replace="structure python:
+	  utils.tzfield(context.timezone, 'timezone', db.config.DEFAULT_TIMEZONE)"/>
   </td>
  </tr>
 

File html/user.register.html

 <form method="POST" onSubmit="return submit_once()"
       enctype="multipart/form-data"
       tal:attributes="action context/designator">
-
+<input type="hidden" name="opaque" tal:attributes="value python: utils.timestamp()" />
 <table class="form">
  <tr>
   <th i18n:translate="">Name</th>