socorro r19 - in trunk: . webapp webapp/Socorro.egg-info webapp/socorro/config webapp/socorro/controllers webapp/socorro/lib webapp/socorro/models webapp/socorro/public/css webapp/socorro/templates webapp/socorro/templates/includes webapp/socorro/templates/report webapp/socorro/templates/topcrasher



Author: fherrera
Date: Fri Aug 29 15:36:44 2008
New Revision: 19
URL: http://svn.gnome.org/viewvc/socorro?rev=19&view=rev

Log:
2008-08-29  Fernando Herrera  <fherrera novell com>

        * webapp/Socorro.egg-info/PKG-INFO:
        * webapp/Socorro.egg-info/SOURCES.txt:
        * webapp/Socorro.egg-info/requires.txt:
        * webapp/setup.py:
        * webapp/socorro/config/routing.py:
        * webapp/socorro/controllers/report.py:
        * webapp/socorro/controllers/topcrasher.py:
        * webapp/socorro/lib/base.py:
        * webapp/socorro/lib/collect.py:
        * webapp/socorro/lib/helpers.py:
        * webapp/socorro/lib/monitor.py:
        * webapp/socorro/lib/platforms.py:
        * webapp/socorro/lib/processor.py:
        * webapp/socorro/lib/queryparams.py:
        * webapp/socorro/models/__init__.py:
        * webapp/socorro/public/css/layout.css:
        * webapp/socorro/public/css/style.css:
        * webapp/socorro/templates/includes/common.html:
        * webapp/socorro/templates/includes/site.html:
        * webapp/socorro/templates/query_form.html:
        * webapp/socorro/templates/report/index.html:
        * webapp/socorro/templates/report/list.html:
        * webapp/socorro/templates/topcrasher/bybranch.html:
        * webapp/socorro/templates/topcrasher/byversion.html:
        * webapp/socorro/templates/topcrasher/index.html: Update to lastest
        socorro sources from 2008-08-29 from http://socorro.googlecode.com/svn
        revision 472




Modified:
   trunk/ChangeLog
   trunk/webapp/Socorro.egg-info/PKG-INFO
   trunk/webapp/Socorro.egg-info/SOURCES.txt
   trunk/webapp/Socorro.egg-info/requires.txt
   trunk/webapp/setup.py
   trunk/webapp/socorro/config/routing.py
   trunk/webapp/socorro/controllers/report.py
   trunk/webapp/socorro/controllers/topcrasher.py
   trunk/webapp/socorro/lib/base.py
   trunk/webapp/socorro/lib/collect.py
   trunk/webapp/socorro/lib/helpers.py
   trunk/webapp/socorro/lib/monitor.py
   trunk/webapp/socorro/lib/platforms.py
   trunk/webapp/socorro/lib/processor.py
   trunk/webapp/socorro/lib/queryparams.py
   trunk/webapp/socorro/models/__init__.py
   trunk/webapp/socorro/public/css/layout.css
   trunk/webapp/socorro/public/css/style.css
   trunk/webapp/socorro/templates/includes/common.html
   trunk/webapp/socorro/templates/includes/site.html
   trunk/webapp/socorro/templates/query_form.html
   trunk/webapp/socorro/templates/report/index.html
   trunk/webapp/socorro/templates/report/list.html
   trunk/webapp/socorro/templates/topcrasher/bybranch.html
   trunk/webapp/socorro/templates/topcrasher/byversion.html
   trunk/webapp/socorro/templates/topcrasher/index.html

Modified: trunk/webapp/Socorro.egg-info/PKG-INFO
==============================================================================
--- trunk/webapp/Socorro.egg-info/PKG-INFO	(original)
+++ trunk/webapp/Socorro.egg-info/PKG-INFO	Fri Aug 29 15:36:44 2008
@@ -1,6 +1,6 @@
 Metadata-Version: 1.0
 Name: Socorro
-Version: 0.1adev-r2
+Version: 0.1adev-r352
 Summary: Breakpad Server
 Home-page: http://code.google.com/p/socorro/
 Author: Robert Sayre

Modified: trunk/webapp/Socorro.egg-info/SOURCES.txt
==============================================================================
--- trunk/webapp/Socorro.egg-info/SOURCES.txt	(original)
+++ trunk/webapp/Socorro.egg-info/SOURCES.txt	Fri Aug 29 15:36:44 2008
@@ -24,6 +24,7 @@
 socorro/controllers/error.py
 socorro/controllers/query.py
 socorro/controllers/report.py
+socorro/controllers/status.py
 socorro/controllers/symbols.py
 socorro/controllers/topcrasher.py
 socorro/docs/index.txt
@@ -49,15 +50,82 @@
 socorro/lib/simplejson/jsonfilter.py
 socorro/lib/simplejson/scanner.py
 socorro/models/__init__.py
+socorro/public/calendar.png
 socorro/public/favicon.ico
 socorro/public/footer_bg.png
 socorro/public/header_bg.png
+socorro/public/loading.png
+socorro/public/css/datePicker.css
 socorro/public/css/layout.css
 socorro/public/css/style.css
+socorro/public/css/flora/flora.accordion.css
+socorro/public/css/flora/flora.all.css
+socorro/public/css/flora/flora.calendar.css
+socorro/public/css/flora/flora.css
+socorro/public/css/flora/flora.datepicker.css
+socorro/public/css/flora/flora.dialog.css
+socorro/public/css/flora/flora.menu.css
+socorro/public/css/flora/flora.resizable.css
+socorro/public/css/flora/flora.slider.css
+socorro/public/css/flora/flora.tablesorter.css
+socorro/public/css/flora/flora.tabs.css
+socorro/public/css/flora/flora.toasterLite.css
+socorro/public/css/flora/flora.toolbar.css
+socorro/public/css/flora/i/accordion-left-act.png
+socorro/public/css/flora/i/accordion-left-over.png
+socorro/public/css/flora/i/accordion-left.png
+socorro/public/css/flora/i/accordion-middle-act.png
+socorro/public/css/flora/i/accordion-middle-over.png
+socorro/public/css/flora/i/accordion-middle.png
+socorro/public/css/flora/i/accordion-right-act.png
+socorro/public/css/flora/i/accordion-right-over.png
+socorro/public/css/flora/i/accordion-right.png
+socorro/public/css/flora/i/asc.gif
+socorro/public/css/flora/i/bg.gif
+socorro/public/css/flora/i/desc.gif
+socorro/public/css/flora/i/dialog-e.gif
+socorro/public/css/flora/i/dialog-n.gif
+socorro/public/css/flora/i/dialog-ne.gif
+socorro/public/css/flora/i/dialog-nw.gif
+socorro/public/css/flora/i/dialog-s.gif
+socorro/public/css/flora/i/dialog-se.gif
+socorro/public/css/flora/i/dialog-sw.gif
+socorro/public/css/flora/i/dialog-title.gif
+socorro/public/css/flora/i/dialog-titlebar-close-hover.png
+socorro/public/css/flora/i/dialog-titlebar-close.png
+socorro/public/css/flora/i/dialog-w.gif
+socorro/public/css/flora/i/menu-submenu.gif
+socorro/public/css/flora/i/resizable-e.gif
+socorro/public/css/flora/i/resizable-n.gif
+socorro/public/css/flora/i/resizable-ne.gif
+socorro/public/css/flora/i/resizable-nw.gif
+socorro/public/css/flora/i/resizable-s.gif
+socorro/public/css/flora/i/resizable-se.gif
+socorro/public/css/flora/i/resizable-sw.gif
+socorro/public/css/flora/i/resizable-w.gif
+socorro/public/css/flora/i/shadow.png
+socorro/public/css/flora/i/slider-bg-1.png
+socorro/public/css/flora/i/slider-bg-2.png
+socorro/public/css/flora/i/slider-handle.gif
+socorro/public/css/flora/i/tabs.png
+socorro/public/css/flora/i/toaster-e.gif
+socorro/public/css/flora/i/toaster-n.gif
+socorro/public/css/flora/i/toaster-ne.gif
+socorro/public/css/flora/i/toaster-nw.gif
+socorro/public/css/flora/i/toaster-s.gif
+socorro/public/css/flora/i/toaster-se.gif
+socorro/public/css/flora/i/toaster-sw.gif
+socorro/public/css/flora/i/toaster-w.gif
+socorro/public/css/flora/i/toolbars-bot-bg.gif
 socorro/public/js/MochiKit/MochiKit.js
 socorro/public/js/PlotKit/PlotKit_Packed.js
 socorro/public/js/PlotKit/excanvas.js
 socorro/public/js/PlotKit/iecanvas.htc
+socorro/public/js/jquery/date.js
+socorro/public/js/jquery/jquery-1.2.1.js
+socorro/public/js/jquery/plugins/ui/jquery.datePicker.js
+socorro/public/js/jquery/plugins/ui/jquery.tablesorter.min.js
+socorro/public/js/jquery/plugins/ui/ui.tabs.js
 socorro/templates/__init__.py
 socorro/templates/admin_index.html
 socorro/templates/branch_maintenance.html
@@ -69,6 +137,9 @@
 socorro/templates/report/__init__.py
 socorro/templates/report/index.html
 socorro/templates/report/list.html
+socorro/templates/report/pending.html
+socorro/templates/status/__init__.py
+socorro/templates/status/index.html
 socorro/templates/topcrasher/__init__.py
 socorro/templates/topcrasher/bybranch.html
 socorro/templates/topcrasher/byversion.html

Modified: trunk/webapp/Socorro.egg-info/requires.txt
==============================================================================
--- trunk/webapp/Socorro.egg-info/requires.txt	(original)
+++ trunk/webapp/Socorro.egg-info/requires.txt	Fri Aug 29 15:36:44 2008
@@ -1,5 +1,5 @@
 Pylons==0.9.5
-SQLAlchemy==0.3.7
+SQLAlchemy>=0.3,<0.4
 Genshi>=0.3.6
-Authkit>=0.3.0pre5
+Authkit==0.3.0pre5
 PasteScript>=1.3.6
\ No newline at end of file

Modified: trunk/webapp/setup.py
==============================================================================
--- trunk/webapp/setup.py	(original)
+++ trunk/webapp/setup.py	Fri Aug 29 15:36:44 2008
@@ -7,8 +7,8 @@
     author="Robert Sayre",
     author_email="sayrer gmail com",
     url="http://code.google.com/p/socorro/";,
-    install_requires=["Pylons==0.9.5", "SQLAlchemy==0.3.7", "Genshi>=0.3.6",
-                      "Authkit>=0.3.0pre5", "PasteScript>=1.3.6"],
+    install_requires=["Pylons==0.9.5", "SQLAlchemy>=0.3,<0.4", "Genshi>=0.3.6",
+                      "Authkit==0.3.0pre5", "PasteScript>=1.3.6"],
     packages=find_packages(),
     include_package_data=True,
     test_suite = 'nose.collector',

Modified: trunk/webapp/socorro/config/routing.py
==============================================================================
--- trunk/webapp/socorro/config/routing.py	(original)
+++ trunk/webapp/socorro/config/routing.py	Fri Aug 29 15:36:44 2008
@@ -12,6 +12,9 @@
     # Set default route to the list of reports.  We can change this later if we
     # create a dashboard or better entry page.
     map.connect('', controller='query', action='query')
