Page Source

from utils.display import Display, Database, Arguments, FileCopy, Modal
from utils.controls import *
from utils.web_exc import WebError
from utils.output import html
import queries, debugging
from queries import CacheQueries
from utils import perms, shapes, bounce, actions
from config import docroot
import string, re, time


####
# Note:
#
# This file has *three* display classes in it.
####

####
# 'Short Term'
#
# This is the number of seconds after the 'publication date' that
# authors and sponsors may make modifications to a tech report
####
short_term_seconds = 24*60*60           # 1 day

####
# TSFURLC
#
# Put a field in a URL, and put a timestamp next to it.
class TSFURLC(TimestampFieldControl, URLControl):
  """Combine the effects of TimestampFieldControl and URLControl
  Usage: TSFURLC("superseded_by", "superseded_by_ts", title="Superseded By")"""
  def __init__(self, field, ts_field, **kwargs):
    TimestampFieldControl.__init__(self, field, ts_field, **kwargs)
    URLControl.__init__(self, field, **kwargs)
  def output_disp(self, display, container):
    url = display.fields[self.field]
    if self.linktext:
      linktext = self.linktext
    else:
      linktext = url

    if url:
      url = misc.escape(url, quote=1)
      container.append("""<a href="%s">%s</a>""" % (url, linktext))
    elif self.nbsppad:
      container.append("&nbsp;")
    container.append(time.strftime(" <i>(updated %x)</i>", time.localtime(display.fields[self.ts_field])))

####
# AuthorsControl
#
# Show a nice list of Authors, with links to people pages where available

class AuthorsControl(MultipleControl, SingleControl):
  """In DISP, a semicolon-separated list of names, with links where
  available. In FORM, a big box allowing you to edit the authors."""
  def output_disp(self, display, container):
    author_str = []
    for author_l, author_n, _ in display.fields['authors']:
      if author_l:
        stag, etag = ('<a href="%(person!login)s">'
                      % display.urls( {'login' : author_l } ), 
                      "</a>")
      else:
        stag, etag = ("","")
      author_str.append(stag + author_n + etag)
    author_str = string.join(author_str, "; ")
    container.append(author_str)

  def output_form(self, display, container):
    # Figure out how many rows to show (4 more than already exist, and at least 8)
    num_rows = 8
    authors_prio = {}
    for login, name, priority in display.fields['authors']:
      authors_prio[priority] = (login, name)
      if num_rows < priority + 4:
        num_rows = priority + 4

    # Fire up some JavaScript
    container.append("""\
<script language="JavaScript">

function copy_to_name(num, val) {
  f = document.forms[0];
  if (!f['author_name_' + num].value)
    f['author_name_' + num].value = val;
}
</script>""")

        
    # Make the row in the big table
    formrow = output.TR()

    # Column 1: title of control
    formrow.append("<b>Authors:</b>")

    # Columns 2,3: our big, honkin' control.
    controlcell = output.TD(attrs={'colspan' : 2})
    table = output.TABLE(attrs={'bgcolor' : '#EEEEEE', 'width' : '100%',
                                'cellspacing' : 0, 'cellpadding' : 2,
                                'border' : 0})

    # -- documentation row
    row = output.TR(attrs={'bgcolor' : '#EEEEEE'})
    col = output.TD(attrs={'valign':'top', 'colspan':3})
    col.append("""<p><b>Instructions:</b>""")
    col.append("""The <i>display name</i> will appear in citations and
    descriptions of the tech report, and may be abbreviated as
    required (e.g., 'John Smith', 'Smith, John', or 'J. Smith'.  If
    the author has a page on the department website, then specify the
    person as the <i>corresponding person</i>.  This will create a
    hyperlink from the person's name to the relevant web page.</p><p>
    Contact <a href="mailto:webmaster@cs.uchicago.edu">webmaster@cs.uchicago.edu</a>
    for more information.</p>""") 
    row.append(col)
    table.append(row)

    # -- title row
    row = output.TR(attrs={'bgcolor' : '#BBBBBB'})
    row.append(output.TD(attrs={'valign':'top'},
                         sub="#"))
    row.append(output.TD(attrs={'valign':'top'},
                         sub="Display Name"))
    row.append(output.TD(attrs={'valign':'top'},
                         sub="Corresponding Person (if applicable)"))
    table.append(row)

    # Get the list of all people, for use in the select (below)
    people = display.get_cached_query_result('people')
    people.insert(0, ('Not on website', ''))

    # each of the rows..
    def box(num, name=''):
      return output.TEXT_INPUT(attrs={'value' : name,
                                      'name' : 'author_name_%d' % num,
                                      'size' : 35})
    def select(num, login='', options=people):
      return output.SELECT(
        name='author_login_%d' % num,
        attrs={'onchange' : "copy_to_name(%d, this.options[this.selectedIndex].text)" % num},
        options=options,
        default=login)
                           
    for num in range(1, num_rows+1):
      row = output.TR()
      row.append(str(num))
      if authors_prio.has_key(num):
        login, name = authors_prio[num]
        row.append(box(num,name))
        row.append(select(num,login))
      else:
        row.append(box(num, ''))
        row.append(select(num, ''))
      table.append(row)

    # finish things up!
    controlcell.append(table)
    formrow.append(controlcell)
    container.append(formrow)

  def input_form(self, display, form_fields):
    num = 1
    fd = display.req.form_data
    rv = []
    
    while fd.has_key('author_name_%d' % num):
      name = fd['author_name_%d' % num]
      login = fd['author_login_%d' % num]

      # If a login was selected, but no name, then fill in the name.
      # This duplicates the functionality of the JavaScript, for the
      # convenience of those who don't have JavaScript.
      if login and not name:
        q = queries.Select(tables="people",
                           where="login = %s" % queries.represent_value(login),
                           columns={'name' : "concat(fname, ' ', lname)"})
        q.execute()
        if q.count():
          name = q.fetch()['name']

      # We *must* have a name to produce output.
      if name:
        if login:
          rv.append((login, name, num))
        else:
          rv.append((None, name, num))

      # on to the next number.
      num += 1

    form_fields['authors'] = rv


####
# TRSearchControl
#
# Print that nice search form
    
