Code

Small readability improvements.
[roundup.git] / roundup / roundupdb.py
index a45ec16ac1748360388d003881da9ac3d841b85a..4bd6f8542902d200e6e7230e74f4a9ff2c933f09 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: roundupdb.py,v 1.77 2003-01-14 22:19:27 richard Exp $
+# $Id: roundupdb.py,v 1.95 2003-11-16 20:01:16 jlgijsbers Exp $
 
 __doc__ = """
 Extending hyperdb with types specific to issue-tracking.
 """
+from __future__ import nested_scopes
 
 import re, os, smtplib, socket, time, random
-import MimeWriter, cStringIO
-import base64, quopri, mimetypes
-# if available, use the 'email' module, otherwise fallback to 'rfc822'
-try :
-    from email.Utils import formataddr as straddr
-except ImportError :
-    # code taken from the email package 2.4.3
-    def straddr(pair, specialsre = re.compile(r'[][\()<>@,:;".]'),
-            escapesre = re.compile(r'[][\()"]')):
-        name, address = pair
-        if name:
-            quotes = ''
-            if specialsre.search(name):
-                quotes = '"'
-            name = escapesre.sub(r'\\\g<0>', name)
-            return '%s%s%s <%s>' % (quotes, name, quotes, address)
-        return address
-
-import hyperdb
-
-# set to indicate to roundup not to actually _send_ email
-# this var must contain a file to write the mail to
-SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '')
+import cStringIO, base64, quopri, mimetypes
+
+from rfc2822 import encode_header
+
+from roundup import password, date, hyperdb
+
+# MessageSendError is imported for backwards compatibility
+from roundup.mailer import Mailer, straddr, MessageSendError
 
 class Database:
     def getuid(self):
         """Return the id of the "user" node associated with the user
         that owns this connection to the hyperdatabase."""
-        return self.user.lookup(self.journaltag)
+        if self.journaltag is None:
+            return None
+        elif self.journaltag == 'admin':
+            # admin user may not exist, but always has ID 1
+            return '1'
+        else:
+            return self.user.lookup(self.journaltag)
+
+    def getUserTimezone(self):
+        """Return user timezone defined in 'timezone' property of user class.
+        If no such property exists return 0
+        """
+        userid = self.getuid()
+        try:
+            timezone = int(self.user.get(userid, 'timezone'))
+        except (KeyError, ValueError, TypeError):
+            # If there is no class 'user' or current user doesn't have timezone 
+            # property or that property is not numeric assume he/she lives in 
+            # Greenwich :)
+            timezone = 0
+        return timezone
+
+    def confirm_registration(self, otk):
+        props = self.otks.getall(otk)
+        for propname, proptype in self.user.getprops().items():
+            value = props.get(propname, None)
+            if value is None:
+                pass
+            elif isinstance(proptype, hyperdb.Date):
+                props[propname] = date.Date(value)
+            elif isinstance(proptype, hyperdb.Interval):
+                props[propname] = date.Interval(value)
+            elif isinstance(proptype, hyperdb.Password):
+                props[propname] = password.Password()
+                props[propname].unpack(value)
+
+        # tag new user creation with 'admin'
+        self.journaltag = 'admin'
+
+        # create the new user
+        cl = self.user
+      
+        props['roles'] = self.config.NEW_WEB_USER_ROLES
+        del props['__time']
+        userid = cl.create(**props)
+        # clear the props from the otk database
+        self.otks.destroy(otk)
+        self.commit()
+        
+        return userid
 
-class MessageSendError(RuntimeError):
-    pass
 
 class DetectorError(RuntimeError):
-    ''' Raised by detectors that want to indicate that something's amiss
-    '''
+    """ Raised by detectors that want to indicate that something's amiss
+    """
     pass
 
 # deviation from spec - was called IssueClass