+
+    # Route to server status page.
+    map.connect('status', controller='status', action='index')
     
     # Routes to topcrasher reports.  :product and :version follow 'by....'
     # because I thought it was correct to have parameters after the controller
@@ -20,6 +23,11 @@
                 controller='topcrasher', action='byversion',
                 requirements=dict(product='[a-zA-Z.]+',
                 version='[0-9a-zA-Z.]+'))
+    map.connect('topcrasher/byversion/:product/:version/:buildId',
+                controller='topcrasher', action='byversion',
+                requirements=dict(product='[a-zA-Z.]+',
+                version='[0-9a-zA-Z.]+',
+                buildId='[0-9]+'))
     map.connect('topcrasher/bybranch/:branch', 
                 controller='topcrasher', action='bybranch',
                 requirements=dict(branch='[0-9a-zA-Z.]+'))

Modified: trunk/webapp/socorro/controllers/report.py
==============================================================================
--- trunk/webapp/socorro/controllers/report.py	(original)
+++ trunk/webapp/socorro/controllers/report.py	Fri Aug 29 15:36:44 2008
@@ -1,6 +1,6 @@
 from socorro.lib.base import *
 from socorro.lib.processor import Processor
-from socorro.models import Report
+from socorro.models import Report, Job, PriorityJob
 from socorro.lib.queryparams import BySignatureLimit, getReportsForParams
 from socorro.lib.http_cache import responseForKey
 import socorro.lib.collect as collect
@@ -16,15 +16,51 @@
 class ReportController(BaseController):
   def index(self, id):
     c.report = Report.by_id(id)
+
+    # If we get no id, 404 -- id can't be blank.
+    if id is None:
+      abort(404, 'Page Not Found')
+
+    # If we don't have a report entry, see if it's in the queue.
+    # If it's there, flag it for priority so the user can see it in ~10 seconds.
+    #
+    # This should not be cached, so we explicitly send no-cache response
+    # headers.
     if c.report is None:
-      abort(404, 'Not found')
 
-    if c.report['build']:
-      resp = responseForKey(c.report['uuid'], expires=(60 * 60))
+      # The pending page adds ?p=1 to redirects.  Check this to avoid inserting 
+      # duplicate priorityjob entries.  
+      try:
+        request.params['p']
+      except(KeyError):
+        c.priority = PriorityJob.add(id)
+
+      h.redirect_to(action='pending', id=id)
+
+    # If we have the report entry, show it as usual.
     else:
-      resp = responseForKey(c.report['uuid'])
-    resp.write(render('report/index'))
-    return resp
+      if c.report['build']:
+        resp = responseForKey(c.report['uuid'], expires=(60 * 60))
+      else:
+        resp = responseForKey(c.report['uuid'])
+
+      resp.write(render('report/index'))
+      return resp
+
+  def pending(self, id):
+
+    # If we get no id, 404 -- id can't be blank.
+    if id is None:
+      abort(404, 'Page Not Found')
+
+    # Check to see if the report already got processed at some point and redirect to it if so.
+    c.report = Report.by_id(id)
+    if c.report is not None:
+      h.redirect_to(action='index', id=id)
+
+    c.job = Job.by_uuid(id)
+
+    return render_response('report/pending')
 
   def find(self):
     # This method should not touch the database!
@@ -44,7 +80,6 @@
     c.params.setFromParams(request.params)
     key = "reportlist_%s" % request.environ["QUERY_STRING"]
     (c.reports, c.builds, ts) = getReportsForParams(c.params, key)
-
     resp = responseForKey("%s%s" % (ts,key))
     resp.write(render('report/list'))
     return resp

Modified: trunk/webapp/socorro/controllers/topcrasher.py
==============================================================================
--- trunk/webapp/socorro/controllers/topcrasher.py	(original)
+++ trunk/webapp/socorro/controllers/topcrasher.py	Fri Aug 29 15:36:44 2008
@@ -1,7 +1,7 @@
 from socorro.models import Report, reports_table, Branch, branches_table
 from socorro.models import getCachedBranchData
 from socorro.lib.base import BaseController
-from socorro.lib.queryparams import BaseLimit, getCrashesForParams
+from socorro.lib.queryparams import BaseLimit, getTopCrashes
 from socorro.lib.http_cache import responseForKey
 from pylons.database import create_engine
 from pylons import c, session, request
@@ -24,17 +24,23 @@
     user can choose which report they want to see.
     """
     (c.products, c.branches, c.product_versions) = getCachedBranchData()
-    resp = render_response('topcrasher/index')
+    etag = "%s%s" % (len(c.branches), len(c.product_versions))
+    resp = responseForKey(etag)
+    resp.write(render('topcrasher/index'))
     return resp
 
-  def byversion(self, product, version):
+  def byversion(self, product, version, buildId=None):
     """
     The purpose of this action is to generate topcrasher reports based on
     product and version.
     """
-    c.params = BaseLimit(versions=[(product, version)], range=(2, 'weeks'))
-    (c.tc, ts) = getCrashesForParams(c.params,"v_%s%s" % (product, version))
-    etag = "%s%s%s" % (product, version, ts)
+    if buildId:
+      (c.tc, ts, c.last_updated) = getTopCrashes(product, version, "v_%s%s%s" % (product, version, buildId), buildId=buildId)
+      etag = "%s%s%s%s" % (product, version, buildId, ts)
+    else:
+      (c.tc, ts, c.last_updated) = getTopCrashes(product, version, "v_%s%s" % (product, version))
+      etag = "%s%s%s" % (product, version, ts)
+
     resp = responseForKey(etag)
     resp.write(render('topcrasher/byversion'))
     return resp
@@ -44,6 +50,7 @@
     The purpose of this action is to generate topcrasher reports based on
     branch.
     """
+    
     c.params = BaseLimit(branches=[branch], range=(2, 'weeks'))
     (c.tc, ts) = getCrashesForParams(c.params, "branch_" + branch)
     etag = "%s%s" % (branch, ts)

Modified: trunk/webapp/socorro/lib/base.py
==============================================================================
--- trunk/webapp/socorro/lib/base.py	(original)
+++ trunk/webapp/socorro/lib/base.py	Fri Aug 29 15:36:44 2008
@@ -27,7 +27,9 @@
         kwargs['echo'] = echo
         database.get_engines()['%s|%s' % (uri, str(kwargs))] = \
           database.create_engine(uri, echo=echo,
-                                 pool_recycle=config.processorConnTimeout) 
+                                 pool_recycle=config.processorConnTimeout,
+                                 convert_unicode=True,
+                                 encoding='utf-8') 
         return WSGIController.__call__(self, environ, start_response)
 
 # Include the '_' function in the public names

Modified: trunk/webapp/socorro/lib/collect.py
==============================================================================
--- trunk/webapp/socorro/lib/collect.py	(original)
+++ trunk/webapp/socorro/lib/collect.py	Fri Aug 29 15:36:44 2008
@@ -36,20 +36,20 @@
 def backOffMessage():
   pass
 
-def makeDumpDir(base):
+def makeDumpDir(dumpDir):
   """Create a directory to hold a group of dumps, and set permissions"""
-  tmpPath = tempfile.mkdtemp(dir=base, prefix=config.dumpDirPrefix)
-  os.chmod(tmpPath, config.dirPermissions)
+  os.makedirs(dumpDir)
+  os.chmod(dumpDir, config.dirPermissions)
   if config.dumpGID is not None:
-    os.chown(tmpPath, -1, config.dumpGID)
-  return tmpPath
+    os.chown(dumpDir, -1, config.dumpGID)
+  return dumpDir
 
 
 def findLastModifiedDirInPath(path):
   names = os.listdir(path)
   breakpadDirs = [os.path.join(path, entry) for entry
                   in names if entry.startswith(config.dumpDirPrefix)]
-  
+
   # This could happen if some other process or person has removed things
   # from our dated paths
   if len(breakpadDirs) == 0:
@@ -79,23 +79,23 @@
   """Return a directory path to hold dump data, creating if necessary"""
   # First make an hourly directory if necessary
   utc = datetime.utcnow()
+  baseminute = "%02u" % (5 * int(utc.minute/5))
   dateString = "%04u-%02u-%02u-%02u" % (utc.year, utc.month, utc.day, utc.hour)
   datePath = os.path.join(config.storageRoot, str(utc.year), str(utc.month),
-                          str(utc.day), str(utc.hour))
+                          str(utc.day), str(utc.hour), config.dumpDirPrefix + baseminute)
 
   # if it's not there yet, create the date directory and its first
   # dump directory
   if not os.path.exists(datePath):
-    makedirs(datePath)
     return (makeDumpDir(datePath), dateString)
 
   # return the last-modified dir if it has less than dumpCount entries,
   # otherwise make a new one
-  latestDir = findLastModifiedDirInPath(datePath)
-  if len(os.listdir(latestDir)) >= config.dumpDirCount:
-    return (makeDumpDir(datePath), dateString)
-  
-  return (latestDir, dateString)
+  #latestDir = findLastModifiedDirInPath(datePath)
+  #if len(os.listdir(latestDir)) >= config.dumpDirCount:
+  #  return (makeDumpDir(datePath), dateString)
+
+  return (datePath, dateString)
 
 def openFileForDumpID(dumpID, dumpDir, suffix, mode):
   filename = os.path.join(dumpDir, dumpID + suffix)
@@ -113,9 +113,9 @@
   (dirPath, dateString) = getParentPathForDump()
   dumpID = str(uuid.uuid1())
   outfile = openFileForDumpID(dumpID, dirPath, config.dumpFileSuffix, 'wb')
-  
+
   # XXXsayrer need to peek at the first couple bytes for a sanity check
-  # breakpad leading bytes: 0x504d444d  
+  # breakpad leading bytes: 0x504d444d
   #
   try:
     while 1:
@@ -128,6 +128,39 @@
 
   return (dumpID, dirPath, dateString)
 
+def doCreateSymbolicLink(targetPathname, linkPathname):
+  """ Create a symbolic link called 'linkPathname' linked to 'targetPathname'"""
+  os.symlink(targetPathname, linkPathname)
+  os.chmod(linkPathname, config.dirPermissions)
+  if config.dumpGID is not None:
+    os.chown(linkPathname, -1, config.dumpGID)
+
+def createSymbolicLinkForIndex(id, path, suffix):
+  """ For each json file stored, we're going to save a symbolic link to that file in a directory of the form:
+        {config.storageRoot}/index/{hostname}/{jsonfile}.symlink  We can access this structure faster than
+        the distributed structure where the actual json and dump files live.
+  """
+  # create path for index link
+  indexLinkPath = os.path.join(config.storageRoot, "index", os.uname()[1])
+  # create relative path for the link target
+  targetRelativePathName = os.path.join("../..", path[len(config.storageRoot) + 1:], "%s%s" % (id, suffix))
+  try:
+    # create symbolic link
+    symbolicLinkPathname = os.path.join(indexLinkPath, "%s%s" % (id, ".symlink"))
+    doCreateSymbolicLink(targetRelativePathName, symbolicLinkPathname)
+  except OSError:
+    # {hostname} directory does not exist
+    try:
+      mkdir(indexLinkPath)
+    except OSError:
+      # "index" directory probably doesn't exist - create it
+      mkdir(os.path.join(config.storageRoot, "index"))
+      # retry creation of {hostname} directory
+      mkdir(indexLinkPath)
+    # retry creation of symbolic link
+    doCreateSymbolicLink(targetRelativePathName, symbolicLinkPathname)
+
+
 def storeJSON(dumpID, dumpDir, form):
   names = [name for name in form.keys() if name != config.dumpField]
   fields = {}