class TRSearchControl(SingleControl):
  """In display mode, draws the search boxes."""
  def output_disp(self,display,nest_container):
    # Calculate the list of all current sponsors.  
    sponsor_list = display.get_cached_query_result('sponsors')
    sponsor_list.insert(0, ('Any',''))

    # Begin building the table of controls
    sponsor = html.SELECT(name='sponsor', options=sponsor_list, default=display.args['sponsor'])
    def label(value): return html.TD(attrs={'tag':'th','align':'right'},sub=[value])
    def box(name,display=display): return html.TEXT_INPUT({'name':name,'value':display.args[name]})
    submitbutton = html.SUBMIT_INPUT({'value' : 'Go'})
    resetbutton = html.BUTTON_INPUT({'value' : 'reset',
                                     'onClick' : """f=document.forms[0];
                                                    f.year.value='';
                                                    f.author.value='';
                                                    f.title.value='';
                                                    f.rep_id.value='';
                                                    f.sponsor.selectedIndex=0;
                                                    return true;"""})
    buttons = html.TD(sub=[submitbutton,resetbutton])
    table = html.TABLE(attrs={'cellpadding':'0','cellspacing':'0'},
                       sub=[html.TR(sub=[label('Year:'), box('year'),
                                         label('Identifier:'), box('rep_id')]),
                            html.TR(sub=[label('Author:'), box('author'),
                                         label('Sponsor:'), sponsor]),
                            html.TR(sub=[label('Title:'),
                                         box('title'),
                                         html.TD(sub=["&nbsp;"]),
                                         buttons])])
    nest_container.append(html.FORM([table],"%(script)s" % display.urls()))


####
# NoResultsControl

class NoResultsControl(SingleControl):
  """If there are no results (display.db_rows() == 0) then display a
  'nothing found' header."""
  def output_disp(self, display, container):
    if display.db_rows() == 0:
      container.append("<h2>No record found</h2>")


####
# NoCommentsControl

class NoCommentsControl(SingleControl):
  """If there are no comments (display.db_rows() == 0) then display a
  'nothing found' header."""
  def output_disp(self, display, container):
    if display.db_rows() == 0:
      container.append("<h2>This technical report has no comments</h2>")

####
# TRSupersededControl

class TRSupersededControl(IfThenElseControl):
  """Draw SUPERSEDED if this TR has been superseded.
     Otherwise draw NORMAL."""
  def __init__(self, normal=[], superseded=[]):
    IfThenElseControl.__init__(self, normal, superseded)
  def condition(self, display):
    debugging.log(str(display.fields['superseded_by']))
    return not display.fields['superseded_by']

####
# FieldPermissionControl

class FieldPermissionControl(IfThenElseControl):
  """Draw PERMITTED in DISP shape, and when the current user may edit
  the indicated field.  Otherwise draw NOT_PERMITTED."""
  def __init__(self, field, permitted=[], not_permitted=[]):
    IfThenElseControl.__init__(self, permitted, not_permitted)
    self.field = field

  def condition_form(self, display):
    return display.can_edit_field(self.field)

  def condition_disp(self, display):
    return 1                            # show permitted controls in
                                        # DISP shape
# shorthand
FPC = FieldPermissionControl

####
# TRCommentsLink

class TRCommentsLink(SingleControl):
  """Just make a link to the tech reports' comments when in FORM shape."""
  def output_form(self, display, container):
    tr = output.TR()
    tr.append("""<b>Comments:</b>""")
    tr.append(output.TD(attrs={'colspan' : 2},
                        sub="""<a href="%(full)s/comments">Edit</a> comments for this
                        tech report or <a href="%(full)s/comments/add">add</a> a
                        new comment.""" % display.req.urls))
    container.append(tr)
                                        

####
# TRDisplay

class TRDisplay(Display, Database, Arguments, FileCopy, Documents,
                Modal, CacheQueries):
  def __init__(self, req):
    Display.__init__(self, req)
    self.notifications = []

  def is_multiple(self):
    return self.db_rows()-1

  ####
  # Single Controls

  single_controls = [

    # rep_id
    
    "\n\n<h1>",
    FPC("rep_id",
        permitted=TextFieldControl("rep_id"),
        not_permitted=NoEditControl("rep_id", title="Identifier") ),
    "</h1>",

    # title

    "\n\n<h2>",
    FPC("title",
        permitted=TextFieldControl("title", title="Title", size=80),
        not_permitted=NoEditControl("title", title="Title")),
    "</h2>",

    # authors
    
    "\n\n<blockquote>\n",
    FPC("authors",
        permitted=AuthorsControl(),
        not_permitted=NoEditControl(None, title="Authors")),
    ". ",

    # publication date

    NoEditControl("text_date", title="Publication Date",
                  comment="The publication date is fixed."),
    ".<br>\n",

    # sponsor
    
    IfExistsControl("sponsor_name",
                    ["\n\nCommunicated by ",
                     SingleLinkControl("%(person!sponsor)s",
                                       NoEditControl("sponsor_name")),
                     ".<br>\n",]),
    FPC("sponsor",
        permitted=CommentControl("Sponsor-change code not written yet."),
        not_permitted=NoEditControl("sponsor_name",
                                    title="Sponsor",
                                    comment="""The sponsor cannot be changed.""")),

    # supersedes, et al.    

    IfExistsControl("supersedes",
                    ["\n<b>Supersedes:</b> ",
                     FPC("supersedes",
                         permitted=TimestampFieldControl("supersedes",
                                                         "supersedes_ts",
                                                         title="Supersedes"),
                         not_permitted=NoEditControl("supersedes",
                                                      title="Supersedes")),
                     "<br>"]),

    IfExistsControl("superseded_by",
                    ["\n<span style='color: red; font-size: 110%; line-height: 1.9em; border-bottom: medium solid #ff0000; font-weight: bold'>Superseded By:</span> ",
                     FPC("superseded_by",
                         permitted=TSFURLC("superseded_by","superseded_by_ts",title="Superseded By"),
                         not_permitted=NoEditControl("superseded_by",
                                                      title="Superseded By")),
                     "<br>"]),

    IfExistsControl("related",
                    ["\n<b>Related To:</b> ",
                     FPC("related",
                         permitted=TimestampFieldControl("related",
                                                         "related_ts",
                                                         title="Related To"),
                         not_permitted=NoEditControl("related",
                                                     title="Related To")),
                     "<br>"]),

    IfExistsControl("obsolete",
                    ["\n<b>Obsolete:</b> ",
                     FPC("obsolete",
                         permitted=TimestampBooleanControl("obsolete",
                                                           "obsolete_ts",
                                                           title="Obsolete"),
                         not_permitted=NoEditControl(None, "Obsolete")),
                     "<br>"]),

    "</blockquote>",

    # abstract

    FPC("abstract",
        permitted=IfExistsControl("abstract",
                                  [ "<h3>Abstract</h3>",
                                    TextAreaControl("abstract", title='Abstract',
                                                    rows=30, cols=90 ) ] ),
        not_permitted=NoEditControl(None, title="Abstract")),

    # authentic document

    TRSupersededControl(
    IfExistsControl("tr_authentic",
                    [ "\n\n<h3>Original Document</h3>",
                      "\n\n<blockquote>The original document is available in ",
                      FPC("tr_authentic",
                          permitted=SingleDocumentControl("tr_authentic",
                                                          title="Original Document",
                                                          show_timestamps=1),
                          not_permitted=NoEditControl(None,
                                                      title="Original Document")),
                      ".</blockquote>", ]),
    IfExistsControl("tr_authentic",
                    [ "\n\n<h3>Original Document</h3>",
                      "\n\n<blockquote>Please visit the new version: ",
                      URLControl("superseded_by"),
                      "<br />",
                      "<em>The obsoleted document is available in ",
                      FPC("tr_authentic",
                          permitted=SingleDocumentControl("tr_authentic",
                                                          title="Original Document",
                                                          show_timestamps=1),
                          not_permitted=NoEditControl(None,
                                                      title="Original Document")),
                      ".</em></blockquote>", ])),
                    
    # additional documents

    IfExistsControl("tr_additional",
                    [ "\n<h3>Additional Document Formats</h3>",
                      "\n<blockquote><p>The document is also available in ",
                      FPC("tr_additional",
                          permitted = [ DocumentControl("tr_additional",
                                                        title="Additional Document Formats",
                                                        show_timestamps=1), ],
                          not_permitted=NoEditControl(None,
                                                      title="Additional Documents")),
                      """.</p>\n\n<p><font color=red>NOTE</font>: The author
                      warrants that these additional documents are identical with the
                      originial to the extent permitted by the translation between the
                      various formats.  However, the webmaster has made no effort to
                      verify this claim.  If the authenticity of the document is an issue,
                      please always refer to the "Original document."  If you find
                      significant alterations, please report to
                      <a href="mailto:webmaster@cs.uchicago.edu">webmaster@cs.uchicago.edu</a>.
                      </p></blockquote>""" ]),

    # comments

    IfExistsControl("num_comments",
                    [ "\n\n<h3>Comments</h3>",
                      "<blockquote>",
                      LinkControl("num_comments", "%(full)s/comments", "Comments"),
                      TRCommentsLink(), # makes a link in FORM shape
                      " are available on this technical report.",
                      "</blockquote>" ] ),
    ]

  
  multiple_container_control = CoalesceContainerControl([
    StaticStringControl(key="techreport_adding"),
    "<h1>Technical Reports</h1>",
    TRSearchControl(),
    NoResultsControl(),
    SimpleContainerControl(html.DL)
    ])
  
  multiple_controls = [
    CoalesceControl([ LinkControl("rep_id",
                                         "%(techreport!rep_id)s",
                                         FieldControl("rep_id")), ]),  
    CoalesceControl([ FieldControl("title"), ". ",
                      AuthorsControl(), ". ",
                      FieldControl("text_date"), ". ",
                      IfExistsControl("sponsor_name",
                                      [ "Communicated by ",
                                        LinkControl("sponsor_name", "%(person!sponsor)s",
                                                    FieldControl("sponsor_name")),
                                        ". " ]), ]),
    ]


  cache_queries = {
    'sponsors' : queries.Select(tables=["tech_reports","people"],
                                where="login = sponsor", \
                                columns={ 'sys_value':'sponsor',
                                          'user_value':'concat(fname," ",lname)' },
                                order="lname",
                                distinct=1),
    'people' : queries.Select(tables="people",
                              where="retired != 1 and pseudo != 1",
                              columns={'sys_value' : 'login',
                                       'user_value' : "concat(fname, ' ', lname)"},
                              order=['lname', 'fname', 'mname']) }

  def process_arguments(self):
    self.args = self.process_search_arguments(argnames = ['rep_id','year','sponsor','author','title'])
    # Want to do a type check on year? - GMK
    
