# Richard Darst, June 2009 ### # Copyright (c) 2009, Richard Darst # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions, and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions, and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the author of this software nor the name of # contributors to this software may be used to endorse or promote products # derived from this software without specific prior written consent. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. ### import os import re import textwrap import time #from meeting import timeZone, meetBotInfoURL # Needed for testing with isinstance() for properly writing. #from items import Topic, Action import items # Data sanitizing for various output methods def html(text): """Escape bad sequences (in HTML) in user-generated lines.""" return text.replace("&", "&").replace("<", "<").replace(">", ">") rstReplaceRE = re.compile('_( |-|$)') def rst(text): """Escapes bad sequences in reST""" return rstReplaceRE.sub(r'\_\1', text) def text(text): """Escapes bad sequences in text (not implemented yet)""" return text # wraping functions (for RST) class TextWrapper(textwrap.TextWrapper): wordsep_re = re.compile(r'(\s+)') def wrapList(item, indent=0): return TextWrapper(width=72, initial_indent=' '*indent, subsequent_indent= ' '*(indent+2), break_long_words=False).fill(item) def replaceWRAP(item): re_wrap = re.compile(r'sWRAPs(.*)eWRAPe', re.DOTALL) def repl(m): return TextWrapper(width=72, break_long_words=False).fill(m.group(1)) return re_wrap.sub(repl, item) def MeetBotVersion(): import meeting if hasattr(meeting, '__version__'): return ' '+meeting.__version__ else: return '' class _BaseWriter(object): def __init__(self, M, **kwargs): self.M = M def format(self, extension=None): """Override this method to implement the formatting. For file output writers, the method should return a unicode object containing the contents of the file to write. The argument 'extension' is the key from `writer_map`. For file writers, this can (and should) be ignored. For non-file outputs, this can be used to This can be used to pass data, """ raise NotImplementedError @property def pagetitle(self): if self.M._meetingTopic: return "%s: %s"%(self.M.channel, self.M._meetingTopic) return "%s Meeting"%self.M.channel def replacements(self): return {'pageTitle':self.pagetitle, 'owner':self.M.owner, 'starttime':time.strftime("%H:%M:%S", self.M.starttime), 'endtime':time.strftime("%H:%M:%S", self.M.endtime), 'timeZone':self.M.config.timeZone, 'fullLogs':self.M.config.basename+'.log.html', 'fullLogsFullURL':self.M.config.filename(url=True)+'.log.html', 'MeetBotInfoURL':self.M.config.MeetBotInfoURL, 'MeetBotVersion':MeetBotVersion(), } def iterNickCounts(self): nicks = [ (n,c) for (n,c) in self.M.attendees.iteritems() ] nicks.sort(key=lambda x: x[1], reverse=True) return nicks def iterActionItemsNick(self): for nick in sorted(self.M.attendees.keys(), key=lambda x: x.lower()): def nickitems(): for m in self.M.minutes: # The hack below is needed because of pickling problems if m.itemtype != "ACTION": continue if m.line.find(nick) == -1: continue m.assigned = True yield m yield nick, nickitems() def iterActionItemsUnassigned(self): for m in self.M.minutes: if m.itemtype != "ACTION": continue if getattr(m, 'assigned', False): continue yield m class TextLog(_BaseWriter): def format(self, extension=None): M = self.M """Write raw text logs.""" return "\n".join(M.lines) update_realtime = True class HTMLlog(_BaseWriter): def format(self, extension=None): """Write pretty HTML logs.""" M = self.M # pygments lexing setup: # (pygments HTML-formatter handles HTML-escaping) import pygments from pygments.lexers import IrcLogsLexer from pygments.formatters import HtmlFormatter import pygments.token as token from pygments.lexer import bygroups # Don't do any encoding in this function with pygments. # That's only right before the i/o functions in the Config # object. formatter = HtmlFormatter(lineanchors='l', full=True, style=M.config.pygmentizeStyle, output_encoding=self.M.config.output_codec) Lexer = IrcLogsLexer Lexer.tokens['msg'][1:1] = \ [ # match: #topic commands (r"(\#topic[ \t\f\v]*)(.*\n)", bygroups(token.Keyword, token.Generic.Heading), '#pop'), # match: #command (others) (r"(\#[^\s]+[ \t\f\v]*)(.*\n)", bygroups(token.Keyword, token.Generic.Strong), '#pop'), ] lexer = Lexer() #from rkddp.interact import interact ; interact() out = pygments.highlight("\n".join(M.lines), lexer, formatter) return out class HTML(_BaseWriter): body = textwrap.dedent('''\ %(pageTitle)s