@@ -139,11 +172,16 @@
   fields["timestamp"] = time()
 
   outfile = openFileForDumpID(dumpID, dumpDir, config.jsonFileSuffix, 'w')
+
   try:
     simplejson.dump(fields, outfile)
   finally:
     outfile.close()
 
+  # create index symbolic link for this json file
+  createSymbolicLinkForIndex(dumpID, dumpDir, config.jsonFileSuffix)
+
+
 def makeResponseForClient(dumpID, dateString):
   response = "CrashID=%s%s\n" % (config.dumpIDPrefix, dumpID)
   if config.reporterURL is not None:

Modified: trunk/webapp/socorro/lib/helpers.py
==============================================================================
--- trunk/webapp/socorro/lib/helpers.py	(original)
+++ trunk/webapp/socorro/lib/helpers.py	Fri Aug 29 15:36:44 2008
@@ -25,4 +25,7 @@
   Return a row class (1|2) based on passed value.  For use in alternating table
   row colors.
   """
-  return 'row'+str(int(i)%2+1)
+  if int(i)%2+1 == 1:
+    return 'odd'
+  else:
+    return 'even'

Modified: trunk/webapp/socorro/lib/monitor.py
==============================================================================
--- trunk/webapp/socorro/lib/monitor.py	(original)
+++ trunk/webapp/socorro/lib/monitor.py	Fri Aug 29 15:36:44 2008
@@ -71,20 +71,26 @@
     print "beat to the punch for uuid " + dumpID
     # This is ok, someone beat us to it
     return
-  
+
   if report is not None:
     didProcess = False
     try:
       sys.stdout.flush()
       print "runProcessor for %s" % dumpID
-      try:
-        processor = Processor(config.processorMinidump,
-                              config.processorSymbols)
-        processor.process(dir, dumpID, report)
-        didProcess = True
-      except:
-        print "Error in processor:"
-        print_exception()
+
+      if report is not False:
+        """Only try to process if the report isn't missing vital information.
+        report will be False if createReport can't validate the report JSON."""
+
+        try:
+          processor = Processor(config.processorMinidump,
+                                config.processorSymbols)
+          processor.process(dir, dumpID, report)
+          didProcess = True
+        except:
+          print "Error in processor:"
+          print_exception()
+
     finally:
       dumppath = os.path.join(dir, dumpID + config.dumpFileSuffix)
 

Modified: trunk/webapp/socorro/lib/platforms.py
==============================================================================
--- trunk/webapp/socorro/lib/platforms.py	(original)
+++ trunk/webapp/socorro/lib/platforms.py	Fri Aug 29 15:36:44 2008
@@ -46,7 +46,8 @@
 
 platformList = PlatformList([Platform('windows', 'Windows', 'Windows NT', '#99F'),
                              Platform('mac', 'Mac OS X', 'Mac OS X', '#3C3'),
-                             Platform('linux', 'Linux', 'Linux', '#C90')])
+                             Platform('linux', 'Linux', 'Linux', '#C90'),
+                             Platform('solaris', 'Solaris', 'Solaris', '#A91')])
 
 def count_platforms():
   return [platform.count() for platform in platformList]

Modified: trunk/webapp/socorro/lib/processor.py
==============================================================================
--- trunk/webapp/socorro/lib/processor.py	(original)
+++ trunk/webapp/socorro/lib/processor.py	Fri Aug 29 15:36:44 2008
@@ -13,7 +13,10 @@
   sys.stdout.flush()
 
 ZERO = timedelta(0)
+
 buildDatePattern = re.compile('^(\\d{4})(\\d{2})(\\d{2})(\\d{2})')
+buildPattern = re.compile('^\\d{10}')
+timePattern = re.compile('^\\d+.?\\d+$')
 
 class UTC(tzinfo):
   def utcoffset(self, dt):
@@ -37,43 +40,83 @@
   try:
     json = simplejson.load(jsonFile)
 
+    if not isValidReport(json):
+      return False
+
     crash_time = None
-    report_date = datetime.now()
     install_age = None
+    uptime = 0 
+    report_date = datetime.now()
 
-    if 'CrashTime' in json and 'InstallTime' in json:
-      crash_time = int(json['CrashTime'])
-      report_date = datetime.fromtimestamp(crash_time, utctz)
-      install_age = crash_time - int(json["InstallTime"])
-    elif 'timestamp' in json:
-      report_date = datetime.fromtimestamp(json["timestamp"], utctz)
+    if 'CrashTime' in json                                                     \
+      and timePattern.match(str(json['CrashTime']))                            \
+      and 'InstallTime' in json                                                \
+      and timePattern.match(str(json['InstallTime'])):
+
+      try:
+        crash_time = int(json['CrashTime'])
+        report_date = datetime.fromtimestamp(crash_time, utctz)
+        install_age = crash_time - int(json['InstallTime'])
+
+        if 'StartupTime' in json \
+          and timePattern.match(str(json['StartupTime'])) \
+          and crash_time >= int(json['StartupTime']):
+          uptime = crash_time - int(json['StartupTime'])
+      except (ValueError):
+        pass
+    elif 'timestamp' in json and timePattern.match(str(json['timestamp'])):
+      try:
+        report_date = datetime.fromtimestamp(json['timestamp'], utctz)
+      except (ValueError):
+        pass
 
     build_date = None
     try:
-      #(y, m, d, h) = map(int, buildDatePattern.match(json["BuildID"]).groups())
-      build_date = datetime(2007, 9, 29, 22)
-    except (AttributeError, ValueError):
+      (y, m, d, h) = map(int,
+                         buildDatePattern.match(str(json['BuildID'])).groups())
+      build_date = datetime(y, m, d, h)
+    except (AttributeError, ValueError, KeyError):
       pass
 
     last_crash = None
-    if 'SecondsSinceLastCrash' in json:
-      last_crash = int(json["SecondsSinceLastCrash"])
+    if 'SecondsSinceLastCrash' in json and timePattern.match(str(json['SecondsSinceLastCrash'])):
+      last_crash = int(json['SecondsSinceLastCrash'])
 
     return model.Report.create(uuid=id,
                                date=report_date,
-                               product=json['product'],
-                               version=json['version'],
-                               build=json['gnome_version'],
+                               product=json.get('ProductName', None),
+                               version=json.get('Version', None),
+                               build=json.get('BuildID', None),
                                url=json.get('URL', None),
                                install_age=install_age,
                                last_crash=last_crash,
+                               uptime=uptime,
                                email=json.get('Email', None),
-			       os_version=json['os_version'],
+                               os_version=json['os_version'],
                                build_date=build_date,
-                               user_id=json.get('UserID', None))
+                               user_id=json.get('UserID', None),
+                               comments=json.get('Comments', None))
   finally:
     jsonFile.close()
 
+def isValidReport(json):
+  """Given a json dict passed from simplejson, we need to verify that required
+  fields exist.  If they don't, we should throw away the dump and continue.
+  Method returns a boolean value -- true if valid, false if not."""
+
+  valid = True
+
+  if 'BuildID' not in json or not buildPattern.match(json['BuildID']):
+    valid = False
+
+  if 'ProductName' not in json:
+    valid = False
+
+  if 'Version' not in json:
+    valid = False
+
+  return valid
+
 def fixupSourcePath(path):
   """Given a full path of a file in a Mozilla source tree,
      strip off anything prior to mozilla/, and convert

Modified: trunk/webapp/socorro/lib/queryparams.py
==============================================================================
--- trunk/webapp/socorro/lib/queryparams.py	(original)
+++ trunk/webapp/socorro/lib/queryparams.py	Fri Aug 29 15:36:44 2008
@@ -1,8 +1,8 @@
-from socorro.models import reports_table as reports, branches_table as branches, frames_table as frames
+from socorro.models import reports_table as reports, branches_table as branches, frames_table as frames, top_crashers_table as topcrashers
 import formencode
 import sqlalchemy
 from pylons.database import create_engine
-from sqlalchemy import sql, func, select, types
+from sqlalchemy import sql, func, select, types, and_, desc
 from sqlalchemy.databases.postgres import PGInterval
 import re
 from socorro.lib.platforms import count_platforms, platformList
@@ -47,7 +47,7 @@
     return platformList[str(value)]
 
 class BaseLimit(object):
-  """A base class which validates date/branch/product/version conditions for
+  """A base class which validates date/branch/product/version/buildid conditions for
   multiple searches."""
 
   datetime_validator = formencode.validators.Regex(
@@ -60,7 +60,7 @@
   stringlist_validator = ListValidator(formencode.validators.String(strip=True))
   platform_validator = ListValidator(PlatformValidator())
   query_validator = formencode.validators.OneOf(['signature', 'stack'])
-  type_validator = formencode.validators.OneOf(['exact', 'contains'])
+  type_validator = formencode.validators.OneOf(['exact', 'startswith', 'contains'])
   signature_validator = formencode.validators.String(strip=True, if_empty=None)
 
   @staticmethod
@@ -70,7 +70,7 @@
     return (num, unit)
 
   def __init__(self, date=None, range=None,
-               products=None, branches=None, versions=None, platforms=None,
+               products=None, branches=None, versions=None, buildid=None, platforms=None,
                query=None, query_search=None, query_type=None):
     self.date = date
     self.range = range   # _range is a tuple (number, interval)
@@ -81,6 +81,7 @@
     self.query = query
     self.query_search = query_search
     self.query_type = query_type
+    self.buildid = buildid
 
   def setFromParams(self, params):
     """Set the values of this object from a request.params instance."""
@@ -101,7 +102,8 @@
 
     for platforms in params.getall('platform'):
       self.platforms.extend(self.platform_validator.to_python(platforms))
-
+    
+    self.query = params.get('buildid', None)
     self.query = self.signature_validator.to_python(params.get('query', None))
     self.query_search = self.query_validator.to_python(params.get('query_search', None))
     self.query_type = self.type_validator.to_python(params.get('query_type', None))
@@ -185,8 +187,11 @@
   
   def filterByQuery(self, q):
     if self.query is not None:
-      if self.getQueryType() == 'contains':
-        pattern = '%' + self.query.replace('%', '%%') + '%'
+      if self.getQueryType() == 'contains' or \
+         self.getQueryType() == 'startswith':
+        pattern = self.query.replace('%', '%%') + '%'
+        if self.getQueryType() == 'contains':
+          pattern = '%' + pattern
         if self.getQuerySearch() == 'signature':
           q = q.filter(reports.c.signature.like(pattern))
         else:
@@ -218,13 +223,23 @@
 
   def query_reports(self):
     selects = [reports.c.date,
+               reports.c.date_processed,
+               reports.c.uptime,
+               reports.c.comments,
                reports.c.uuid,
                reports.c.product,
                reports.c.version,
                reports.c.build,
                reports.c.signature,
                reports.c.url,
-               reports.c.os_name]
+               reports.c.os_name,
+               reports.c.os_version,
+               reports.c.cpu_name,
+               reports.c.cpu_info,
+               reports.c.address,
+               reports.c.reason,
+               reports.c.last_crash,
+               reports.c.install_age]
     s = select(selects,
                order_by=sql.desc(reports.c.date),
                limit=500,
@@ -289,6 +304,8 @@
                limit=100,
                engine=create_engine())
     s.append_whereclause(reports.c.signature != None)