##  def process_arguments(self):
##    # We do something a little special here.  If we have form data, we parse it and
##    # then construct a URL out of it (in /n1/a1/n2/a2.. format) and bounce there.
##    if self.req.form_data and not self.req.url.arguments:
##      self.args = self.parse_arguments(['rep_id','year','sponsor','author','title'])
##      # We have form data, so get it and generate a new URL.
##      if self.args:
##        url = "%(script)s" % self.req.urls
##        for name, value in self.args.items():
##          if value:
##            url = url + "/%s/%s" % (name, value)
##      # if we've generatted anything interesting, bounce
##      if url != "%(script)s" % self.req.urls:
##        bounce.bounce(self.req, url)
##    else:
##      # otherwise, ignore form_data, because it might be input from an 'Edit' action.
##      self.args = self.parse_arguments(['rep_id','year','sponsor','author','title'],
##                                       form_data=0)

  def perform_action(self):
    self.process_arguments()
    self.prep_database()
    if self.permit_mode('edit'):
      rv = self.perform_database_update()
      self.send_notification()

  def make_page(self, page):
    self.process_arguments()
    self.prep_database()
    page.set_type("research")
    page.set_title("Technical Reports")
    if self.is_multiple():
      page.append(self.multiple_database())
      if perms.may(self.req.login, 'add', 'techreports'):
        page.add_navigation("%(techreport:add)s" % self.req.urls,
                            "Post Tech Report")
    else:
      self.add_modes_to_page(page)
      page.add_navigation("%(techreport:)s" % self.req.urls, "Tech Reports")
      # we need to know what a TRCommentDisplay thinks about this user adding
      # a comment.  So we create one and ask it.
      if TRCommentsDisplay(self.req).permit_action("add"):
        page.add_navigation("%(techreport!rep_id)s/comments/add" % self.req.urls(self.args),
                            "Add Comment")
      if self.get_mode() == 'edit':
        self.shape = shapes.FORM
      page.append(self.single_database())



  def permit_mode(self, mode):
    return ((mode == 'display') or      # display mode
            (mode == 'edit' and         # permitted to edit any field?
             self.can_edit_field('any')))

  field_permissions = {
    # field : (sponsor can?, author can?, short-term?*, admin can?* )
    #  * short-term means sponsor and author can, if within a short
    #    time of creating the tech report
    #  * admin means perms.can(self.req.login, 'admin', 'techreports')
    'rep_id' :             (0, 0, 0, 0),
    'title' :              (0, 0, 1, 1),
    'authors' :            (0, 0, 1, 1),
    'pub_date' :           (0, 0, 0, 0),
    'sponsor' :            (0, 0, 0, 0),
    'supersedes' :         (1, 1, 1, 1),
    'superseded_by' :      (1, 1, 1, 1),
    'related' :            (1, 1, 1, 1),
    'obsolete' :           (1, 1, 1, 1),
    'abstract' :           (0, 0, 1, 1),
    'tr_authentic' :       (0, 0, 1, 1),
    'tr_additional' :      (1, 1, 1, 1)
    }

  def can_edit_field(self, field):
    """Can the current user edit the designated field?  If FIELD is
    'any', can the designated user edit any field?"""
    # First gather (and cache) some characteristics of this request
    if not hasattr(self, 'is_sponsor'):
      self.is_sponsor = (self.req.login
                         and not self.is_multiple()
                         and self.req.login == self.fields['sponsor'])
    if not hasattr(self, 'is_author'):
      self.is_author = (self.req.login
                        and not self.is_multiple()
                        # use map to get the list of logins of the authors
                        and self.req.login in map(lambda x:x[0], self.fields['authors']))
    if not hasattr(self, 'is_short_term'):
      self.is_short_term = (hasattr(self, 'fields')
                            and (time.time() - self.fields['pub_date']) < short_term_seconds)
    if not hasattr(self, 'is_admin'):
      self.is_admin = perms.may(self.req.login, 'admin', 'techreports')

    # a helper function to check the permission matrix (see above)
    def can(matrix_row, is_sponsor, is_author, is_short_term, is_admin):
      return ((matrix_row[0] and is_sponsor) or
              (matrix_row[1] and is_author) or
              (matrix_row[2] and is_short_term and (is_sponsor or is_author)) or
              (matrix_row[3] and is_admin))

    # 'any' means look for all of them, and return true if any of them apply.
    if field == 'any':
      for _,matrix_row in self.field_permissions.items():
        if can(matrix_row, self.is_sponsor, self.is_author,
               self.is_short_term, self.is_admin): 
          return 1
      return 0

    # otherwise it's a specific field
    return can(self.field_permissions[field], self.is_sponsor,
               self.is_author, self.is_short_term, self.is_admin)  

  def db_rows(self):
    return self.query.count()

  def notify_field_changed(self, field, old=None, new=None, changes=None):
    message = "----\nField %s changed " % field
    if old is not None and new is not None:
      if old.find('\n') != -1 or new.find('\n') != -1:
        message += "from: \n%s\n to:\n%s" % (old, new)
      else:
        message += "from '%s' to '%s'" % (old, new)
    else:
      message += "as follows:\n%s" % changes
    self.notifications.append(message)

  def send_notification(self):
    if self.notifications:
      self.notifications.insert(0, "User '%s' edited tech report %s:"
                                % (self.req.login, self.form_fields['rep_id']))

      actions.notify('techreports', string.join(self.notifications, '\n'))

  def db_query(self):
    tables="tech_reports as t left join people as p on t.sponsor = p.login"
    wc = []                             # where clause

    # Do any matching specified by the search form.
    
    if self.args['author']:
      # If we're searching on author, left join that table and use it in
      # the 'where' clause, but leave it out of the columns returned
      # and use DISTINCT so we only get one copy of each row.
      tables = tables + " left join tech_report_authors as ta ON t.rep_id = ta.rep_id"
      self.args['author'] = string.lower(self.args['author'])
      wc.append("""(LOWER(ta.name) rlike %(author)s OR
                    LOWER(ta.login) rlike %(author)s)""" %
                { 'author' : queries.represent_value(self.args['author']) })
    if self.args['year']:
      wc.append('pub_date rlike %s' % queries.represent_value("^" + self.args['year']))
    if self.args['sponsor']:
      wc.append('sponsor = %s' % queries.represent_value(self.args['sponsor']))
    if self.args['rep_id']:
      wc.append('t.rep_id rlike %s' % queries.represent_value(self.args['rep_id']))
    if self.args['title']:
      wc.append('LOWER(title) rlike %s' % \
           queries.represent_value(string.lower(self.args['title'])))

    # convert to a string
    wc = string.join(wc, ' AND ')

    # These are the main columns we want
    columns={'rep_id' : 't.rep_id',
             'title' : 'title',
             'text_date' : 'date_format(pub_date, "%e %M, %Y")',
             'pub_date' : 'unix_timestamp(pub_date)',
             'sponsor' : 'sponsor',
             'sponsor_name' : "concat(p.fname, ' ', p.lname)",
             'related' : 'related',
             'related_ts' : 'UNIX_TIMESTAMP(related_ts)',
             'supersedes' : 'supersedes',
             'supersedes_ts' : 'UNIX_TIMESTAMP(supersedes_ts)',
             'superseded_by' : 'superseded_by',
             'superseded_by_ts' : 'UNIX_TIMESTAMP(superseded_by_ts)',
             'obsolete' : 'obsolete',
             'obsolete_ts' : 'UNIX_TIMESTAMP(obsolete_ts)',
             'abstract' : 'abstract' }
    order="t.pub_date desc, rep_id desc"
    q = queries.Select(tables=tables,
                       where=wc,
                       columns=columns,
                       order=order,
                       distinct=1)
    q.execute()

    # Now, even more special bouncing stuff (see process_arguments for the rest):
    # If there's a single result, and the argument portion of the URL is not
    # one element long, then we find out what tech report this is, and stick the
    # rep id (e.g., TR-2001-22) into the first argument with a bounce.  This lets
    # us be sure we're editing only one element.
    if q.count() == 1 and len(self.req.url.arguments) != 1:
      self.fields = q.fetch()
      url = "%(script)s/%(!rep_id)s" % self.req.urls(self.fields)
      bounce.bounce(self.req, url)
      
    self.query = q
    

  def db_fetch(self):
    # Fetch from the database
    self.fields = self.query.fetch()

    # Bail if we're done (fetch returned None)
    if (not self.fields):
      return None

    # Fetch the authors for this row
    tables = "tech_report_authors"
    wc = 'rep_id = "%s" ' % self.fields['rep_id']
    columns=['name', 'login', 'priority']
    order="priority asc"
    q = queries.Select(tables=tables,
                       where=wc,
                       columns=columns,
                       order=order)
    q.execute()
    self.fields['authors'] = []
    for author in q.fetchall():
      self.fields['authors'].append((author['login'],author['name'],author['priority']))
    
    # And find any documents for this report (we only do this in
    # single mode, because we don't use the info in multiple mode)
    if not self.is_multiple():
      self.db_fetch_documents('tr_authentic', self.fields['rep_id'])
      self.db_fetch_documents('tr_additional', self.fields['rep_id'])

    # In single mode only, get the number of comments
      q = queries.Select(tables='tech_report_comments',
                         where='rep_id = %s' % queries.represent_value(self.fields['rep_id']))
      q.execute()
      self.fields['num_comments'] = q.count()

    return self.fields

  def db_update_trivial(self, field_name, update, fields, form_fields):
    """For the trivial case of a field-update function, where it's just text
    which needs to get into the SQL UPDATE clause if it's changed.  Returns a
    boolean: true means the record was updated."""
    # See if we need to do an update
    if fields[field_name] == form_fields[field_name]:
      return None

    # Format a notification
    self.notify_field_changed(field_name, fields[field_name], form_fields[field_name])

    # Make the change
    update[field_name] = form_fields[field_name]

    return 1

  def db_update_title(self, update, fields, form_fields):
    self.db_update_trivial('title', update, fields, form_fields)

  def db_update_authors(self, update, fields, form_fields):
    # See if we need to do an update
    if fields['authors'] == form_fields['authors']:
      return

    # Format a notification
    def format_authors(authors):
      return string.join([ "%s (%s)" % (name, login)
                           for login, name, priority
                           in authors ], '; ')

    old_authors = format_authors(fields['authors'])
    new_authors = format_authors(form_fields['authors'])
    self.notify_field_changed('authors', old_authors, new_authors)

    # We lock down the table, delete every author for this report,
    # then re-add those we've received in self.form_fields.
    # self.form_fields['authors'] is a list of tuples (LOGIN, NAME, PRIORITY).
    lock = queries.Lock(tables='tech_report_authors', locktype="WRITE")
    lock.lock()

    try:
      rep_id = form_fields['rep_id']
      # Delete all of the old rows for this user
      d = queries.Delete(table='tech_report_authors',
                         where='rep_id = %s' % queries.represent_value(rep_id))
      d.execute()

      # make a list of new rows that should be inserted
      rows = []
      for login, name, priority in self.form_fields['authors']:
        rows.append({'rep_id' : rep_id,
                     'name' : name,
                     'login' : login,
                     'priority' : priority})

      # And now insert the rows
      if rows:
        i = queries.Insert(table='tech_report_authors', rows=rows)
        i.execute()
      
    finally:
      lock.unlock()

  def db_update_supersedes(self, update, fields, form_fields):
    if self.db_update_trivial('supersedes', update, fields, form_fields):
      update['supersedes_ts'] = queries.SQL('NOW()')

  def db_update_superseded_by(self, update, fields, form_fields):
    if self.db_update_trivial('superseded_by', update, fields, form_fields):
      update['superseded_by_ts'] = queries.SQL('NOW()')

  def db_update_related(self, update, fields, form_fields):
    if self.db_update_trivial('related', update, fields, form_fields):
      update['related_ts'] = queries.SQL('NOW()')

  def db_update_obsolete(self, update, fields, form_fields):
    old_obs = fields['obsolete']
    new_obs = form_fields['obsolete']

    # See if we need to do an update
    if old_obs == new_obs:
      return None

    def yn(bool): return (bool and "yes") or "no"

    # Format a notification
    self.notify_field_changed('obsolete', yn(old_obs), yn(new_obs))

    # Make the change
    update['obsolete'] = new_obs
    update['obsolete_ts'] = queries.SQL('NOW()')

  def db_update_abstract(self, update, fields, form_fields):
    self.db_update_trivial('abstract', update, fields, form_fields)
    
  def db_update_tr_authentic(self, update, fields, form_fields):
    changes = self.db_update_documents('tr_authentic', fields['rep_id'])
    if changes:
      self.notify_field_changed('tr_authentic', changes=changes)

  def db_update_tr_additional(self, update, fields, form_fields):
    changes = self.db_update_documents('tr_additional', fields['rep_id'])
    if changes:
      self.notify_field_changed('tr_additional', changes=changes)

  def db_update(self):
    # if we don't *know* we have a unique record, fail.
    if len(self.req.url.arguments) != 1:
      # (this shouldn't ever happen, given the bouncing that happens in db_query,
      # so I won't worry about the clarity of this error message too much)
      raise WebError("""Incompletely Specified Report""",
                     """You must edit a specific tech report.""")

    rep_id = self.args['rep_id']
    form_fields = self.form_fields
    form_fields['rep_id'] = rep_id

    update = queries.Update(table="tech_reports",
                            where="rep_id = %s" % queries.represent_value(rep_id))

    # Now, for each field, check permissions and call
    # db_update_<fieldname> if the user has permission to do so.
    #
    # Note that each of these db_update_<fieldname> functions checks the old value
    # and the new value of the field.  This presents something of a race condition.
    # I don't think it will ever happen, though ;-)
    for field in self.field_permissions.keys():
      if self.can_edit_field(field):
        apply(getattr(self, "db_update_" + field), (update, self.fields, self.form_fields))

    if update.sets:
      update.execute()
    