%(pageTitle)s

Meeting started by %(owner)s at %(starttime)s %(timeZone)s. (full logs)
%(MeetingItems)s
Meeting ended at %(endtime)s %(timeZone)s. (full logs)


Action Items
    %(ActionItems)s

Action Items, by person
    %(ActionItemsPerson)s

People Present (lines said):
    %(PeoplePresent)s

Generated by MeetBot%(MeetBotVersion)s. ''') def format(self, extension=None): """Write the minutes summary.""" M = self.M # Add all minute items to the table MeetingItems = [ ] for m in M.minutes: MeetingItems.append(m.html(M)) MeetingItems = "\n".join(MeetingItems) # Action Items ActionItems = [ ] for m in M.minutes: # The hack below is needed because of pickling problems if m.itemtype != "ACTION": continue ActionItems.append("
  • %s
  • "%html(m.line)) if len(ActionItems) == 0: ActionItems.append("
  • (none)
  • ") ActionItems = "\n".join(ActionItems) # Action Items, by person (This could be made lots more efficient) ActionItemsPerson = [ ] for nick, items in self.iterActionItemsNick(): headerPrinted = False for m in items: if not headerPrinted: ActionItemsPerson.append("
  • %s
      "%html(nick)) headerPrinted = True ActionItemsPerson.append("
    1. %s
    2. "%html(m.line)) if headerPrinted: ActionItemsPerson.append("
  • ") # unassigned items: ActionItemsPerson.append("
  • UNASSIGNED
      ") numberUnassigned = 0 for m in self.iterActionItemsUnassigned(): ActionItemsPerson.append("
    1. %s
    2. "%html(m.line)) numberUnassigned += 1 if numberUnassigned == 0: ActionItemsPerson.append("
    3. (none)
    4. ") ActionItemsPerson.append('
    \n
  • ') ActionItemsPerson = "\n".join(ActionItemsPerson) # People Attending PeoplePresent = [ ] # sort by number of lines spoken for nick, count in self.iterNickCounts(): PeoplePresent.append('
  • %s (%s)
  • '%(html(nick), count)) PeoplePresent = "\n".join(PeoplePresent) # Actual formatting and replacement repl = self.replacements() repl.update({'MeetingItems':MeetingItems, 'ActionItems': ActionItems, 'ActionItemsPerson': ActionItemsPerson, 'PeoplePresent':PeoplePresent, }) body = self.body body = body%repl body = replaceWRAP(body) return body class HTML2(_BaseWriter): body = textwrap.dedent('''\ %(pageTitle)s

    %(pageTitle)s

    Meeting started by %(owner)s at %(starttime)s %(timeZone)s. (full logs)
    %(MeetingItems)s Meeting ended at %(endtime)s %(timeZone)s. (full logs)


    Action Items
      %(ActionItems)s

    Action Items, by person
      %(ActionItemsPerson)s

    People Present (lines said):
      %(PeoplePresent)s

    Generated by MeetBot%(MeetBotVersion)s. ''') def format(self, extension=None): """Write the minutes summary.""" M = self.M # Add all minute items to the table MeetingItems = [ ] MeetingItems.append("
      ") haveTopic = None inSublist = False for m in M.minutes: item = '
    1. '+m.html2(M) if m.itemtype == "TOPIC": if inSublist: MeetingItems.append("
    ") inSublist = False if haveTopic: MeetingItems.append("
    ") item = item haveTopic = True else: if not inSublist: MeetingItems.append('
      ') inSublist = True if haveTopic: item = wrapList(item, 2)+"" else: item = wrapList(item, 0)+"" MeetingItems.append(item) #MeetingItems.append("") if inSublist: MeetingItems.append("
    ") if haveTopic: MeetingItems.append("") MeetingItems.append("") MeetingItems = "\n".join(MeetingItems) # Action Items ActionItems = [ ] for m in M.minutes: # The hack below is needed because of pickling problems if m.itemtype != "ACTION": continue ActionItems.append("
  • %s
  • "%html(m.line)) if len(ActionItems) == 0: ActionItems.append("
  • (none)
  • ") ActionItems = "\n".join(ActionItems) # Action Items, by person (This could be made lots more efficient) ActionItemsPerson = [ ] for nick, items in self.iterActionItemsNick(): headerPrinted = False for m in items: if not headerPrinted: ActionItemsPerson.append("
  • %s
      "%html(nick)) headerPrinted = True ActionItemsPerson.append("
    1. %s
    2. "%html(m.line)) if headerPrinted: ActionItemsPerson.append("
  • ") # unassigned items: ActionItemsPerson.append("
  • UNASSIGNED
      ") numberUnassigned = 0 for m in self.iterActionItemsUnassigned(): ActionItemsPerson.append("
    1. %s
    2. "%html(m.line)) numberUnassigned += 1 if numberUnassigned == 0: ActionItemsPerson.append("
    3. (none)
    4. ") ActionItemsPerson.append('
    \n
  • ') ActionItemsPerson = "\n".join(ActionItemsPerson) # People Attending PeoplePresent = [ ] # sort by number of lines spoken for nick, count in self.iterNickCounts(): PeoplePresent.append('
  • %s (%s)
  • '%(html(nick), count)) PeoplePresent = "\n".join(PeoplePresent) # Actual formatting and replacement repl = self.replacements() repl.update({'MeetingItems':MeetingItems, 'ActionItems': ActionItems, 'ActionItemsPerson': ActionItemsPerson, 'PeoplePresent':PeoplePresent, }) body = self.body body = body%repl body = replaceWRAP(body) return body class ReST(_BaseWriter): body = textwrap.dedent("""\ %(titleBlock)s %(pageTitle)s %(titleBlock)s sWRAPsMeeting started by %(owner)s at %(starttime)s %(timeZone)s. The `full logs`_ are available.eWRAPe .. _`full logs`: %(fullLogs)s Meeting log ----------- %(MeetingItems)s Meeting ended at %(endtime)s %(timeZone)s. Action Items ------------ %(ActionItems)s Action Items, by person ----------------------- %(ActionItemsPerson)s People Present (lines said) --------------------------- %(PeoplePresent)s Generated by `MeetBot`_%(MeetBotVersion)s .. _`MeetBot`: %(MeetBotInfoURL)s """) def format(self, extension=None): """Return a ReStructured Text minutes summary.""" M = self.M # Agenda items MeetingItems = [ ] M.rst_urls = [ ] M.rst_refs = { } haveTopic = None for m in M.minutes: item = "* "+m.rst(M) if m.itemtype == "TOPIC": if haveTopic: MeetingItems.append("") item = wrapList(item, 0) haveTopic = True else: if haveTopic: item = wrapList(item, 2) else: item = wrapList(item, 0) MeetingItems.append(item) MeetingItems = '\n\n'.join(MeetingItems) MeetingURLs = "\n".join(M.rst_urls) del M.rst_urls, M.rst_refs MeetingItems = MeetingItems + '\n\n'+MeetingURLs # Action Items ActionItems = [ ] for m in M.minutes: # The hack below is needed because of pickling problems if m.itemtype != "ACTION": continue #already escaped ActionItems.append(wrapList("* %s"%rst(m.line), indent=0)) ActionItems = "\n\n".join(ActionItems) # Action Items, by person (This could be made lots more efficient) ActionItemsPerson = [ ] for nick in sorted(M.attendees.keys(), key=lambda x: x.lower()): headerPrinted = False for m in M.minutes: # The hack below is needed because of pickling problems if m.itemtype != "ACTION": continue if m.line.find(nick) == -1: continue if not headerPrinted: ActionItemsPerson.append("* %s"%rst(nick)) headerPrinted = True ActionItemsPerson.append(wrapList("* %s"%rst(m.line), 2)) m.assigned = True # unassigned items: ActionItemsPerson.append("* **UNASSIGNED**") numberUnassigned = 0 for m in M.minutes: if m.itemtype != "ACTION": continue if getattr(m, 'assigned', False): continue ActionItemsPerson.append(wrapList("* %s"%rst(m.line), 2)) numberUnassigned += 1 if numberUnassigned == 0: ActionItemsPerson.append(" * (none)") ActionItemsPerson = "\n\n".join(ActionItemsPerson) # People Attending PeoplePresent = [ ] # sort by number of lines spoken for nick, count in self.iterNickCounts(): PeoplePresent.append('* %s (%s)'%(rst(nick), count)) PeoplePresent = "\n\n".join(PeoplePresent) # Actual formatting and replacement repl = self.replacements() repl.update({'titleBlock':('='*len(repl['pageTitle'])), 'MeetingItems':MeetingItems, 'ActionItems': ActionItems, 'ActionItemsPerson': ActionItemsPerson, 'PeoplePresent':PeoplePresent, }) body = self.body body = body%repl body = replaceWRAP(body) return body class HTMLfromReST(_BaseWriter): def format(self, extension=None): M = self.M import docutils.core rst = ReST(M).format(extension) rstToHTML = docutils.core.publish_string(rst, writer_name='html', settings_overrides={'file_insertion_enabled': 0, 'raw_enabled': 0, 'output_encoding':self.M.config.output_codec}) return rstToHTML class Text(_BaseWriter): body = textwrap.dedent("""\ %(titleBlock)s %(pageTitle)s %(titleBlock)s sWRAPsMeeting started by %(owner)s at %(starttime)s %(timeZone)s. The full logs are available at %(fullLogsFullURL)s .eWRAPe Meeting log ----------- %(MeetingItems)s Meeting ended at %(endtime)s %(timeZone)s. Action Items ------------ %(ActionItems)s Action Items, by person ----------------------- %(ActionItemsPerson)s People Present (lines said) --------------------------- %(PeoplePresent)s Generated by `MeetBot`_%(MeetBotVersion)s .. _`MeetBot`: %(MeetBotInfoURL)s """) def format(self, extension=None): """Return a ReStructured Text minutes summary.""" M = self.M # Agenda items MeetingItems = [ ] #M.rst_urls = [ ] #M.rst_refs = { } haveTopic = None for m in M.minutes: item = "* "+m.text(M) if m.itemtype == "TOPIC": if haveTopic: MeetingItems.append("") item = wrapList(item, 0) haveTopic = True else: if haveTopic: item = wrapList(item, 2) else: item = wrapList(item, 0) MeetingItems.append(item) MeetingItems = '\n'.join(MeetingItems) #MeetingURLs = "\n".join(M.rst_urls) #del M.rst_urls, M.rst_refs MeetingItems = MeetingItems# + '\n\n'+MeetingURLs # Action Items ActionItems = [ ] for m in M.minutes: # The hack below is needed because of pickling problems if m.itemtype != "ACTION": continue #already escaped ActionItems.append(wrapList("* %s"%text(m.line), indent=0)) ActionItems = "\n".join(ActionItems) # Action Items, by person (This could be made lots more efficient) ActionItemsPerson = [ ] for nick in sorted(M.attendees.keys(), key=lambda x: x.lower()): headerPrinted = False for m in M.minutes: # The hack below is needed because of pickling problems if m.itemtype != "ACTION": continue if m.line.find(nick) == -1: continue if not headerPrinted: ActionItemsPerson.append("* %s"%text(nick)) headerPrinted = True ActionItemsPerson.append(wrapList("* %s"%text(m.line), 2)) m.assigned = True # unassigned items: ActionItemsPerson.append("* **UNASSIGNED**") numberUnassigned = 0 for m in M.minutes: if m.itemtype != "ACTION": continue if getattr(m, 'assigned', False): continue ActionItemsPerson.append(wrapList("* %s"%text(m.line), 2)) numberUnassigned += 1 if numberUnassigned == 0: ActionItemsPerson.append(" * (none)") ActionItemsPerson = "\n".join(ActionItemsPerson) # People Attending PeoplePresent = [ ] # sort by number of lines spoken for nick, count in self.iterNickCounts(): PeoplePresent.append('* %s (%s)'%(text(nick), count)) PeoplePresent = "\n".join(PeoplePresent) # Actual formatting and replacement repl = self.replacements() repl.update({'titleBlock':('='*len(repl['pageTitle'])), 'MeetingItems':MeetingItems, 'ActionItems': ActionItems, 'ActionItemsPerson': ActionItemsPerson, 'PeoplePresent':PeoplePresent, }) body = self.body body = body%repl body = replaceWRAP(body) return body