+    if self.buildid:
+      s.append_whereclause(reports.c.build == self.buildid)
 
     def FilterToAppend(clause):
       s.append_whereclause(clause)
@@ -309,7 +326,8 @@
 
     if self.query is not None:
       sigtype = {'exact': 'is exactly',
-                 'contains': 'contains'}[self.getQueryType()]
+                 'contains': 'contains',
+                 'startswith': 'starts with'}[self.getQueryType()]
 
       sigquery = {'signature': 'the crash signature',
                   'stack': 'one of the top 10 stack frames'}[self.getQuerySearch()]
@@ -347,6 +365,33 @@
       q = q.filter(reports.c.signature == self.signature)
     return q
 
+def getTopCrashes(product, version, key, buildId=None):
+  """
+  Get a list of top crashes for a specific key.
+  Returns a tuple of the topcrashers, current timestamp, and the last updated timestamp from the topcrashers table.
+  """
+  def getTopCrashers():
+    db = create_engine()
+    if buildId:
+      last_updated_sql = db.execute("SELECT last_updated FROM topcrashers WHERE product='%s' AND version='%s' AND build='%s' ORDER BY last_updated DESC LIMIT 1" % (product, version, buildId))
+      for each in last_updated_sql:
+        last_updated = each[0]
+
+      tc = [r for r in topcrashers.select(engine=create_engine(), whereclause=and_(topcrashers.c.product == product, topcrashers.c.version == version, topcrashers.c.build == buildId, topcrashers.c.last_updated == last_updated)).execute()]
+      ts = time.time()
+    else:
+      last_updated_sql = db.execute("SELECT last_updated FROM topcrashers WHERE product='%s' AND version='%s' ORDER BY last_updated DESC LIMIT 1" % (product, version))
+      for each in last_updated_sql:
+        last_updated = each[0]
+
+      tc = [r for r in topcrashers.select(engine=create_engine(), whereclause=and_(topcrashers.c.product == product, topcrashers.c.version == version, topcrashers.c.last_updated == last_updated), order_by=desc(topcrashers.c.total)).execute()]
+      ts = time.time()
+
+    return (tc, ts, last_updated)
+
+  tccache = pylons.cache.get_cache('tc_data')
+  return tccache.get_value(key, createfunc=getTopCrashers,
+                           type="memory", expiretime=60)
 ### XXXcombine the two functions below
 def getCrashesForParams(params, key):
   """

Modified: trunk/webapp/socorro/models/__init__.py
==============================================================================
--- trunk/webapp/socorro/models/__init__.py	(original)
+++ trunk/webapp/socorro/models/__init__.py	Fri Aug 29 15:36:44 2008
@@ -2,6 +2,7 @@
 from datetime import datetime
 from socorro.lib import config, EmptyFilter
 from cStringIO import StringIO
+from sqlalchemy.exceptions import SQLError
 import sys
 import os
 import re
@@ -11,7 +12,7 @@
 class TruncatingString(types.TypeDecorator):
   """
   Truncating string type.
-  
+
   By default, SQLAlchemy will throw an error if a string that is too long
   is inserted into the database. We subclass the default String type to
   automatically truncate to the correct length.
@@ -24,7 +25,9 @@
     return value[:self.length]
 
   def convert_result_value(self, value, engine):
-    return value
+    if value is None:
+      return None
+    return value.decode('utf-8')
 
 """
 Define database structure.
@@ -44,24 +47,31 @@
          default=text("nextval('seq_reports_id')"),
          primary_key=True),
   Column('date', DateTime(timezone=True)),
-  Column('uuid', String(50), index=True, unique=True, nullable=False),
-  Column('product', String(30)),
-  Column('version', String(16)),
-  Column('build', String(30)),
+  Column('date_processed', DateTime(), nullable=False, default=func.now()),
+  Column('uuid', Unicode(50), index=True, unique=True, nullable=False),
+  Column('product', Unicode(30)),
+  Column('version', Unicode(16)),
+  Column('build', Unicode(30)),
   Column('signature', TruncatingString(255), index=True),
   Column('url', TruncatingString(255), index=True),
   Column('install_age', Integer),
   Column('last_crash', Integer),
+  Column('uptime', Integer),
   Column('comments', TruncatingString(500)),
   Column('cpu_name', TruncatingString(100)),
   Column('cpu_info', TruncatingString(100)),
   Column('reason', TruncatingString(255)),
-  Column('address', String(20)),
+  Column('address', Unicode(20)),
   Column('os_name', TruncatingString(100)),
   Column('os_version', TruncatingString(100)),
   Column('email', TruncatingString(100)),
   Column('build_date', DateTime()),
-  Column('user_id', String(50))
+  Column('user_id', Unicode(50)),
+  Column('starteddatetime', DateTime()),
+  Column('completeddatetime', DateTime()),
+  Column('success', Boolean),
+  Column('message', TEXT(convert_unicode=True)),
+  Column('truncated', Boolean)
 )
 
 def upgrade_reports(dbc):
@@ -84,10 +94,11 @@
     print "  Updated %s rows." % cursor.rowcount
   else:
     print "ok"
+
   print "  Checking for reports.user_id...",
   cursor.execute("""SELECT 1 FROM pg_attribute
                     WHERE attrelid = 'reports'::regclass
-                    AND attname = 'user_id'""");
+                    AND attname = 'user_id'""")
   if cursor.rowcount == 0:
     print "adding"
     cursor.execute('ALTER TABLE reports ADD user_id character(50)')
@@ -105,19 +116,106 @@
   else:
     print "ok"
 
+  print "  Checking for reports.date_processed...",
+  cursor.execute("""SELECT 1 FROM pg_attribute
+                    WHERE attrelid = 'reports'::regclass
+                    AND attname = 'date_processed'""")
+  if cursor.rowcount == 0:
+    print "setting"
+    cursor.execute("""ALTER TABLE reports
+                      ADD date_processed timestamp without time zone
+                      NOT NULL default NOW()""")
+  else:
+    print "ok"
+
+  print "  Checking for reports.comments...",
+  cursor.execute("""SELECT 1 FROM pg_attribute
+                    WHERE attrelid = 'reports'::regclass
+                    AND attname = 'comments'""")
+  if cursor.rowcount == 0:
+    print "setting"
+    cursor.execute("""ALTER TABLE reports ADD comments text NULL""")
+  else:
+    print "ok"
+
+  print "  Checking for reports.uptime...",
+  cursor.execute("""SELECT 1 FROM pg_attribute
+                    WHERE attrelid = 'reports'::regclass
+                    AND attname = 'uptime'""")
+  if cursor.rowcount == 0:
+    print "setting"
+    cursor.execute("""ALTER TABLE reports ADD uptime integer NOT NULL default 0""")
+  else:
+    print "ok"
+
+  print "  Checking for reports.success...",
+  cursor.execute("""SELECT 1 FROM pg_attribute
+                    WHERE attrelid = 'reports'::regclass
+                    AND attname = 'success'""")
+  if cursor.rowcount == 0:
+    print "setting"
+    cursor.execute("""ALTER TABLE reports
+                      ADD success boolean NULL""")
+  else:
+    print "ok"
+
+  print "  Checking for reports.starteddatetime...",
+  cursor.execute("""SELECT 1 FROM pg_attribute
+                    WHERE attrelid = 'reports'::regclass
+                    AND attname = 'starteddatetime'""")
+  if cursor.rowcount == 0:
+    print "setting"
+    cursor.execute("""ALTER TABLE reports 
+                      ADD starteddatetime timestamp without time zone NULL""")
+  else:
+    print "ok"
+
+  print "  Checking for reports.completeddatetime...",
+  cursor.execute("""SELECT 1 FROM pg_attribute
+                    WHERE attrelid = 'reports'::regclass
+                    AND attname = 'completeddatetime'""")
+  if cursor.rowcount == 0:
+    print "setting"
+    cursor.execute("""ALTER TABLE reports
+                      ADD completeddatetime timestamp without time zone NULL""")
+  else:
+    print "ok"
+
+  print "  Checking for reports.message...",
+  cursor.execute("""SELECT 1 FROM pg_attribute
+                    WHERE attrelid = 'reports'::regclass
+                    AND attname = 'message'""")
+  if cursor.rowcount == 0:
+    print "setting"
+    cursor.execute("""ALTER TABLE reports
+                      ADD message text NULL""")
+  else:
+    print "ok"
+
+  print "  Checking for reports.truncated...",
+  cursor.execute("""SELECT 1 FROM pg_attribute
+                    WHERE attrelid = 'reports'::regclass
+                    AND attname = 'truncated'""")
+  if cursor.rowcount == 0:
+    print "setting"
+    cursor.execute("""ALTER TABLE reports
+                      ADD truncated boolean NULL""")
+  else:
+    print "ok"
+
 frames_table = Table('frames', meta,
-  Column('report_id', Integer, ForeignKey('reports.id'), primary_key=True),
+  Column('report_id', Integer, ForeignKey('reports.id', ondelete='CASCADE'), primary_key=True),
   Column('frame_num', Integer, nullable=False, primary_key=True, autoincrement=False),
-  Column('signature', TruncatingString(255))
+  Column('signature', TruncatingString(255)),
 )
 
 modules_table = Table('modules', meta,
-  Column('report_id', Integer, ForeignKey('reports.id'), primary_key=True),
+  Column('report_id', Integer, ForeignKey('reports.id', ondelete='CASCADE'), primary_key=True),
   Column('module_key', Integer, primary_key=True, autoincrement=False),
   Column('filename', TruncatingString(40), nullable=False),
-  Column('debug_id', String(40)),
+  Column('debug_id', Unicode(40)),
   Column('module_version', TruncatingString(15)),
-  Column('debug_filename', TruncatingString(40)),
+  Column('debug_filename', TruncatingString(40))
 )
 
 def upgrade_modules(dbc):
@@ -137,14 +235,17 @@
                 {'length': modules_table.c.debug_id.type.length})
   cur.close()
 