#######################################################################
# NewTRDisplay
#
# A whole new Display class

class NewTRDisplay(Display, Database, Documents, FileCopy, Modal, CacheQueries):
  single_controls = [
    NoEditControl("rep_id", title="Identifier",
                  comment="""The Identifier will be assigned
                  automatically when you click 'Commit Changes'."""),

    TextFieldControl("title", title="Title", size=80),

    AuthorsControl(),

    NoEditControl("pub_date", title="Publication Date",
                  comment="""The publication date will be set
                  when you click 'Commit Changes'."""),

    NoEditControl("sponsor_name",
                  title="Sponsor",
                  comment="""By adding this tech report, you
                  automatically become its sponsor."""),

    TimestampFieldControl("supersedes",
                          "supersedes_ts",
                          title="Supersedes"),

    TimestampFieldControl("superseded_by",
                          "superseded_by_ts",
                          title="Superseded By"),

    TimestampFieldControl("related",
                          "related_ts",
                          title="Related To"),

    TimestampBooleanControl("obsolete",
                            "obsolete_ts",
                            title="Obsolete"),

    CommentControl("""<p>We <b>require</b> that every tech-report
    begin with an Abstract as part of the document.  In addition, we
    <b>require</b> that the author furnish a copy of the Abstract in
    plain text format for display in the "Abstract" box, below.
    (Techstaff will help you if you have trouble converting the
    abstract to this format.)  This copy must be identical with the
    Abstract which is part of the document (to the extent translation
    of formulas between different typesetting languages permits).</p>
    
    <p>We <b>suggest</b> that while writing the Abstract, the author
    bear in mind that the Abstract may be the only part of the paper
    read by most readers.  Therefore the Abstract should be
    self-contained (no references to undefined terms; literature items
    need to be cited in detail), and understandable by a broad
    audience.  It is not necessary to state the results in their most
    general form.</p>
    
    <p>It is the author's responsiblity that the visible copy of the
    Abstract (in the "Abstract" box) be identical with the Abstract
    included in the document.</p>"""),

    HTMLEditorControl("abstract", None, title='Abstract',
                    rows=12, cols=90, caption="Plain text only" ),

    SingleDocumentControl("tr_authentic",
                          title="Original Document",
                          show_timestamps=1),
    
    CommentControl("""By entering additional formats of the document,
    the author represents that they are identical with the original
    version."""),

    DocumentControl("tr_additional",
                    title="Additional Document Formats",
                    show_timestamps=1), 
    ]

  def permission_error(self):
    raise WebError("Permission Denied",
                   """You do not have permission to add a tech report. In general, only
                   CS faculty may actually add a tech report (thus becoming the tech
                   report's sponsor).""")
    
  def perform_action(self):
    if self.permit_action('add'):
      self.perform_database_insert()
      self.send_notification()
    else:
      self.permission_error()
    return "%(script)s/added/%(!rep_id)s" % self.req.urls({'rep_id' : self.new_rep_id})

  def make_page(self, page):        
    if not self.permit_action('add'):
      self.permission_error()

    page.append("<h1>Post New Tech Report</h1>")
    page.set_title("Post New Tech Report")
    page.add_navigation("%(techreport:)s" % self.req.urls, "Tech Reports")

    # If we're to display the 'warning: you have 24 hours' page..
    if self.req.url.arguments[0] == 'added':
      new_rep_id = self.req.url.arguments[1]
      page.append("<p>Tech report %s has been succesfully posted.</p>" % new_rep_id)
      page.append("""<p>You and any authors with site logins now have
      24 hours to make changes to the record.  After 24 hours, any
      changes must be made by the tech report overseer or the website
      administrators.  Contact <a
      href="mailto:webmaster@cs.uchicago.edu">webmaster@cs.uchicago.edu</a>
      for more information.</p>""")
      page.append("""<p>Click <a href="%(techreport!rep_id)s">here</a>
      to see the newly added tech report.</p>""" %
      self.req.urls({'rep_id' : new_rep_id}))
    else:
      self.shape = shapes.FORM            # always form shape
      page.append(self.single_database())

  def permit_action(self, mode):
    return mode == 'add' and perms.may(self.req.login, 'add', 'techreports')

  cache_queries = {
    'people' : queries.Select(tables="people",
                              where="retired != 1 and pseudo != 1",
                              columns={'sys_value' : 'login',
                                       'user_value' : "concat(fname, ' ', lname)"},
                              order=['lname', 'fname', 'mname'])
    }
                    
  def db_query(self):
    pass                                # nothing to query

  def db_fetch(self):
    # This isn't really a fetch so much as a bunch of defaults..
    self.fields = {}
    self.fields['rep_id'] = 'To Be Determined'
    self.fields['title'] = ''
    self.fields['authors'] = []
    self.fields['pub_date'] = 'To Be Determined'

    # Get the user's full name
    s = queries.Select(tables="people",
                       columns={'name' : "CONCAT(fname, ' ', lname)"},
                       where="login = %s" % queries.represent_value(self.req.login))    
    s.execute()
    r = s.fetch()
    if r:
      self.fields['sponsor_name'] = r['name']
    else:
      raise WebError("Internal Error", "Cannot find your full name in the database")

    self.fields['supersedes'], self.fields['supersedes_ts'] = '', ''
    self.fields['superseded_by'], self.fields['superseded_by_ts'] = '', ''
    self.fields['related'], self.fields['related_ts'] = '', ''
    self.fields['obsolete'], self.fields['obsolete_ts'] = None, ''
    self.fields['abstract'] = ''
    self.fields['tr_authentic'] = []
    self.fields['tr_additional'] = []
    return self.fields

  def get_next_rep_id(self):
    # assumes the tech_reports table is locked.
    year = time.localtime(time.time())[0]
    rep_id_stub = 'TR-%04d-%%' % year
    # Get MySQL to do the calculation for us..
    s = queries.Select(tables="tech_reports",
                       where="rep_id like %s" % queries.represent_value(rep_id_stub),
                       order = 'rep_id desc',
                       limit=1,
                       columns = { 'next_id' : 'substring(rep_id, 9,2)+1' })
    s.execute()
    row = s.fetch()
    if row:
      next_id = row['next_id']
    else:
      next_id = 1

    new_rep_id = 'TR-%04d-%02d' % (year, next_id)
    return new_rep_id
    
  def db_check_title(self): return self.form_fields['title']
  def db_check_authors(self): return self.form_fields['authors']
  def db_check_abstract(self): return self.form_fields['abstract']
  def db_check_original(self): return self.form_fields['tr_authentic']

  def db_insert(self):
    # Insert self.form_fields into the database.

    # We've got a bunch of consistency checks to do first, though.
    checks = [
      (self.db_check_title, "Tech reports must have a full title."),
      (self.db_check_authors, "Tech reports must have at least one author."),
      (self.db_check_abstract, """Every new tech report is
      <i>required</i> to have an <i>exact copy</i> of the abstract in
      the paper entered in the abstract field."""),
      (self.db_check_original, "A new tech report must have an original document."),
      ]

    errs = []
    for fn, err in checks:
      if not fn(): errs.append(err)

    if errs:
      raise WebError("Tech Report Submission Errors",
                     "<ul>" +
                     string.join(["<li>%s" % err for err in errs], '\n') +
                     """</ul>
                     Please click the <i>Back</i> button and try again.""")
                     
    # OK, I guess now you can go ahead and add it.
    # lock tables first
    locker = queries.Lock(tables=["tech_reports", "tech_report_authors"])
    locker.lock()

    try:
      # get a new rep_id
      new_rep_id = self.get_next_rep_id()

      # now start inserting stuff
      tech_reports = queries.Insert(table="tech_reports")
      tech_report_authors = queries.Insert(table="tech_report_authors")
      tech_reports.row({'rep_id' : new_rep_id,
                        'title' : self.form_fields['title'],
                        'pub_date' : queries.SQL('NOW()'),
                        'abstract' : self.form_fields['abstract'],
                        'sponsor' : self.req.login,
                        'supersedes' : self.form_fields['supersedes'],
                        'superseded_by' : self.form_fields['superseded_by'],
                        'obsolete' : self.form_fields['obsolete'],
                        'related' : self.form_fields['related'],
                        'supersedes_ts' : queries.SQL('NOW()'),
                        'superseded_by_ts' : queries.SQL('NOW()'),
                        'obsolete_ts' : queries.SQL('NOW()'),
                        'related_ts' : queries.SQL('NOW()'),
                        })

      for login, name, priority in self.form_fields['authors']:
        tech_report_authors.row({'rep_id' : new_rep_id,
                                 'name' : name,
                                 'login' : login,
                                 'priority' : priority})

      # Do the database manipulations themselves
      tech_reports.execute()
      tech_report_authors.execute()
    finally:
      locker.unlock()

    # These don't need to be inside the lock
    self.db_update_documents('tr_authentic', new_rep_id)    
    self.db_update_documents('tr_additional', new_rep_id)

    # send this back to perform_action
    self.new_rep_id = new_rep_id

  def send_notification(self):
    # Send an overseer notification for the current tech report (in self.form_fields)
    message = self.file_contents(
      os.path.join(docroot, "research", "publications", "tr_notification.txt"),
      { 'user' : self.req.login,
        'rep_id' : self.new_rep_id,
        'author_string' : string.join(['%s (%s)' % (name, login)
                                       for login, name, priority
                                       in self.form_fields['authors'] ], '; '),
        'text_date' : time.strftime("%d %B %Y", time.localtime(time.time())),
        'supersedes' : self.form_fields['supersedes'],
        'superseded_by' : self.form_fields['superseded_by'],
        'related' : self.form_fields['related'],
        'obsolete_str' : (self.form_fields['obsolete'] and "yes") or "no",
        'abstract' : self.form_fields['abstract'],
        'tr_authentic' : self.repr_documents('tr_authentic', self.new_rep_id),
        'tr_additional' : self.repr_documents('tr_additional', self.new_rep_id),
       })
    actions.notify('techreports', message)

  def is_multiple(self):
    return None                         # never multiple

  

