Changeset 3046

Show
Ignore:
Timestamp:
01/13/08 00:59:36 (10 months ago)
Author:
ixokai
Message:

Added attachment notifications for tickets; a 'default domain' resolver (always append @domain), and the ability for users to join any number of 'groups' defined as admin and if said groups are CC'd to (by @<groupname>) everyone in said group gets the notice.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • announcerplugin/0.11/announcerplugin/api.py

    r3041 r3046  
    9494         
    9595        If a single item is to be returned, use yield instead of return.""" 
    96  
     96         
     97    def get_format_alternative(transport, realm, style): 
     98        """...""" 
     99         
    97100    def format(transport, realm, style, event): 
    98101        """Converts the event into the specified style. If the transport or 
     
    107110    def format_subject(transport, realm, style, event): 
    108111        """Returns a suitable subject line for the specified event.""" 
     112         
     113    def format_headers(transport, realm, style, event): 
     114        """...""" 
    109115 
    110116class IAnnouncementDistributor(Interface): 
  • announcerplugin/0.11/announcerplugin/distributors/email_distributor.py

    r3041 r3046  
    22from trac.util.compat import set, sorted 
    33from trac.config import Option, BoolOption, IntOption, OrderedExtensionsOption 
     4from trac.util import get_pkginfo 
    45from announcerplugin.api import IAnnouncementDistributor 
    56from announcerplugin.api import IAnnouncementFormatter 
     
    78from announcerplugin.api import IAnnouncementAddressResolver 
    89from announcerplugin.api import AnnouncementSystem 
     10import announcerplugin, trac 
    911 
    1012from email.MIMEMultipart import MIMEMultipart 
     
    6567        """Email address(es) to always send notifications to, 
    6668           addresses do not appear publicly (Bcc:). (''since 0.10'').""") 
    67             
    68     smtp_default_domain = Option('announcer', 'smtp_default_domain', '', 
    69         """Default host/domain to append to address that do not specify one""") 
    70          
     69                    
    7170    ignore_domains = Option('announcer', 'ignore_domains', '', 
    7271        """Comma-separated list of domains that should not be considered 
     
    111110    """If true, the actual delivery of the message will occur in a separate thread.""") 
    112111     
     112    default_email_format = Option('announcer', 'default_email_format', 'text/plain') 
     113     
    113114    def __init__(self): 
    114115        if self.use_threaded_delivery: 
     
    149150                if format not in messages: 
    150151                    messages[format] = set() 
    151                      
     152                 
    152153                if name and not address: 
    153154                    for resolver in self.resolvers: 
    154155                        address = resolver.get_address_for_name(name) 
    155156                        if address: 
     157                            self.log.debug("EmailDistributor found the address '%s' for '%s' via: %s" % ( 
     158                                    address, name, resolver.__class__.__name__ 
     159                                ) 
     160                            ) 
    156161                            break 
    157162                             
     
    171176                    ) 
    172177                    self._do_send(transport, event, format, messages[format], formats[format]) 
    173  
     178                     
    174179    def _get_default_format(self): 
    175         return 'plaintext' 
     180        return self.default_email_format 
    176181         
    177182    def _get_preferred_format(self, realm, sid): 
     
    199204        subject = formatter.format_subject(transport, event.realm, format, event) 
    200205         
    201         parentMessage = MIMEMultipart("related") 
    202         parentMessage['Subject'] = subject 
    203         parentMessage['From'] = self.smtp_from 
    204         parentMessage['To'] = self.env.project_name 
    205         parentMessage['Reply-To'] = self.smtp_replyto 
    206         parentMessage.preamble = 'This is a multi-part message in MIME format.' 
    207          
    208         msgText = MIMEText(output, 'html') 
     206        alternate_format = formatter.get_format_alternative(transport, event.realm, format) 
     207        if alternate_format: 
     208            alternate_output = formatter.format(transport, event.realm, alternate_format, event) 
     209        else: 
     210            alternate_output = None 
     211             
     212        rootMessage = MIMEMultipart("related") 
     213        trac_version = get_pkginfo(trac.core).get('version', trac.__version__) 
     214        announcer_version = get_pkginfo(announcerplugin).get('version', 'Undefined') 
     215         
     216        rootMessage['X-Mailer'] = 'AnnouncerPlugin v%s on Trac v%s' % (announcer_version, trac_version) 
     217        rootMessage['X-Trac-Version'] = trac_version 
     218        rootMessage['X-Announcer-Version'] = announcer_version 
     219        rootMessage['X-Trac-Project'] = self.env.project_name 
     220        rootMessage['Precedence'] = 'bulk' 
     221        rootMessage['Auto-Submitted'] = 'auto-generated' 
     222         
     223        provided_headers = formatter.format_headers(transport, event.realm, format, event) 
     224        for key in provided_headers: 
     225            rootMessage['X-Announcement-%s' % key.capitalize()] = str(provided_headers[key]) 
     226         
     227        rootMessage['Subject'] = subject 
     228        rootMessage['From'] = self.smtp_from 
     229        rootMessage['To'] = self.env.project_name 
     230        rootMessage['Reply-To'] = self.smtp_replyto 
     231        rootMessage.preamble = 'This is a multi-part message in MIME format.' 
     232         
     233        if alternate_output: 
     234            parentMessage = MIMEMultipart('alternative') 
     235            rootMessage.attach(parentMessage) 
     236        else: 
     237            parentMessage = rootMessage 
     238         
     239        if alternate_output: 
     240            msgText = MIMEText(alternate_output, 'html' in alternate_format and 'html' or 'plain') 
     241            parentMessage.attach(msgText) 
     242         
     243        msgText = MIMEText(output, 'html' in format and 'html' or 'plain') 
    209244        parentMessage.attach(msgText) 
    210245         
    211246        start = time.time() 
    212247         
    213         package = (self.smtp_from, [x[1] for x in recipients if x], parentMessage.as_string() ) 
     248        package = (self.smtp_from, [x[1] for x in recipients if x], rootMessage.as_string() ) 
    214249        if self.use_threaded_delivery: 
    215250            self._deliveryQueue.put(package) 
    216251        else: 
    217252            self._transmit(*package) 
    218              
     253 
    219254        stop = time.time() 
    220255        self.log.debug("EmailDistributor took %s seconds to send." % (round(stop-start,2))) 
  • announcerplugin/0.11/announcerplugin/formatters/ticket_email.py

    r3041 r3046  
    3030class TicketEmailFormatter(Component): 
    3131    implements(IAnnouncementFormatter) 
    32      
    33     default_email_format = Option('announcer', 'default_email_format', 'plaintext') 
    34      
     32         
    3533    ticket_email_subject = Option('announcer', 'ticket_email_subject', "Ticket #${ticket.id}: ${ticket['summary']}") 
    3634     
     
    4644        if transport == "email": 
    4745            if realm == "ticket": 
    48                 yield "plaintext
    49                 yield "html" 
     46                yield "text/plain
     47                yield "text/html" 
    5048                 
    5149        return 
     50         
     51    def get_format_alternative(self, transport, realm, style): 
     52        if transport == "email": 
     53            if realm == "ticket": 
     54                if style == "text/html": 
     55                    return "text/plain" 
     56 
     57        return None 
     58         
     59    def format_headers(self, transport, realm, style, event): 
     60        ticket = event.target 
     61        return dict( 
     62            realm=realm, 
     63            ticket=ticket.id, 
     64            priority=ticket['priority'], 
     65            severity=ticket['severity']             
     66        ) 
    5267         
    5368    def format_subject(self, transport, realm, style, event): 
     
    6075        if transport == "email": 
    6176            if realm == "ticket": 
    62                 if hasattr(self, '_format_%s' % style): 
    63                     return getattr(self, '_format_%s' % style)(event) 
     77                if style == "text/plain": 
     78                    return self._format_plaintext(event) 
     79                elif style == "text/html": 
     80                    return self._format_html(event) 
    6481 
    6582    def _format_plaintext(self, event): 
     
    100117            long_changes = long_changes, 
    101118            short_changes = short_changes, 
     119            attachment= event.attachment 
    102120        ) 
    103121         
     
    155173            long_changes = long_changes, 
    156174            short_changes = short_changes, 
     175            attachment= event.attachment             
    157176        ) 
    158177         
  • announcerplugin/0.11/announcerplugin/producers/attachment.py

    r3015 r3046  
    11from trac.core import * 
    22from trac.attachment import IAttachmentChangeListener 
    3 from announcerplugin.api import AnnouncementSystem, AnnouncementEvent 
     3from announcerplugin.api import AnnouncementSystem 
     4from announcerplugin.producers.ticket import TicketChangeEvent 
     5from trac.ticket.model import Ticket 
    46 
    5 class AttachmentChangeEvent(AnnouncementEvent): 
    6     def __init__(self, realm, category, target,  
    7                  author=None): 
    8         AnnouncementEvent.__init__(self, realm, category, target) 
    9  
    10         self.author = author 
    11                  
    127class AttachmentChangeProducer(Component): 
    138    implements(IAttachmentChangeListener) 
     
    1712 
    1813    def attachment_added(self, attachment): 
    19         announcer = AnnouncementSystem(ticket.env) 
    20         announcer.send( 
    21             AttachmentChangeEvent(attachment.parent_realm, "attachment added", 
    22                 attachment, author=attachment.author,  
    23             ) 
    24         )             
     14        parent = attachment.resource.parent 
     15 
     16        if parent.realm == "ticket": 
     17            ticket = Ticket(self.env, parent.id) 
     18            announcer = AnnouncementSystem(ticket.env) 
     19            announcer.send( 
     20                TicketChangeEvent("ticket", "attachment added", ticket, 
     21                    attachment=attachment, author=attachment.author,  
     22                ) 
     23            )             
    2524 
    2625    def attachment_deleted(self, attachment): 
    27         announcer = AnnouncementSystem(ticket.env) 
    28         announcer.send( 
    29             AttachmentChangeEvent(attachment.parent_realm, "attachment added",  
    30                 attachment, author=attachment.author,  
    31             ) 
    32         ) 
     26        # announcer = AnnouncementSystem(ticket.env) 
     27        # announcer.send( 
     28        #     AttachmentChangeEvent(attachment.parent_realm, "attachment added",  
     29        #         attachment, author=attachment.author,  
     30        #     ) 
     31        # ) 
     32        pass 
  • announcerplugin/0.11/announcerplugin/producers/__init__.py

    r3015 r3046  
    11import ticket 
     2import attachment 
  • announcerplugin/0.11/announcerplugin/producers/ticket.py

    r3015 r3046  
    11from trac.core import * 
     2from trac.config import BoolOption 
    23from trac.ticket.api import ITicketChangeListener 
    34from announcerplugin.api import AnnouncementSystem, AnnouncementEvent 
     
    56class TicketChangeEvent(AnnouncementEvent): 
    67    def __init__(self, realm, category, target,  
    7                  comment=None, author=None, changes=None): 
     8                 comment=None, author=None, changes={}, 
     9                 attachment=None): 
    810        AnnouncementEvent.__init__(self, realm, category, target) 
    911 
     
    1113        self.comment = comment 
    1214        self.changes = changes 
     15        self.attachment = attachment 
    1316 
    1417class TicketChangeProducer(Component): 
    1518    implements(ITicketChangeListener) 
     19     
     20    ignore_cc_changes = BoolOption('announcer', 'ignore_cc_changes', False, 
     21        doc="""When true, the system will not send out announcement events if 
     22        the only field that was changed was CC. A change to the CC field that 
     23        happens at the same as another field will still result in an event 
     24        being created.""") 
    1625     
    1726    def __init__(self, *args, **kwargs): 
     
    2736         
    2837    def ticket_changed(self, ticket, comment, author, old_values): 
     38        if old_values.keys() == ['cc'] and not comment: 
     39            return 
     40             
    2941        announcer = AnnouncementSystem(ticket.env) 
    3042        announcer.send( 
  • announcerplugin/0.11/announcerplugin/resolvers/specified.py

    r3041 r3046  
    1919               AND authenticated=1 
    2020               AND name=%s 
    21         """, (name,'specified_email')) 
     21        """, (name,'announcer_specified_email')) 
    2222         
    2323        result = cursor.fetchone() 
  • announcerplugin/0.11/announcerplugin/subscribers/__init__.py

    r3015 r3046  
    11import ticket 
    22import ticket_compat 
     3import ticket_groups 
  • announcerplugin/0.11/announcerplugin/subscribers/ticket_compat.py

    r3040 r3046  
    44from trac.web.chrome import add_warning 
    55from trac.config import BoolOption 
     6import re 
    67 
    78class StaticTicketSubscriber(Component): 
     
    8990    def get_subscription_categories(self, realm): 
    9091        if realm == 'ticket': 
    91             return ('created', 'changed'
     92            return ('created', 'changed', 'attachment added'
    9293        else: 
    9394            return tuple() 
     
    9697        if event.realm == "ticket": 
    9798            ticket = event.target 
    98             if event.category == "created"
     99            if event.category in ('changed', 'attachment added')
    99100                component = model.Component(self.env, ticket['component']) 
    100101                if component.owner: 
     
    133134            r = result[0] == '0' 
    134135            self.log.debug("LegacyTicketSubscriber excluded '%s' because of opt-out rule: %s" % (sid,preference)) 
    135             return r 
     136            return True 
    136137         
    137138        return False 
     
    144145         
    145146    def get_subscription_categories(self, *args): 
    146         return ('changed',
     147        return ('changed', 'attachment added'
    147148         
    148149    def get_subscriptions_for_event(self, event): 
    149150        if event.realm == 'ticket': 
    150             if event.category == 'changed'
     151            if event.category in ('changed', 'attachment added')
    151152                cc = event.target['cc'] 
    152                 for chunk in cc.split(','): 
     153                for chunk in re.split('\s|,', cc): 
     154                    chunk = chunk.strip() 
     155                    if not chunk or chunk.startswith('@'): 
     156                        continue 
     157                         
    153158                    if '@' in chunk: 
    154                         address = chunk.strip() 
     159                        address = chunk 
    155160                        name = None 
    156161                    else: 
    157                         name = chunk.strip() 
     162                        name = chunk 
    158163                        address = None 
     164                         
    159165                    if name or address: 
    160166                        self.log.debug("CarbonCopySubscriber added '%s <%s>' because of rule: carbon copied" % (name,address)) 
  • announcerplugin/0.11/announcerplugin/subscribers/ticket.py

    r3026 r3046  
    1111         
    1212    def get_subscription_categories(self, realm): 
    13         return ('created', 'changed'
     13        return ('created', 'changed', 'attachment added'
    1414         
    1515    def get_subscriptions_for_event(self, event): 
  • announcerplugin/0.11/announcerplugin/templates/ticket_email_mimic.html

    r3041 r3046  
    160160      </ul> 
    161161    </py:if> 
     162    <py:if test="attachment"> 
     163      <div class="commentstitle" style="font-size: small; margin: 1em;">Attachments:</div> 
     164      <ul> 
     165        <li>File <span class="fieldname" style="font-weight: bold; font-style: italic;">${attachment.filename}</span> added<py:if test="attachment.description">: <span style="font-style: italic">${attachment.description}</span></py:if></li> 
     166      </ul> 
     167    </py:if> 
    162168    <py:if test="comment"> 
    163169      <div class="commentstitle" style="font-size: small; margin: 1em;">Comments:</div> 
  • announcerplugin/0.11/announcerplugin/templates/ticket_email_plaintext.txt

    r3040 r3046  
    1818${ticket['description']} 
    1919{% end %}\ 
    20 {% if has_changes %}\ 
     20{% if has_changes or attachment %}\ 
    2121--------------------------------------------------------------------- 
    2222Changes (by ${author}):  
     
    3535${long_changes[change]} 
    3636{% end %}\ 
     37{% end %}\ 
     38{% if attachment %}\ 
     39Attachment: 
     40 * File '${attachment.filename}' added{% if attachment.description %}: ${attachment.description} {% end %} 
    3741{% end %} 
    3842{% if comment %}\