-extensions_table = Table('extensions', meta, Column('report_id', Integer, ForeignKey('reports.id'), primary_key=True), Column('extension_key', Integer, primary_key=True, autoincrement=False),
-  Column('extension_id', String(100), nullable=False),
-  Column('extension_version', String(16))
+extensions_table = Table('extensions', meta,
+  Column('report_id', Integer, ForeignKey('reports.id', ondelete='CASCADE'), primary_key=True),
+  Column('extension_key', Integer, primary_key=True, autoincrement=False),
+  Column('extension_id', Unicode(100), nullable=False),
+  Column('extension_version', Unicode(16))
 )
 
 dumps_table = Table('dumps', meta,
-  Column('report_id', Integer, ForeignKey('reports.id'), primary_key=True),
-  Column('data', TEXT())
+  Column('report_id', Integer, ForeignKey('reports.id', ondelete='CASCADE'), primary_key=True),
+  Column('truncated', Boolean, default=False),
+  Column('data', TEXT(convert_unicode=True))
 )
 
 branches_table = Table('branches', meta,
@@ -153,6 +254,57 @@
   Column('branch', String(24), nullable=False)
 )
 
+jobs_id_sequence = Sequence('jobs_id_seq', meta)
+
+priorityjobs_table = Table('priorityjobs', meta,
+  Column('uuid', Unicode(50), primary_key=True, nullable=False)
+)
+
+processors_id_sequence = Sequence('processors_id_seq', meta)
+
+processors_table = Table('processors', meta,
+  Column('id', Integer, processors_id_sequence,
+         default=text("nextval('processors_id_seq')"),
+         primary_key=True),
+  Column('name', String(255), nullable=False),
+  Column('startdatetime', DateTime(), nullable=False),
+  Column('lastseendatetime', DateTime())
+)
+
+jobs_table = Table('jobs', meta,
+  Column('id', Integer, jobs_id_sequence,
+         default=text("nextval('jobs_id_seq')"),
+         primary_key=True),
+  Column('pathname', String(1024), nullable=False),
+  Column('uuid', Unicode(50), index=True, unique=True, nullable=False),
+  Column('owner', Integer, ForeignKey('processors.id')),
+  Column('priority', Integer, default=0),
+  Column('queueddatetime', DateTime()),
+  Column('starteddatetime', DateTime()),
+  Column('completeddatetime', DateTime()),
+  Column('success', Boolean),
+  Column('message', TEXT(convert_unicode=True))
+)
+
+top_crashers_table = Table('topcrashers', meta,
+  Column('id', Integer, primary_key=True),
+  Column('signature', String(255), nullable=False),
+  Column('version', String(30), nullable=False),
+  Column('product', String(30), nullable=False),
+  Column('build', String(30), nullable=False),
+  Column('total', Integer),
+  Column('win', Integer),
+  Column('mac', Integer),
+  Column('linux', Integer),
+  Column('rank', Integer),
+  Column('last_rank', Integer),
+  Column('trend', String(30)),
+  Column('uptime', Float(2)),
+  Column('users', Integer),
+  Column('last_updated', DateTime())
+)
+
+
 lock_function_definition = """
 declare
 begin
@@ -323,7 +475,7 @@
     cmd := subst('CREATE TABLE $$ (
                     CHECK(report_id >= $$),
                     PRIMARY KEY(report_id, frame_num),
-                    FOREIGN KEY(report_id) REFERENCES $$ (id)
+                    FOREIGN KEY(report_id) REFERENCES $$ (id) ON DELETE CASCADE
                   ) INHERITS (frames)',
                  ARRAY[ quote_ident(objname),
                         quote_literal(start_id),
@@ -334,7 +486,7 @@
     cmd := subst('CREATE TABLE $$ (
                     CHECK(report_id >= $$),
                     PRIMARY KEY(report_id, module_key),
-                    FOREIGN KEY(report_id) REFERENCES $$ (id)
+                    FOREIGN KEY(report_id) REFERENCES $$ (id) ON DELETE CASCADE
                   ) INHERITS (modules)',
                  ARRAY[ quote_ident(objname),
                         quote_literal(start_id),
@@ -345,7 +497,7 @@
     cmd := subst('CREATE TABLE $$ (
                     CHECK(report_id >= $$),
                     PRIMARY KEY(report_id, extension_key),
-                    FOREIGN KEY(report_id) REFERENCES $$ (id)
+                    FOREIGN KEY(report_id) REFERENCES $$ (id) ON DELETE CASCADE
                   ) INHERITS (extensions)',
                  ARRAY[ quote_ident(objname),
                         quote_literal(start_id),
@@ -356,7 +508,7 @@
     cmd := subst('CREATE TABLE $$ (
                     CHECK(report_id >= $$),
                     PRIMARY KEY(report_id),
-                    FOREIGN KEY(report_id) REFERENCES $$ (id)
+                    FOREIGN KEY(report_id) REFERENCES $$ (id) ON DELETE CASCADE
                   ) INHERITS (dumps)',
                  ARRAY[ quote_ident(objname),
                         quote_literal(start_id),
@@ -417,6 +569,7 @@
   upgrade_reports(dbc)
   upgrade_modules(dbc)
 
+
 def ensure_partitions(dbc):
   print "Checking for database partitions...",
   cur = dbc.cursor()
@@ -445,6 +598,7 @@
 
 fixupSpace = re.compile(r' (?=[\*&,])')
 fixupComma = re.compile(r'(?<=,)(?! )')
+stripArgs = re.compile(r'\(.*\)')
 
 filename_re = re.compile('[/\\\\]([^/\\\\]+)$')
 
@@ -476,14 +630,16 @@
   """
   if localEngine:
     return localEngine
-  
+
   from pylons.database import create_engine
   return create_engine(pool_recycle=config.processorConnTimeout)
 
 class Frame(dict):
-  def __init__(self, module_name, function, source, source_line, instruction):
+  def __init__(self, module_name, frame_num, function, source, source_line, instruction):
     self['module_name'] = module_name
+    self['frame_num'] = frame_num
     self['signature'] = make_signature(module_name, function, source, source_line, instruction)
+    self['short_signature'] = stripArgs.sub('', self['signature'])
     self['function'] = function
     self['source'] = source
     self['source_line'] = source_line
@@ -500,8 +656,8 @@
           if root in config.vcsMappings[type]:
             self['source_link'] = config.vcsMappings[type][root] % \
                                     {'file': source_file,
-                                     'revision': revision, 
-                                     'line': source_line} 
+                                     'revision': revision,
+                                     'line': source_line}
       else:
         self['source_filename'] = os.path.split(source)[1]
 
@@ -553,6 +709,7 @@
     self._read_header(lines)
     self._read_stackframes(lines)
 
+  # Daddy wants some rollback for when things go bad.
   def flush(self):
     signature = None
 
@@ -560,9 +717,9 @@
         self.crashed_thread <= len(self.threads) and
         len(self.threads[self.crashed_thread]) > 0):
       signature = self.threads[self.crashed_thread][0]['signature']
-      print "Calculating signature: %s" % signature
+      print "Calculating signature for report %s: %s" % (self['uuid'], signature)
     else:
-      print "Something didn't work for signatures"
+      print "Failed to create signature for report %s:" % self['uuid']
 
     updatevalues = {'signature':  signature,
                     'cpu_name':   self['cpu_name'],
@@ -582,14 +739,15 @@
     r = reports_table.update(whereclause=reports_table.c.id==self['id']). \
         compile(engine=getEngine(), parameters=updatevalues).execute(updatevalues)
 
-    module_data = [{'report_id': self['id'],
-                    'module_key': i,
-                    'filename': self.modules[i].filename,
-                    'debug_id': self.modules[i].debug_id,
-                    'module_version': self.modules[i].module_version,
-                    'debug_filename': self.modules[i].debug_filename}
-                   for i in xrange(0, len(self.modules))]
-    r = modules_table.insert().compile(engine=getEngine()).execute(*module_data)
+    if self.modules is not None and len(self.modules) > 1:
+      module_data = [{'report_id': self['id'],
+                      'module_key': i,
+                      'filename': self.modules[i].filename,
+                      'debug_id': self.modules[i].debug_id,
+                      'module_version': self.modules[i].module_version,
+                      'debug_filename': self.modules[i].debug_filename}
+                     for i in xrange(0, len(self.modules))]
+      r = modules_table.insert().compile(engine=getEngine()).execute(*module_data)
 
     if (self.crashed_thread is not None and
         self.crashed_thread <= len(self.threads)):
@@ -613,7 +771,7 @@
       # empty line separates header data from thread data
       if line == '':
         return
-      
+
       values = map(EmptyFilter, line.split("|"))
       if values[0] == 'OS':
         self['os_name'] = values[1]
@@ -633,14 +791,19 @@
                                      debug_id=values[4],
                                      module_version=values[2],
                                      debug_filename=values[3]))
-  
+
   def _read_stackframes(self, lines):
     for line in lines:
       (thread_num, frame_num, module_name, function, source, source_line, instruction) = map(EmptyFilter, line.split("|"))
       thread_num = int(thread_num)
       while thread_num >= len(self.threads):
         self.threads.append([])