@@ -97,64 +130,54 @@ class IssueClass:
         
         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'.
         """
-        users = self.db.user
-        messages = self.db.msg
-
-        # figure the recipient ids
+        authid = self.db.msg.safeget(msgid, 'author')
+        recipients = self.db.msg.safeget(msgid, 'recipients', [])
+        
         sendto = []
-        r = {}
-        recipients = messages.get(msgid, 'recipients')
-        for recipid in messages.get(msgid, 'recipients'):
-            r[recipid] = 1
-
-        # figure the author's id, and indicate they've received the message
-        authid = messages.get(msgid, 'author')
-
+        seen_message = dict([(recipient, 1) for recipient in recipients])
+               
+        def add_recipient(userid):
+            # make sure they have an address
+            address = self.db.user.get(userid, 'address')
+            if address:
+                sendto.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
+                    (self.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 (self.db.config.MESSAGES_TO_AUTHOR == 'yes' and
-                users.get(authid, 'username') != 'anonymous'):
-            sendto.append(authid)
-        r[authid] = 1
-
-        # now deal with cc people.
-        for cc_userid in cc :
-            if r.has_key(cc_userid):
-                continue
-            # send it to them
-            sendto.append(cc_userid)
-            recipients.append(cc_userid)
-
-        # now figure the nosy people who weren't recipients
-        nosy = self.get(nodeid, whichnosy)
-        for nosyid in nosy:
-            # Don't send nosy mail to the anonymous user (that user
-            # shouldn't appear in the nosy list, but just in case they
-            # do...)
-            if users.get(nosyid, 'username') == 'anonymous':
-                continue
-            # make sure they haven't seen the message already
-            if not r.has_key(nosyid):
-                # send it to them
-                sendto.append(nosyid)
-                recipients.append(nosyid)
+        if (good_recipient(authid) and
+            (self.db.config.MESSAGES_TO_AUTHOR == 'yes' or
+             (self.db.config.MESSAGES_TO_AUTHOR == 'new' and not oldvalues))):
+            add_recipient(authid)
+        
+        if authid:
+            seen_message[authid] = 1
+        
+        # now deal with the nosy and cc people who weren't recipients.
+        for userid in cc + self.get(nodeid, whichnosy):
+            if good_recipient(userid):
+                add_recipient(userid)        
 
-        # generate a change note
         if oldvalues:
             note = self.generateChangeNote(nodeid, oldvalues)
         else:
             note = self.generateCreateNote(nodeid)
 
-        # we have new recipients
+        # If we have new recipients, update the message's recipients
+        # and send the mail.
         if sendto:
-            # map userids to addresses
-            sendto = [users.get(i, 'address') for i in sendto]
-
-            # update the message's recipients list
-            messages.set(msgid, recipients=recipients)
-
-            # send the message
+            if msgid:
+                self.db.msg.set(msgid, recipients=recipients)
             self.send_message(nodeid, msgid, note, sendto, from_address)
 
     # backwards compatibility - don't remove
@@ -167,32 +190,30 @@ class IssueClass:
         users = self.db.user
         messages = self.db.msg
         files = self.db.file
-
-        # determine the messageid and inreplyto of the message
-        inreplyto = messages.get(msgid, 'inreplyto')
-        messageid = messages.get(msgid, 'messageid')
+       
+        inreplyto = messages.safeget(msgid, 'inreplyto')
+        messageid = messages.safeget(msgid, 'messageid')
 
         # make up a messageid if there isn't one (web edit)
         if not messageid:
             # this is an old message that didn't get a messageid, so
             # create one
             messageid = "<%s.%s.%s%s@%s>"%(time.time(), random.random(),
-                self.classname, nodeid, self.db.config.MAIL_DOMAIN)
+                                           self.classname, nodeid,
+                                           self.db.config.MAIL_DOMAIN)
             messages.set(msgid, messageid=messageid)
 
         # send an email to the people who missed out
         cn = self.classname
         title = self.get(nodeid, 'title') or '%s message copy'%cn
-        # figure author information
-        authid = messages.get(msgid, 'author')
-        authname = users.get(authid, 'realname')
+
+        authid = messages.safeget(msgid, 'author')
+        authname = users.safeget(authid, 'realname')
         if not authname:
-            authname = users.get(authid, 'username')
-        authaddr = users.get(authid, 'address')
+            authname = users.safeget(authid, 'username', '')
+        authaddr = users.safeget(authid, 'address', '')
         if authaddr:
             authaddr = " <%s>" % straddr( ('',authaddr) )
-        else:
-            authaddr = ''
 
         # make the message body
         m = ['']
@@ -202,14 +223,17 @@ class IssueClass:
             m.append(self.email_signature(nodeid, msgid))
 
         # add author information
-        if len(self.get(nodeid,'messages')) == 1:
-            m.append("New submission from %s%s:"%(authname, authaddr))
+        if authid:
+            if len(self.get(nodeid,'messages')) == 1:
+                m.append("New submission from %s%s:"%(authname, authaddr))
+            else:
+                m.append("%s%s added the comment:"%(authname, authaddr))
         else:
-            m.append("%s%s added the comment:"%(authname, authaddr))
+            m.append("System message:")
         m.append('')
 
         # add the content
-        m.append(messages.get(msgid, 'content'))
+        m.append(messages.safeget(msgid, 'content', ''))
 
         # add the change note
         if note:
@@ -226,7 +250,7 @@ class IssueClass:
         content_encoded = content_encoded.getvalue()
 
         # get the files for this message
-        message_files = messages.get(msgid, 'files')
+        message_files = msgid and messages.get(msgid, 'files') or None
 
         # make sure the To line is always the same (for testing mostly)
         sendto.sort()
@@ -240,34 +264,26 @@ class IssueClass:
         if from_tag:
             from_tag = ' ' + from_tag
 
+        subject = '[%s%s] %s' % (cn, nodeid, encode_header(title))
+        author = straddr((encode_header(authname) + from_tag, from_address))
+
         # create the message
-        message = cStringIO.StringIO()
-        writer = MimeWriter.MimeWriter(message)
-        writer.addheader('Subject', '[%s%s] %s'%(cn, nodeid, title))
-        writer.addheader('To', ', '.join(sendto))
-        writer.addheader('From', straddr((authname + from_tag, from_address)))
-        writer.addheader('Reply-To', straddr((self.db.config.TRACKER_NAME,
-            from_address)))
-        writer.addheader('Date', time.strftime("%a, %d %b %Y %H:%M:%S +0000",
-            time.gmtime()))
-        writer.addheader('MIME-Version', '1.0')
+        mailer = Mailer(self.db.config)
+        message, writer = mailer.get_standard_message(sendto, subject, author)
+
+        tracker_name = encode_header(self.db.config.TRACKER_NAME)
+        writer.addheader('Reply-To', straddr((tracker_name, from_address)))
         if messageid:
             writer.addheader('Message-Id', messageid)
         if inreplyto:
             writer.addheader('In-Reply-To', inreplyto)
 
-        # add a uniquely Roundup header to help filtering
-        writer.addheader('X-Roundup-Name', self.db.config.TRACKER_NAME)
-
-        # avoid email loops
-        writer.addheader('X-Roundup-Loop', 'hello')
-
         # attach files
         if message_files:
             part = writer.startmultipartbody('mixed')
             part = writer.nextpart()
             part.addheader('Content-Transfer-Encoding', 'quoted-printable')
-            body = part.startbody('text/plain')
+            body = part.startbody('text/plain; charset=utf-8')
             body.write(content_encoded)
             for fileid in message_files:
                 name = files.get(fileid, 'name')
@@ -295,27 +311,10 @@ class IssueClass:
             writer.lastpart()
         else:
             writer.addheader('Content-Transfer-Encoding', 'quoted-printable')
-            body = writer.startbody('text/plain')
+            body = writer.startbody('text/plain; charset=utf-8')
             body.write(content_encoded)
 
-        # now try to send the message
-        if SENDMAILDEBUG:
-            open(SENDMAILDEBUG, 'a').write('FROM: %s\nTO: %s\n%s\n'%(
-                self.db.config.ADMIN_EMAIL,
-                ', '.join(sendto),message.getvalue()))
-        else:
-            try:
-                # send the message as admin so bounces are sent there
-                # instead of to roundup
-                smtp = smtplib.SMTP(self.db.config.MAILHOST)
-                smtp.sendmail(self.db.config.ADMIN_EMAIL, sendto,
-                    message.getvalue())
-            except socket.error, value:
-                raise MessageSendError, \
-                    "Couldn't send confirmation email: mailhost %s"%value
-            except smtplib.SMTPException, value:
-                raise MessageSendError, \
-                    "Couldn't send confirmation email: %s"%value
+        mailer.smtp_send(sendto, message)
 
     def email_signature(self, nodeid, msgid):
         ''' Add a signature to the e-mail with some useful information
@@ -335,8 +334,8 @@ class IssueClass:
         email = straddr((self.db.config.TRACKER_NAME,
             self.db.config.TRACKER_EMAIL))
 
-        line = '_' * max(len(web), len(email))
-        return '%s\n%s\n%s\n%s'%(line, email, web, line)
+        line = '_' * max(len(web)+2, len(email))
+        return '%s\n%s\n<%s>\n%s'%(line, email, web, line)
 
 
     def generateCreateNote(self, nodeid):
@@ -396,8 +395,14 @@ class IssueClass:
                 continue
             if key in ('activity', 'creator', 'creation'):
                 continue
-            new_value = cl.get(nodeid, key)
+            # not all keys from oldvalues might be available in database
+            # this happens when property was deleted
+            try:                
+                new_value = cl.get(nodeid, key)
+            except KeyError:
+                continue
             # the old value might be non existent
+            # this happens when property was added
             try:
                 old_value = oldvalues[key]
                 if type(new_value) is type([]):