########################################################################
# TRCommentsDisplay
#
# A whole new Display class

class TRCommentsDisplay(Display, Database, Arguments, Documents, Modal, CacheQueries):
  def process_arguments(self):
    if not hasattr(self, 'args'):
      self.args = self.parse_arguments(['rep_id', 'comments', 'comment_id', 'added'])
    
  def perform_action(self):
    self.process_arguments()
    self.prep_database()
    
    if (self.req.url.internal.has_key('delete')):
      if self.permit_action('delete'):
        deleted_comment_id = self.req.url.internal['delete'][0]
        # Fetch the old data
        s = queries.Select(tables = 'tech_report_comments',
                           where = 'id = %s' % queries.represent_value(deleted_comment_id))
        s.execute()
        row = s.fetch()
        if row:
          # Notify
          self.notify(action='deleted', user=self.req.login,
                      rep_id=row['rep_id'], title=row['title'],
                      note=row['note'],
                      documents=self.repr_documents('tr_comments',
                                                    deleted_comment_id))
          # and delete
          self.delete_comment(deleted_comment_id)
        return "%(techreport!rep_id)s/comments" % self.req.urls(self.args)
      else:
        raise WebError("Permission Denied",
                       "You do not have permission to delete this comment.")
    elif self.args['comment_id'] == 'add':
      if self.permit_action('add'):
        self.perform_database_insert()
        self.notify(action='added', user=self.req.login,
                    rep_id=self.args['rep_id'], title=self.form_fields['title'],
                    note=self.form_fields['note'],
                    documents=self.repr_documents('tr_comments',
                                                  self.args['comment_id']))
        # Return a URL with '/added' at the end of it, so we can tag the next page
        # with a 'that was successful' note.
        if self.args['added']:
          return "%(path)s" % self.req.urls
        else:
          return "%(path)s/added" % self.req.urls
      else:
        raise WebError("Permission Denied",
                       "You do not have permission to add a comment.")
    elif self.permit_mode('edit'):
      self.perform_database_update()
      self.notify(action='edited', user=self.req.login,
                  rep_id=self.args['rep_id'], title=self.form_fields['title'],
                  note=self.form_fields['note'],
                  documents=self.repr_documents('tr_comments',
                                                self.args['comment_id']))
    else:
      raise WebError("Permission Denied",
                     "You do not have permission to edit this comment.")

  def notify(self, **fields):
    # fields should have keys action, user, rep_id, title, note, documents
    actions.notify("techreports",
"""User '%(user)s' %(action)s a comment for tech report %(rep_id)s.

Title: %(title)s
Note: %(note)s
Documents:
%(documents)s""" % fields)

  def permit_mode(self, mode):
    return ((mode == 'display') or
            (mode == 'edit' and
             (self.is_author_or_sponsor(self.req.login) or
              self.is_overseer(self.req.login) or
              perms.may(self.req.login, 'admin', 'techreports'))))

  def permit_action(self, action):
    self.process_arguments()
    return ((action == 'delete' and
             (self.is_author_or_sponsor(self.req.login) or
              self.is_overseer(self.req.login) or
              perms.may(self.req.login, 'admin', 'techreports'))) or
            (action == 'add' and
             (self.is_author_or_sponsor(self.req.login) or
              self.is_overseer(self.req.login) or
              perms.may(self.req.login, 'admin', 'techreports'))))

  def make_page(self, page):
    self.process_arguments()
    self.prep_database()

    # Multiple Comments
    if self.is_multiple():
      self.add_modes_to_page(page)
      page.append("<h1>" + self.get_multiple_title() + "</h1>")
      page.set_title(self.get_multiple_title())
      page.add_navigation("%(techreport:)s" % self.req.urls, "Tech Reports")
      page.add_navigation("%(techreport!rep_id)s" % self.req.urls(self.args),
                          self.args['rep_id'])
      if self.permit_action('add'):
        page.add_navigation("%(techreport!rep_id)s/comments/add" % self.req.urls(self.args),
                            "Add Comment")
      page.append(self.multiple_database())
    # Add a Comment
    elif self.is_add():
      self.shape = shapes.FORM
      title = "Add a Comment for Tech Report " + self.args['rep_id']
      page.set_title(title)
      if self.args['added']:            # add a nice note if we just successfully added.
        page.append("""\
        <h2><font color=red>Comment Added Successfully</font></h2>
        <blockquote>If desired, you may add another below. 
        Otherwise, return to the <a href="%(techreport!rep_id)s/comments">list
        of comments</a>.</blockquote>""" % self.req.urls(self.args))
      page.append("<h1>" + title + "</h1>")
      page.add_navigation("%(techreport:)s" % self.req.urls, "Tech Reports")
      page.add_navigation("%(techreport!rep_id)s" % self.req.urls(self.args),
                          self.args['rep_id'])
      page.add_navigation("%(techreport!rep_id)s/comments" % self.req.urls(self.args),
                          "Comments")
      page.append(self.single_database())
    # Single Comment
    else:
      self.add_modes_to_page(page)
      page.set_title(self.get_single_title())
      page.add_navigation("%(techreport:)s" % self.req.urls, "Tech Reports")
      page.add_navigation("%(techreport!rep_id)s" % self.req.urls(self.args),
                          self.args['rep_id'])
      page.add_navigation("%(techreport!rep_id)s/comments" % self.req.urls(self.args),
                          "Comments")
      if self.get_mode() == 'edit':
        self.shape = shapes.FORM
      page.append(self.single_database())

  def get_multiple_title(self):
    return "Comments For " + self.args['rep_id']

  multiple_container_control = CoalesceContainerControl([
    NoCommentsControl(),                # helpful message if there are no rows.
    SimpleContainerControl(html.DL),
    ])

  multiple_controls = [
    # All of these are inside of a <dl> tag, so the CoalesceControls
    # group stuff into either the <dt> or <dd> tags.

    # <dt>
    CoalesceControl(
    [ "<b>", FieldControl('title'), "</b>",
      IfModeControl("edit",
                    [ " [&nbsp;",
                      LinkControl("id",
                                  "%(techreport!rep_id)s/comments/%(!id)s",
                                  "Edit&nbsp;This&nbsp;Comment"),
                      "&nbsp;] [&nbsp;",
                      LinkControl("id",
                                  "%(action:)s&delete=%(!id)s",
                                  "Delete&nbsp;This&nbsp;Comment"),
                      "&nbsp;]",
                      ] ),
      ]),

    # <dd>
    CoalesceControl(
    [ "Posted by ",
      LinkControl("author",
                  "%(person!author)s",
                  FieldControl('author_name')), "<br>\n",
      "Posted on ", FieldControl('creation_date'), "<br>\n",
      IfExistsControl('note', ["<p>",
                               FieldControl('note'),
                               "</p>"]),
      IfExistsControl("tr_comments",
                      [ "Attachment available in ",
                        DocumentControl('tr_comments', title="Associated Documents",
                                        show_timestamps=1),
                        "." ]),
      ])
    ]

  def get_single_title(self):
    return 'Comment For ' + self.fields['rep_id'] + ': ' + self.fields['title']

  single_controls = [
    "<h1>Comment For ",
    NoEditControl("rep_id", title="Tech Report Identifier"), ": ", 
    TextFieldControl("title", title="Comment Title"),
    "</h1>",

    "<b>Posted By:</b> ",
    SingleLinkControl("%(person!author)s",
                      NoEditControl("author_name")),
    IfShapeControl(formcontrols=NoEditControl("author_name", title="Creator")),
    "<br>",
    "<b>Posted On:</b> ",
    NoEditControl("creation_date", title="Creation Date", comment="""\
    <font color=red>NOTE</font>: Editing a comment
    will automatically set the <b>Creation Date</b> to the date the modifications
    were made.  If you wish to make modifications that should not alter the Creation
    Date (such as correction of a typo), please contact the tech report overseer or
    <a href="mailto:webmaster@cs.uchicago.edu">webmaster@cs.uchicago.edu</a>."""),
    "<br>",
    "<blockquote>", TextAreaControl('note', title="Note"),
    "</blockquote>",
    IfExistsControl("tr_comments",
                    [ "Attachment available in ",
                      DocumentControl('tr_comments', title="Associated Documents",
                                      show_timestamps=1),
                      "." ]),
                      
    ]

  def is_author_or_sponsor(self, login):
    """Is LOGIN a sponsor or author of this TR?"""
    if not hasattr(self, '_responsible_logins'):
      # Get the authors
      s = queries.Select(tables="tech_report_authors",
                         where="""login is not NULL AND
                         rep_id = %s""" % queries.represent_value(self.args['rep_id']),
                         columns=['login'])
      s.execute()
      rv = [ row['login'] for row in s.fetchall() ]

      # and the sponsor
      s = queries.Select(tables="tech_reports",
                         where="rep_id = %s" % queries.represent_value(self.args['rep_id']),
                         columns=['sponsor'])
      s.execute()
      row = s.fetch()
      if row: rv.append(row['sponsor'])

      self._responsible_logins = rv

    return login in self._responsible_logins

  def is_overseer(self, login):
    if not hasattr(self, '_overseers'):
      self._overseers = [ ov['login'] for ov in actions.lookup_overseers('techreports') ]
    return login in self._overseers

  def is_multiple(self):
    if self.args['comment_id']:
      return None
    return 1

  def is_add(self):
    if self.args['comment_id'] == 'add' and self.permit_action('add'):
      return 1
    return None

  def db_query(self):
    # There's no querying to be done if this is an add.
    if self.is_add(): return
    
    wc = "tech_report_comments.author = people.login"
    wc += " AND rep_id = %s" % queries.represent_value(self.args['rep_id'])
    if self.args['comment_id']:
      wc += " AND id = %s" % queries.represent_value(self.args['comment_id'])
    q = queries.Select(tables="tech_report_comments, people",
                       where=wc,
                       order='created',
                       columns={'rep_id' : 'rep_id',
                                'id' : 'id',
                                'creation_date' : 'DATE_FORMAT(created, "%e %M, %Y %T")',
                                'author' : 'author',
                                'author_name' : 'CONCAT(fname, " ", lname)',
                                'title' : 'title',
                                'note' : 'note'}
                       )
    q.execute()
    # if it's not found in single mode, give up.
    if not self.is_multiple() and q.count() == 0:
      raise WebError("Comment Not Found",
                     """The specified comment does not exist in the database.""")
    self.query = q

  def db_rows(self):
    return self.query.count()

  def db_defaults(self):
    s = queries.Select(tables="people",
                       where="login = %s" % queries.represent_value(self.req.login),
                       columns = { 'name' : 'CONCAT(fname, " ", lname)',
                                   'date' : 'DATE_FORMAT(NOW(), "%e %M, %Y")', }
                       )
    s.execute()
    row = s.fetch()

    # Set up some reasonable defaults.
    self.fields = { 'rep_id' : self.args['rep_id'],
                    'id' : 'ADD',
                    'creation_date' : row and row['date'] or 'Today',
                    'author' : self.req.login,
                    'author_name' : row and row['name'] or 'Your Name',
                    'title' : '',
                    'note' : '',
                    'tr_comments' : [] }

    return self.fields

  def db_fetch(self):
    # if this is an add, return the defaults.
    if self.is_add(): return self.db_defaults()
    
    # Fetch from the database
    self.fields = self.query.fetch()
    if self.fields:
      self.db_fetch_documents('tr_comments', self.fields['id'])

    return self.fields

  def db_update(self):
    # put changes to the database
    u = queries.Update(table="tech_report_comments",
                       where="rep_id = %s and id = %s"
                       % (queries.represent_value(self.args['rep_id']),
                          queries.represent_value(self.args['comment_id'])))
    u['title'] = self.form_fields['title']
    u['note'] = self.form_fields['note']
    # if there was a change, and the user is an author or sponsor,
    # update the date stamp.
    if ((self.fields['title'] != self.form_fields['title'] or
         self.fields['note'] != self.form_fields['note']) and
        self.is_author_or_sponsor(self.req.login) and
        not self.is_overseer(self.req.login)):
      u['created'] = queries.SQL('NOW()')
    u.execute()

    self.db_update_documents('tr_comments', self.fields['id'])

  def db_insert(self):
    # put changes to the database
    i = queries.Insert(table="tech_report_comments")
    i.row({ 'rep_id' : self.args['rep_id'],
            'created' : queries.SQL('NOW()'),
            'author' : self.req.login,
            'title' : self.form_fields['title'],
            'note' : self.form_fields['note'] })
    i.execute()

    # Tuck the new ID somewhere that perform_action can find it.
    self.args['comment_id'] = i.insert_id()

    self.db_update_documents('tr_comments', self.args['comment_id'])

  def delete_comment(self, comment_id):
    d = queries.Delete(table="tech_report_comments",
                       where="rep_id = %s and id = %s" %
                       (queries.represent_value(self.args['rep_id']),
                        queries.represent_value(comment_id)))
    d.execute()
    self.db_delete_documents('tr_comments', comment_id)
                                                

def new(req):
  # Do a bunch of multiplexing to decide which display class to use.  We
  # can either give the user the 'add tech report' display class, the regular
  # techreport class (single and multiple) or the comments display class.
  if len(req.url.arguments) >= 1 and req.url.arguments[0] in ('add', 'added'):
    return NewTRDisplay(req)
  if len(req.url.arguments) >= 2 and req.url.arguments[1] == 'comments':
    return TRCommentsDisplay(req)
  else:
    return TRDisplay(req)