+
+        if module_name is None:
+          continue
+
       self.threads[thread_num].append(Frame(module_name=module_name,
+                                            frame_num=frame_num,
                                             function=function,
                                             source=source,
                                             source_line=source_line,
@@ -651,7 +814,24 @@
     self.product = product
     self.version = version
     self.branch = branch
-  
+
+  def flush(self):
+    branch_data = [{'product': self.product,
+                   'version': self.version,
+                   'branch': self.branch}]
+    r = branches_table.insert().compile(engine=getEngine()).execute(*branch_data)
+    return
+
+  @staticmethod
+  def getAll():
+    """
+    Just returns everything in branches table.
+    """
+    return select([branches_table],
+                  distinct=True,
+                  order_by=[branches_table.c.branch],
+                  engine=getEngine()).execute()
+
   @staticmethod
   def getBranches():
     """
@@ -667,9 +847,9 @@
     """
     Return a list of distinct [product, branch] sorted by product name and branch.
     """
-    return select([branches_table.c.product, branches_table.c.branch], 
+    return select([branches_table.c.product, branches_table.c.branch],
                   distinct=True,
-                  order_by=[branches_table.c.product, 
+                  order_by=[branches_table.c.product,
                             branches_table.c.branch],engine=getEngine()).execute()
 
   @staticmethod
@@ -677,7 +857,7 @@
     """
     Return a list of distinct [product] sorted by product.
     """
-    return select([branches_table.c.product], 
+    return select([branches_table.c.product],
                   distinct=True,
                   order_by=branches_table.c.product,engine=getEngine()).execute()
 
@@ -710,7 +890,7 @@
                                type="memory", expiretime=360)
 
 class Module(object):
-  def __init__(self, filename, debug_id, 
+  def __init__(self, filename, debug_id,
                module_version, debug_filename):
     self.filename = filename
     self.debug_id = debug_id
@@ -724,6 +904,29 @@
     self.extension_id = extension_id
     self.extension_version = extension_version
 
+class Job(object):
+  """
+  For accessing and updating the reports queue.
+  """
+
+  @staticmethod
+  def by_uuid(uuid):
+    """ Get queue information for a pending uuid. """
+    return select([jobs_table], limit=1, whereclause=jobs_table.c.uuid==uuid,
+                  engine=getEngine()).execute().fetchone()
+
+class PriorityJob(object):
+  @staticmethod
+  def add(uuid):
+    """ Insert passed UUID into the priorityjobs table. """
+    vals = [{'uuid': uuid}]
+    try:
+      priorityjobs_table.insert().compile(engine=getEngine()).execute(*vals)
+    except(SQLError):
+      pass
+
+
+
 #
 # Check whether we're running outside Pylons
 #
@@ -737,6 +940,6 @@
   from sqlalchemy.ext.sessioncontext import SessionContext
   localEngine = create_engine(config.processorDatabaseURI,
                               strategy="threadlocal",
-                              poolclass=pool.QueuePool, 
+                              poolclass=pool.QueuePool,
                               pool_recycle=config.processorConnTimeout,
                               pool_size=1)

Modified: trunk/webapp/socorro/public/css/layout.css
==============================================================================
--- trunk/webapp/socorro/public/css/layout.css	(original)
+++ trunk/webapp/socorro/public/css/layout.css	Fri Aug 29 15:36:44 2008
@@ -430,6 +430,45 @@
 
 /* page content */
 
+#menu {
+}
+
+#quickfind {
+  display: inline;
+  float: right;
+  font-size: smaller;
+}
+
+#report-header {
+  float: left;
+}
+
+#report-header-details {
+  float: right;
+  font-size: smaller;
+}
+
+#report-header-details span {
+  font-weight: bold;
+}
+
+#report-index {
+  clear: both;
+}
+
+
+.hidden {
+  display: none;
+}
+
+h1.loading {
+  background: transparent url(../loading.png) no-repeat left;
+  padding-left: 25px;
+}
+
+
+
+
 div#content {
 	clear: both;
 	padding: 1em;

Modified: trunk/webapp/socorro/public/css/style.css
==============================================================================
--- trunk/webapp/socorro/public/css/style.css	(original)
+++ trunk/webapp/socorro/public/css/style.css	Fri Aug 29 15:36:44 2008
@@ -57,7 +57,7 @@
   font-weight:normal;
 }
 .list th {
-  background: #ccf;
+  background: #dfd;
   border: 1px solid #000;
   font-weight: bold;
   padding: 2px;
@@ -70,13 +70,13 @@
 .list th a:hover {
   background-color: #fff;
 }
-.row1 {
+.even {
   background-color: #eee;
 }
-.row2 {
+.odd {
   background-color: #ddd;
 }
-.row1:hover, .row2:hover {
+.even:hover, .odd:hover {
   background-color: #fff;
 }
 
@@ -84,6 +84,26 @@
   text-align: right;
 }
 
+div.code {
+  background-color: #eee;
+  border: 1px solid #ccc;
+  font-family: monospace;
+  width: 640px;
+  height: 480px;
+  white-space: pre;
+  overflow: auto;
+}
+
+th a.expand {
+  display: inline;
+  font-size: smaller;
+}
+
+dt {
+  font-weight: bold;
+}
+
+
 /* styling page content */
 
 h1 {

Modified: trunk/webapp/socorro/templates/includes/common.html
==============================================================================
--- trunk/webapp/socorro/templates/includes/common.html	(original)
+++ trunk/webapp/socorro/templates/includes/common.html	Fri Aug 29 15:36:44 2008
@@ -4,7 +4,7 @@
 from socorro.lib.platforms import platformList
 ?>
 
-<table py:def="list_by_signature(signatures, params)" class="list">
+<table py:def="list_by_signature(signatures, params)" id="signatureList" class="tablesorter">
  <thead>
   <tr>
    <th>Rank</th>
@@ -34,42 +34,86 @@
  </tbody>
 </table>
 
-<table xmlns:py="http://genshi.edgewall.org/"; py:def="list_reports(reports)" class="list">
+<table py:def="list_topcrashers(signatures)" id="signatureList" class="tablesorter">
  <thead>
   <tr>
-   <th rowspan="2">Date</th>
-   <th colspan="2">Product/Version/Build/OS</th>
+   <th>Rank</th>
+   <th>Signature</th>
+   <th>Build ID</th>
+   <th>#</th>
+   <th>Win</th>
+   <th>Lin</th>
+   <th>Mac</th>
   </tr>
+ </thead>
+ <tbody>
+  <?python row=1 ?>
+  <tr py:for="signature in signatures" class="${h.get_row_class(row)}">
+   <td>${str(row)}</td>
+   <td><a href="${h.url_for('/report/list', signature=signature.signature, range_unit='weeks', version=c.product + ':' + c.version, range_value='2')}" title="View reports with this signature.">${signature.signature}</a></td>
+   <td>${signature.build}</td>
+   <td>${signature.total}</td>
+   <td>${signature.win}</td>
+   <td>${signature.linux}</td>
+   <td>${signature.mac}</td>
+  <?python row+=1 ?>
+  </tr>
+ </tbody>
+</table>
+
+<table xmlns:py="http://genshi.edgewall.org/"; py:def="list_reports(reports)" class="tablesorter" id="reportsList">
+ <thead>
   <tr>
-   <th>Signature</th>
-   <th>URL</th>
+   <th>Date</th>
+   <th>Product</th>
+   <th>Version</th>
+   <th>Build</th>
+   <th>OS</th>
+   <th>CPU</th>
+   <th>Reason</th>
+   <th>Address</th>
+   <th>Uptime</th>
+   <th>Comments</th>
   </tr>
  </thead>
  <tbody>
-  <?python row = 0 ?>
   <py:for each="report in reports">
-   <tr class="${h.get_row_class(row)}">
-    <td class="report-date" rowspan="2">
+   <tr>
+    <td class="report-date">
      <a href="${h.url_for(id=report.uuid, controller='report', action='index')}">
-      ${report.date.strftime("%Y-%m-%d")}&nbsp;${report.date.strftime("%H:%M")}
+      ${report.date_processed.strftime("%Y-%m-%d")}&nbsp;${report.date_processed.strftime("%H:%M")}
      </a>
-    </td>
-    <td class="report-product" colspan="2">
-     ${report.product} ${report.version}: ${report.build} -
-     ${report.os_name}
-    </td>
+    </td> <td>${report.product}</td>
+    <td>${report.version}</td>
+    <td>${report.build}</td>
+    <td>${report.os_name} ${report.os_version}</td>
+    <td>${report.cpu_name}</td>
+    <td>${report.reason}</td>
+    <td>${report.address}</td>
+    <td>${report.uptime}</td>
+    <td>${report.comments}</td>
    </tr>
-   <tr class="${'row'+str(row%2+1)}">
-    <td class="report-signature">
-     <code>
-      ${report.signature}
-     </code>
-    </td>
-    <td class="report-url">
-     ${report.url}
+  </py:for>
+ </tbody>
+</table>
+
+<table xmlns:py="http://genshi.edgewall.org/"; py:def="list_comments(reports)" class="tablesorter" id="commentsList">
+ <thead>
+  <tr>
+   <th>Date</th>
+   <th>Comments</th>
+  </tr>
+ </thead>
+ <tbody>
+  <py:for each="report in reports">
+   <tr>
+    <td class="report-date">
+     <a href="${h.url_for(id=report.uuid, controller='report', action='index')}">
+      ${report.date_processed.strftime("%Y-%m-%d")}&nbsp;${report.date_processed.strftime("%H:%M")}
+     </a>
     </td>
+    <td>${report.comments}</td>
    </tr>
-   <?python row+=1 ?>
   </py:for>
  </tbody>
 </table>

Modified: trunk/webapp/socorro/templates/includes/site.html
==============================================================================
--- trunk/webapp/socorro/templates/includes/site.html	(original)
+++ trunk/webapp/socorro/templates/includes/site.html	Fri Aug 29 15:36:44 2008
@@ -45,19 +45,22 @@
           Crash Reporter
         </h1>
 	<div id="control">
-	  <form id="crash-search" action="${h.url_for(controller='report', action='find')}"
-		py:if="not request.params.get('do_query', None)">
-	   <input type="text" size="24" name="id" id="search-crash-id"/>
-	   <input type="submit" id="search-crash-submit" value="Go to Report" />
-	  </form>
+         <form id="quickfind" action="${h.url_for(controller='report', action='find')}">
+          <input type="text" size="24" name="id" id="crash-id"/>
+          <input type="submit" value="Go to Report" />
+         </form>
 	</div>
         <div id="tabs">
           <ul id="portal-globalnav">
             <li id="portaltab-root">
               <a href="/"><span>Home</span></a>
             </li>
-	    <li><a href="${ h.url_for(controller='query', action='query', id=None) }"><span>Find a report</span></a></li>
-	    <li><a href="${ h.url_for(controller='topcrasher', action='index') }"><span>Top Crashers</span></a></li>
+              <div id="menu">
+               <ul>
+                <li>${ h.link_to('Find a Report', h.url_for(controller='query', action='query', id=None)) }</li>
+                <li>${ h.link_to('Top Crashers', h.url_for(controller='topcrasher', action='index')) }</li>
+               </ul>
+              </div>
           </ul>
         </div> <!-- end of #tabs -->
       </div> <!-- end of #header -->

Modified: trunk/webapp/socorro/templates/query_form.html
==============================================================================
--- trunk/webapp/socorro/templates/query_form.html	(original)
+++ trunk/webapp/socorro/templates/query_form.html	Fri Aug 29 15:36:44 2008
@@ -11,17 +11,28 @@
  <xi:include href="includes/common.html" />
  <head>
   <title>Crash Reports</title>
+  ${ h.stylesheet_link_tag("/css/datePicker.css") }
+  ${ h.stylesheet_link_tag('/css/flora/flora.tablesorter.css') }
+  ${ h.javascript_include_tag("/js/jquery/jquery-1.2.1.js") }
+  ${ h.javascript_include_tag("/js/jquery/date.js") }
+  ${ h.javascript_include_tag("/js/jquery/plugins/ui/jquery.datePicker.js") }
+  ${ h.javascript_include_tag('/js/jquery/plugins/ui/jquery.tablesorter.min.js') }
+  <script type="text/javascript">
+  <![CDATA[
+  $(function()
+  {
+    Date.format='yyyy-mm-dd';
+    $('.date-pick').datePicker();
+    $('.date-pick').dpSetStartDate('2007-01-01');
+    $('#signatureList').tablesorter(); 
+  });
+  ]]>
+  </script>
  </head>
  <body>
   <div id="content">
 
-  <form id="quickfind" action="${h.url_for(controller='report', action='find')}"
-        py:if="not request.params.get('do_query', None)">
-   <input type="submit" value="Go to Report" />
-   <input type="text" size="24" name="id" id="crash-id"/>
-  </form>
-
-  <h1>Search Crash Reports</h1>
+  <h1 class="first">Search Crash Reports</h1>
   <form name="query" method="GET" action="${h.url_for(action='query')}">
    <input type="hidden" name="do_query" value="1" />
 
@@ -70,12 +81,13 @@
       Stack Signature
      </option>
      <option value="stack" selected="${c.params.getQuerySearch() == 'stack' and 'selected' or ''}">
-      Top 10 Stack Frames
+      One of the top 10 Stack Frames
      </option>
     </select>
     <select name="query_type">
      <option py:for="(value, desc) in (('contains', 'contains'),
-                                       ('exact', 'is exactly'))"
+                                       ('exact', 'is exactly'),
+				       ('startswith', 'starts with'))"
              value="${value}"
              selected="${c.params.getQueryType() == value and 'selected' or ''}">
       ${desc}
@@ -85,8 +97,8 @@
    </p>
 
    <p>
-    <label for="date">Occuring before date (default: now, format yyyy-mm-dd HH:MM:SS): </label>
-    <input type="text" name="date" size="20" value="${c.params.date}" />
+    <label for="date">Occurring before date (default: now, format yyyy-mm-dd HH:MM:SS): </label>
+    <input type="text" name="date" size="20" class="date-pick" value="${c.params.date}" />
     <br />
     <label for="range_value">Date range: </label>
     <input type="text" name="range_value" value="${c.params.getRange()[0]}" size="2" /><select name="range_unit">

Modified: trunk/webapp/socorro/templates/report/index.html
==============================================================================
--- trunk/webapp/socorro/templates/report/index.html	(original)
+++ trunk/webapp/socorro/templates/report/index.html	Fri Aug 29 15:36:44 2008
@@ -4,63 +4,100 @@
    xmlns:xi="http://www.w3.org/2001/XInclude";>
  <xi:include href="../includes/site.html" />
  <head>
-  <title>Breakpad Crash Report: ${c.report.uuid}</title>
- ${ h.javascript_include_tag(builtins=True) }
+  <title>[@ ${c.report.signature} ] - ${c.report.product} ${c.report.version} Crash Report - Report ID: ${c.report.uuid}</title>
+  ${ h.javascript_include_tag("/js/jquery/jquery-1.2.1.js") }
+  ${ h.javascript_include_tag("/js/jquery/plugins/ui/ui.tabs.js") }
+  ${ h.stylesheet_link_tag("/css/flora/flora.all.css") }
+  <script type="text/javascript">
+  $(document).ready(function(){
+    $('#report-index > ul').tabs();
+    $('#showallthreads').removeClass('hidden').click(function(){
+    $('#allthreads').toggle(400);
+      return false;
+    });
+    $('.signature-column').append(' <a class="expand" href="#">[Expand]</a>');
+    $('.expand').click(function(){
+      // swap cell title into cell text for each cell in this column
+      $("td:nth-child(3)", $(this).parents('tbody')).each(function(){
+        $(this).text($(this).attr('title')).removeAttr('title');
+      });
+      $(this).remove();
+      return false;
+    });
+  });
+  </script> 
  </head>
  <body>
   <div id="content">
-   <p id="report-nav">${h.link_to('Record','#record')} ${h.link_to('Frames','#frames')}
-   ${h.link_to('Modules','#modules')}</p>
-   <h1 class="first" id="record">${c.report.product} ${c.report.version} Crash Report</h1>
-   <table class="list record">
-    <tr class="row1">
-     <th>UUID</th><td>${c.report.uuid}</td>
-    </tr>
-    <tr class="row2">
-     <th>Time</th><td>${c.report.date}</td>
-    </tr>
-    <tr class="row1">
-     <th>Build ID</th><td>${c.report.build}</td>
-    </tr>
-    <tr class="row2">
-     <th>OS</th><td>${c.report.os_name}</td>
-    </tr>
-    <tr class="row1">
-     <th>OS Version</th><td>${c.report.os_version}</td>
-    </tr>
-    <tr class="row2">
-     <th>CPU</th><td>${c.report.cpu_name}</td>
-    </tr>
-    <tr class="row1">
-     <th>CPU Info</th><td>${c.report.cpu_info}</td>
-    </tr>
-    <tr class="row2">
-     <th>Crash Reason</th><td>${c.report.reason}</td>
-    </tr>
-    <tr class="row1">
-     <th>Crash Address</th><td>${c.report.address}</td>
-    </tr>
-    <tr class="row2" py:if="c.report.url is not None">
-     <?python
-        url = c.report.url
-        if len(url) > 100:
-           url = url[:100] + "&#0133;"
-     ?>
-     <th>URL</th><td>${h.link_to(url, url=c.report.url)}</td>
-    </tr>
-  </table>
-  <py:if test="c.report['dump'] is not None and len(c.report.threads) > 0">
-  <h2 id="frames">Stack Traces</h2>
-  <table class="list" py:def="stack_trace(frames)">
+   <h1 id="report-header" class="first">${c.report.product} ${c.report.version} Crash Report [@ ${c.report.signature} ]</h1>
+   <div id="report-header-details">ID: <span>${c.report.uuid}</span><br/> Signature: <span>${c.report.signature}</span></div>
+   <div id="report-index" class="flora">
+    <ul>
+     <li><a href="#details"><span>Details</span></a></li>
+     <li><a href="#frames"><span>Frames</span></a></li>
+     <li><a href="#modules"><span>Modules</span></a></li>
+     <li><a href="#rawdump"><span>Raw Dump</span></a></li>
+    </ul>
+    <div id="details">
+     <table class="list record">
+      <tr class="odd">
+       <th>Signature</th><td>${c.report.signature}</td>
+      </tr>
+      <tr class="even">
+       <th>UUID</th><td>${c.report.uuid}</td>
+      </tr>
+      <tr class="odd">
+       <th>Time</th><td>${c.report.date}</td>
+      </tr>
+      <tr class="even">
+       <th>Uptime</th><td>${c.report.uptime}</td>
+      </tr>
+      <tr class="odd">
+       <th>Product</th><td>${c.report.product}</td>
+      </tr>
+      <tr class="even">
+       <th>Version</th><td>${c.report.version}</td>
+      </tr>
+      <tr class="odd">
+       <th>Build ID</th><td>${c.report.build}</td>
+      </tr>
+      <tr class="even">
+       <th>OS</th><td>${c.report.os_name}</td>
+      </tr>
+      <tr class="odd">
+       <th>OS Version</th><td>${c.report.os_version}</td>
+      </tr>
+      <tr class="even">
+       <th>CPU</th><td>${c.report.cpu_name}</td>
+      </tr>
+      <tr class="odd">
+       <th>CPU Info</th><td>${c.report.cpu_info}</td>
+      </tr>
+      <tr class="even">
+       <th>Crash Reason</th><td>${c.report.reason}</td>
+      </tr>
+      <tr class="odd">
+       <th>Crash Address</th><td>${c.report.address}</td>
+      </tr>
+      <tr class="even">
+       <th>Comments</th><td>${c.report.comments}</td>
+      </tr>
+    </table>
+   </div>
+   <div id="frames">
+    <py:if test="c.report['dump'] is not None and len(c.report.threads) > 0">
+    <table class="list" py:def="stack_trace(frames)">
      <tr>
-      <th>frame</th>
-      <th>signature</th>
-      <th>source</th>
+      <th>Frame</th>
+      <th>Module</th>
+      <th class="signature-column">Signature</th>
+      <th>Source</th>
      </tr>
-     <?python row = 0 ?>
+     <?python row = 1 ?>
      <tr py:for="frame in frames" class="${h.get_row_class(row)}">
       <td>${frame.frame_num}</td>
-      <td>${frame.signature}</td>
+      <td>${frame.module_name}</td>
+      <td title="${frame.signature}">${frame.short_signature}</td>
       <td py:choose="frame.source_link is not None">
        <py:when test="True">
         ${h.link_to(frame.source_info, url=frame.source_link)} 
@@ -69,47 +106,48 @@
       </td>
       <?python row += 1 ?>
      </tr>
-  </table>
-  <h3>Stack of Crashing Thread</h3>
-  ${stack_trace(c.report.threads[c.report.crashed_thread])}
-  <!-- this just gives me tons of errors in the JS Console -->
-  <!-- h.parallel_effects(h.visual_effect('toggle_appear', 'allthreads'),h.visual_effect('toggle_appear', 'link_toggle') -->
-  <p id="link_toggle">${h.link_to('Click to view other threads', url='#allthreads', onclick=h.visual_effect('toggle_appear', 'allthreads') + ' return false;')}</p>
-  <div id="allthreads">
-   <py:for each="i in range(len(c.report.threads))">
-     <py:if test="i != c.report.crashed_thread">
-       <h3>Stack of Thread ${i}</h3>
-       ${stack_trace(c.report.threads[i])}
-     </py:if>
-   </py:for>
-  </div>
-  <script>document.getElementById("allthreads").style.display="none";</script>
-  </py:if>
-   <py:if test="c.report['dump'] is not None and len(c.report.modules) > 0">
-    <h2 id="modules">Modules</h2>
-    <table class="list">
-     <tr>
-      <th>filename</th>
-      <th>Version</th>
-      <th>Debug Identifier</th>
-      <th>Debug Filename</th>
-     </tr>
-     <?python row = 0 ?>
-     <tr py:for="module in c.report.modules" class="${h.get_row_class(row)}">
-      <td>${module.filename}</td>
-      <td>${module.module_version}</td>
-      <td>${module.debug_id}</td>
-      <td>${module.debug_filename}</td>
-      <?python row += 1 ?>
-     </tr>
     </table>
-   </py:if>
-  <py:if test="c.report['dump'] is not None">
-   <p>${h.link_to('Click to view raw dump.', url='#rawdump', onclick=h.visual_effect('toggle_appear', 'rawdump') + ' return false;')}</p>
-   <pre id="rawdump">${c.report['dump']}</pre>
-   <script>document.getElementById("rawdump").style.display="none";</script>
-  </py:if>
+    <h2>Crashing Thread</h2>
+    ${stack_trace(c.report.threads[c.report.crashed_thread])}
+    <p id="showallthreads" class="hidden">${h.link_to('Show/hide other threads', url='#allthreads')}</p>
+    <div id="allthreads">
+     <py:for each="i in range(len(c.report.threads))">
+      <py:if test="i != c.report.crashed_thread">
+       <h2>Thread ${i}</h2>
+       ${stack_trace(c.report.threads[i])}
+      </py:if>
+     </py:for>
+    </div>
+    <script>document.getElementById("allthreads").style.display="none";</script>
+    </py:if>
+   </div>
+   <div id="modules">
+    <py:if test="c.report['dump'] is not None and len(c.report.modules) > 0">
+     <table class="list">
+      <tr>
+       <th>Filename</th>
+       <th>Version</th>
+       <th>Debug Identifier</th>
+       <th>Debug Filename</th>
+      </tr>
+      <?python row = 1 ?>
+      <tr py:for="module in c.report.modules" class="${h.get_row_class(row)}">
+       <td>${module.filename}</td>
+       <td>${module.module_version}</td>
+       <td>${module.debug_id}</td>
+       <td>${module.debug_filename}</td>
+       <?python row += 1 ?>
+      </tr>
+     </table>
+    </py:if>
+   </div>
+   <div id="rawdump">
+    <py:if test="c.report['dump'] is not None">
+      <div class="code">${c.report['dump']}</div>
+    </py:if>
+   </div>
   </div>
-  <!-- end content -->
+ </div>
+ <!-- end content -->
  </body>
 </html>

Modified: trunk/webapp/socorro/templates/report/list.html
==============================================================================
--- trunk/webapp/socorro/templates/report/list.html	(original)
+++ trunk/webapp/socorro/templates/report/list.html	Fri Aug 29 15:36:44 2008
@@ -1,5 +1,3 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
- "http://www.w3.org/TR/html4/strict.dtd";>
 
 <?python
  from calendar import timegm
@@ -12,25 +10,41 @@
   <xi:include href="../includes/site.html" />
 <head>
   <title>Crash Reports in ${c.params.signature}</title>
-
-  <script type="text/javascript" src="${h.url_for('/js/MochiKit/MochiKit.js')}"></script>
-  <script type="text/javascript" src="${h.url_for('/js/PlotKit/excanvas.js')}"></script>
-  <script type="text/javascript" src="${h.url_for('/js/PlotKit/PlotKit_Packed.js')}"></script>
+  ${ h.javascript_include_tag('/js/MochiKit/MochiKit.js') }
+  ${ h.javascript_include_tag('/js/PlotKit/excanvas.js') }
+  ${ h.javascript_include_tag('/js/PlotKit/PlotKit_Packed.js') }
+  ${ h.javascript_include_tag('/js/jquery/jquery-1.2.1.js') }
+  ${ h.javascript_include_tag('/js/jquery/plugins/ui/ui.tabs.js') }
+  ${ h.javascript_include_tag('/js/jquery/plugins/ui/jquery.tablesorter.min.js') }
+  ${ h.stylesheet_link_tag('/css/flora/flora.all.css') }
+
+  <script type="text/javascript">
+  <![CDATA[
+  $(document).ready(function() { 
+    $('#buildid-table').tablesorter(); 
+    $('#reportsList').tablesorter({sortList:[[8,1]]});
+    $('#report-list > ul').tabs();
+  }); 
+  ]]>
+  </script>
 
   <style type="text/css">
    #buildid-outer {
      display: none;
    }
    #buildid-div {
+     float: left;
      height: 160px;
      width: 500px;
      margin-top: 0.5em;
      margin-bottom: 0.5em;
      margin-left: auto;
-     margin-right: auto;
+     margin-right: 1.5em;
    }
    #buildid-labels {
-     float: left;
+   }
+   .clear {
+    clear: both;
    }
   </style>    
 
@@ -39,54 +53,58 @@
 <body>
 
  <div id="content">
- <h1 class="first">Crash Reports in ${c.params.signature}</h1>
-
- <p>${str(c.params)}</p>
-
-
- <div id="buildid-outer">
-  <ul id="buildid-labels">
-   <li py:for="platform in platformList"
-       py:if="len(c.params.platforms) == 0 or platform in c.params.platforms"
-       style="color: ${platform.color()}">
-     ${platform.name()}
-   </li>
-  </ul>
-
-  <div id="buildid-div">
-  <canvas id="buildid-graph" width="500" height="160"></canvas>
+  <h1 class="first">Crash Reports in ${c.params.signature}</h1>
+  <p>${str(c.params)}</p>
+  <div id="report-list">
+   <ul>
+    <li><a href="#graph"><span>Graph</span></a></li>
+    <li><a href="#table"><span>Table</span></a></li>
+    <li><a href="#reports"><span>Reports</span></a></li>
+   </ul>
+   <div id="graph">
+    <div id="buildid-outer"> <div id="buildid-div"> <canvas id="buildid-graph" width="500" height="160"></canvas>
+     </div>
+     <ul id="buildid-labels">
+      <li py:for="platform in platformList"
+          py:if="len(c.params.platforms) == 0 or platform in c.params.platforms"
+          style="color: ${platform.color()}">
+        ${platform.name()}
+      </li>
+     </ul>
+    </div>
+    <div class="clear"></div>
+   </div>
+   <div id="table">
+    <table id="buildid-table" class="tablesorter">
+     <thead>
+      <tr>
+       <th>Build ID</th>
+       <th py:if="len(c.params.platforms) != 1">Crashes</th>
+       <th py:for="platform in platformList"
+           py:if="len(c.params.platforms) == 0 or platform in c.params.platforms">
+        ${platform.name()[:3]}
+       </th>
+      </tr>
+     </thead>
+     <tbody>
+      <tr py:for="build in c.builds">
+       <td class="human-buildid">${build.build_date.strftime('%Y%m%d%H')}</td>
+       <td class="crash-count" py:if="len(c.params.platforms) != 1">
+        ${build.count} - ${"%.3f%%" % (build.frequency * 100)}
+       </td>
+       <td py:for="platform in platformList"
+           py:if="len(c.params.platforms) == 0 or platform in c.params.platforms">
+        ${getattr(build, 'count_%s' % platform.id())} -
+        ${"%.3f%%" % (getattr(build, 'frequency_%s' % platform.id()) * 100)}
+       </td>
+      </tr>
+     </tbody>
+    </table>
+   </div>
+   <div id="reports">
+    ${list_reports(c.reports)}
+   </div>
   </div>
- </div>
-
- ${list_reports(c.reports)}
- 
- <table id="buildid-table" class="list">
-  <thead>
-   <tr>
-    <th>Build ID</th>
-    <th py:if="len(c.params.platforms) != 1">Crashes</th>
-    <th py:for="platform in platformList"
-        py:if="len(c.params.platforms) == 0 or platform in c.params.platforms">
-     ${platform.name()[:3]}
-    </th>
-   </tr>
-  </thead>
-  <tbody>
-   <?python row=1?>
-   <tr py:for="build in c.builds" class="${h.get_row_class(row)}">
-    <td class="human-buildid">${build.build_date.strftime('%Y%m%d%H')}</td>
-    <td class="crash-count" py:if="len(c.params.platforms) != 1">
-     ${build.count} - ${"%.3f%%" % (build.frequency * 100)}
-    </td>
-    <td py:for="platform in platformList"
-        py:if="len(c.params.platforms) == 0 or platform in c.params.platforms">
-     ${getattr(build, 'count_%s' % platform.id())} -
-     ${"%.3f%%" % (getattr(build, 'frequency_%s' % platform.id()) * 100)}
-    </td>
-    <?python row+=1 ?>
-   </tr>
-  </tbody>
- </table>
 
  </div>
 
@@ -190,16 +208,18 @@
    ]; 
    colors.pop();
 
-   var chart = new CanvasRenderer($('buildid-graph'), layout,
+   var chart = new CanvasRenderer(MochiKit.DOM.getElement('buildid-graph'), layout,
                 {IECanvasHTC: '${h.url_for('/js/PlotKit/iecanvas.htc')}',
 		 colorScheme: colors,
 		 shouldStroke: true,
 		 strokeColor: null,
-		 strokeWidth: 2});
+		 strokeWidth: 2,
+     shouldFill: false,
+     axisLabelWidth: 75});
 
      chart.render();
 
-     $('buildid-outer').style.display = 'block';
+     MochiKit.DOM.getElement('buildid-outer').style.display = 'block';
  }
  </script>
 

Modified: trunk/webapp/socorro/templates/topcrasher/bybranch.html
==============================================================================
--- trunk/webapp/socorro/templates/topcrasher/bybranch.html	(original)
+++ trunk/webapp/socorro/templates/topcrasher/bybranch.html	Fri Aug 29 15:36:44 2008
@@ -6,11 +6,23 @@
   <xi:include href="../includes/common.html" />
 <head>
   <title>Top Crashers for the ${c.branch} Branch</title>
+  ${ h.javascript_include_tag('/js/jquery/jquery-1.2.1.js') }
+  ${ h.javascript_include_tag('/js/jquery/plugins/ui/jquery.tablesorter.min.js') }
+  ${ h.stylesheet_link_tag('/css/flora/flora.tablesorter.css') }
+  <script type="text/javascript">
+  <![CDATA[
+  $(document).ready(function() 
+      { 
+          $('#signatureList').tablesorter(); 
+      } 
+  ); 
+  ]]>
+  </script>
 </head>
 
 <body>
 
-<div id="content" py:choose="c.tc.rowcount">
+<div id="content" py:choose="len(c.tc)">
  <h1 class="first">Top Crashers for the ${c.branch} Branch</h1>
   <p py:when="0">No results were found.</p>
   <div py:otherwise="">Below are the top 100 crashers over the last 2 weeks.

Modified: trunk/webapp/socorro/templates/topcrasher/byversion.html
==============================================================================
--- trunk/webapp/socorro/templates/topcrasher/byversion.html	(original)
+++ trunk/webapp/socorro/templates/topcrasher/byversion.html	Fri Aug 29 15:36:44 2008
@@ -5,16 +5,28 @@
   <xi:include href="../includes/site.html" />
   <xi:include href="../includes/common.html" />
 <head>
-  <title>Top Crashers for ${c.product} ${c.version}</title>
+  <title>Top Crashers for ${c.product} ${c.version} ${c.params.buildid}</title>
+  ${ h.javascript_include_tag('/js/jquery/jquery-1.2.1.js') }
+  ${ h.javascript_include_tag('/js/jquery/plugins/ui/jquery.tablesorter.min.js') }
+  ${ h.stylesheet_link_tag('/css/flora/flora.tablesorter.css') }
+  <script type="text/javascript">
+  <![CDATA[
+  $(document).ready(function() 
+      { 
+          $("#signatureList").tablesorter(); 
+      } 
+  ); 
+  ]]>
+  </script>
 </head>
 
 <body>
 
-<div id="content" py:choose="c.tc.rowcount">
- <h1 class="first">Top Crashers for ${c.product} ${c.version}</h1>
+<div id="content" py:choose="len(c.tc)">
+ <h1 class="first">Top Crashers for ${c.product} ${c.version} ${c.params.buildid}</h1>
   <p py:when="0">No results were found.</p>
-  <div py:otherwise="">Below are the top 100 crashers over the last 2 weeks.
-   ${list_by_signature(c.tc, c.params)}
+  <div py:otherwise="">Below are the top 100 crashers as of ${c.last_updated}
+   ${list_topcrashers(c.tc)}
   </div>
 </div>
 <!-- end content -->

Modified: trunk/webapp/socorro/templates/topcrasher/index.html
==============================================================================
--- trunk/webapp/socorro/templates/topcrasher/index.html	(original)
+++ trunk/webapp/socorro/templates/topcrasher/index.html	Fri Aug 29 15:36:44 2008
@@ -10,16 +10,6 @@
 <body>
 
 <div id="content">
-
- <h1 class="first">Top Crashers by Branch</h1>
- <ul>
-  <li py:for="row in c.branches">
-   <a href="${ h.url_for('/'+'/'.join(['topcrasher',
-            'bybranch',
-            row.branch])) }">${row.branch}</a>
-  </li>
- </ul>
- 
  <h1>Top Crashers by Version</h1>
  <ul>
   <li py:for="row in c.product_versions">
@@ -32,6 +22,5 @@
 
 </div>
 <!-- end content -->
-
 </body>
 </html>



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]