From 14642adc5aff91f10f74f997ed2edf22ba58f29a Mon Sep 17 00:00:00 2001 From: Vas Crabb Date: Wed, 2 Aug 2017 00:41:09 +1000 Subject: [PATCH] minimaws web mode enhancements: * Support serving static assets, use for stylesheet, script and images * Better error pages, reject unsupported HTTP methods * Replace lists with sortable tables with more detail (click headings to sort) * Add pages for exploring source files, link from machine pages - Can start from full source file list at http://localhost:8080/sourcefile/ (nw) JavaScript performance can drop when sorting really big tables, e.g. the list of all source files, or the list of machines in some of the fruit machine drivers. This update doesn't expose machine/device information, just consolidating what's there. The wsgiref server is adding headers to prevent caching, I'll look for a workaround. --- scripts/minimaws/lib/assets/common.js | 55 ++++ scripts/minimaws/lib/assets/sortasc.png | Bin 0 -> 1157 bytes scripts/minimaws/lib/assets/sortdesc.png | Bin 0 -> 1195 bytes scripts/minimaws/lib/assets/sortind.png | Bin 0 -> 1247 bytes scripts/minimaws/lib/assets/style.css | 8 + scripts/minimaws/lib/dbaccess.py | 44 ++- scripts/minimaws/lib/htmltmpl.py | 110 ++++++- scripts/minimaws/lib/wsgiserve.py | 357 ++++++++++++++++++----- 8 files changed, 488 insertions(+), 86 deletions(-) create mode 100644 scripts/minimaws/lib/assets/common.js create mode 100644 scripts/minimaws/lib/assets/sortasc.png create mode 100644 scripts/minimaws/lib/assets/sortdesc.png create mode 100644 scripts/minimaws/lib/assets/sortind.png create mode 100644 scripts/minimaws/lib/assets/style.css diff --git a/scripts/minimaws/lib/assets/common.js b/scripts/minimaws/lib/assets/common.js new file mode 100644 index 00000000000..8ab57941127 --- /dev/null +++ b/scripts/minimaws/lib/assets/common.js @@ -0,0 +1,55 @@ +// license:BSD-3-Clause +// copyright-holders:Vas Crabb + +function sort_table(tbl, col, dir, numeric) +{ + var tbody = tbl.tBodies[0]; + var trows = Array.prototype.slice.call(tbody.rows, 0).sort( + function (x, y) + { + if (numeric) + return dir * (parseInt(x.cells[col].textContent) - parseInt(y.cells[col].textContent)); + else + return dir * x.cells[col].textContent.localeCompare(y.cells[col].textContent); + }) + trows.forEach(function (row) { tbody.appendChild(row); }); +} + + +function make_table_sortable(tbl) +{ + var headers = tbl.tHead.rows[0].cells; + var i; + for (i = 0; i < headers.length; i++) + { + (function (col) + { + var dir = 1; + var sorticon = document.createElement("img"); + sorticon.setAttribute("src", assetsurl + "/sortind.png"); + sorticon.style.cssFloat = "right"; + sorticon.style.marginLeft = "0.5em"; + headers[col].appendChild(sorticon); + headers[col].addEventListener( + 'click', + function () + { + imgsrc = sorticon.getAttribute("src"); + imgsrc = imgsrc.substr(imgsrc.lastIndexOf('/') + 1); + if (imgsrc != 'sortind.png') + dir = -dir; + if (dir < 0) + sorticon.setAttribute("src", assetsurl + "/sortdesc.png"); + else + sorticon.setAttribute("src", assetsurl + "/sortasc.png"); + var i; + for (i = 0; i < headers.length; i++) + { + if (i != col) + headers[i].lastChild.setAttribute("src", assetsurl + "/sortind.png"); + } + sort_table(tbl, col, dir, headers[col].getAttribute("class") == "numeric"); + }); + }(i)); + } +} diff --git a/scripts/minimaws/lib/assets/sortasc.png b/scripts/minimaws/lib/assets/sortasc.png new file mode 100644 index 0000000000000000000000000000000000000000..29b893cdd0229fc49e9fd394db5855eb314fe5a0 GIT binary patch literal 1157 zcmaJ>U1$_n7@c*~Xk!+Gl{OU%*NI{kcm8&>>yEqGWHY0#bwXGdx`JS4XKu37Waf@D zH|``4691zHG5Dgyms0Sd1>bzAeMtNPO@l3sA&G@zMes>bkwQvo?VThW`;c^)VeY+i z&iT&w-S5u%_O`vDnvFFSMTN5aq#SvFLY}%c)f5%D@cGmwGWitC1$+>7;x8u~G6!{oPtZr_)U!>4`+ZgjfZh*<-n)m!ju&MKbo6SsKhk zaCeGcFjY|6K`U|~Xk>)2#&JA|MHoJkh(;1Sfxz(`>)%3{+a*RD#Rvx$FPcPi^iDA+ z?OBXPW+}Rau`RM}xm;$-5e7M3ET2duSWaLCAxscqx6i_=7q;Anih=}P%`t3jAPe}4 zY7zC~6iu8iq+r@hvX;A8CQ>lgQ*D-KI6tKdP*MILYMM)E7w6!6zsm}{`92%6Iq0HZ zM=>i&aa5 zSt&&mEygf(F&5*6Oth7c$#R_M<#-}4M>ElAG$Y01@tD9>xDwKOeeDWYf6oMMKUR`kLWGpxVG|Jm}&66qs< zxYWR8vUCKXMf%<$4L-p)O_Ia$Dl4_*y8cg!Wn2VQ*t z^W1^y%=5Pc$%_+@o(^37NV(V4bD`zVAFD@tsleH1oyp^4)y+?S4^CanHy&QQeavcq zY#%Sxf(H+eeO3Qct@3hq@~yr<^z`Drj(Uckd3~*__N%X8O-xS>N=;0uEIgTN!@$6-lNl0G65;D(m7Jfemk3g$SCLx))Xl(PV_#8_n4Fzj zqL7rDo|$K>^nUk#C56lsTcvPQUjyF)=hTc$kE){7;3~h6MhI|Z8xE1&_n zsU?XD6}dTi#a0!zN?>!XfNYSkzLEl1NlCV?62wsvz5xo(`9-M;rg}!Y$p!|73TDQ7 zhQ^jA#+Et?Mh1ok`XFSaYhYnzVrpehm3bwJ6}oxF$}kgLQj3#|G7CyF^YauyCMG83 zmzLNn0bL65LT&-v*t}wBFaZNhzap_f-%!s0(sSEWOOk6e}|`Ln9{>XHzp*S93!{S942q zR}&`_6B8#Fb8~YuBLkRTm;B^Xkl8T3DG0r0IQ4=OMQ#DmW|!2W%(B!Jx1#)91+eF> zGI6`b7^itqy(zfeVuDkzKF~4xpeRO)a+nY>9f6qe1PtWBvp{MdFx?jc6L`k#p0B_> zChFBs;7_RJj22kX_?R2T#t3IbCkLl-D0J1`hJr5bdc?Iu!j3n*S9TH8Y+V98Wq}ro(2>A}TH-&A0Ud+oXf{8zMxUd0E>& zGkj!U@i6Mfl??_HI2&f|XDHsyIbkJVO8ntvYz)r37v0~pRp9}s`0#Y~b6Mw<&;$Tl Cq=usa literal 0 HcmV?d00001 diff --git a/scripts/minimaws/lib/assets/sortind.png b/scripts/minimaws/lib/assets/sortind.png new file mode 100644 index 0000000000000000000000000000000000000000..773dd6aec023c00e3c8b75139268121f518972ef GIT binary patch literal 1247 zcmeAS@N?(olHy`uVBq!ia0vp^;y^6G!N$PA*rjo$56F=$ag8Vm&QB{TPb^Aha7@Wh zN>%X8O-xS>N=;0uEIgTN!@$6-lNl0G65;D(m7Jfemk3g$SCLx))Xl(PV_#8_n4Fzj zqL7rDo|$K>^nUk#C56lsTcvPQUjyF)=hTc$kE){7;3~h6MhI|Z8xE1&_n zsU?XD6}dTi#a0!zN?>!XfNYSkzLEl1NlCV?62wsvz5xo(`9-M;rg}!Y$p!|73TDQ7 zhQ^jA#+Et?Mh1ok`XFSaYhYnzVrpehm3bwJ6}oxF$}kgLQj3#|G7CyF^YauyCMG83 zmzLNn0bL65LT&-v*t}wBFaZNhzap_f-%!s0(sSEWOOk6f0LlOA7;6M-www7Xw2>S942q zR}&`_6B8#Fb8~YuBLkRTm;B^Xkl8T3DG0r$IQ4=OMQ#DmW|!2W%(B!Jx1#)91+eF> zGO@VD!qnN(#Lx|Bp0gWvw?Oo!U~vmnuNh9g`as9%gQ6HI%3(slbOd6;6EKhi&jP7= zz;s^(OyJMr+be;2%+u4wF{I*FQd&a749+978-Iu@Gqe0*7i3^$N&dsmv}1+5z-ML_ zfi4{ZZ-v!OjWa#C{sFnln-)48;0Qj;a-@8M$}&SEqbdby<{+`YcS18*>^HGEFf827 zC28M&vQgxa0ndlziw#KvyVwLi99SyRmA^Qfp)pS5uRyzTIunb4-uLDYh6-tJ<^SVk zx^6a9SPMK?&`vwH=!1HdkMi!{#U!2F7CzhrOIE#Ih_jq-z+Gzopr0G%S25C8xG literal 0 HcmV?d00001 diff --git a/scripts/minimaws/lib/assets/style.css b/scripts/minimaws/lib/assets/style.css new file mode 100644 index 00000000000..7e9e0125cd2 --- /dev/null +++ b/scripts/minimaws/lib/assets/style.css @@ -0,0 +1,8 @@ +/* license:BSD-3-Clause + * copyright-holders:Vas Crabb + */ + +th { text-align: left; background-color: #ddd; padding: 0.25em } +td { padding-left: 0.25em; padding-right: 0.25em } + +table[class=sysinfo] th { text-align: right } diff --git a/scripts/minimaws/lib/dbaccess.py b/scripts/minimaws/lib/dbaccess.py index 3358b538b3c..e6ff07261fe 100644 --- a/scripts/minimaws/lib/dbaccess.py +++ b/scripts/minimaws/lib/dbaccess.py @@ -167,20 +167,48 @@ class QueryCursor(object): def get_devices_referenced(self, machine): return self.dbcurs.execute( - 'SELECT devicereference.device AS shortname, machine.description AS description ' \ - 'FROM devicereference LEFT JOIN machine ON devicereference.device = machine.shortname ' \ - 'WHERE devicereference.machine = ? ' \ - 'ORDER BY machine.description ASC, devicereference.device ASC', + 'SELECT devicereference.device AS shortname, machine.description AS description, sourcefile.filename AS sourcefile ' \ + 'FROM devicereference LEFT JOIN machine ON devicereference.device = machine.shortname LEFT JOIN sourcefile ON machine.sourcefile = sourcefile.id ' \ + 'WHERE devicereference.machine = ?', (machine, )) def get_device_references(self, shortname): return self.dbcurs.execute( - 'SELECT shortname, description ' \ - 'FROM machine ' \ - 'WHERE id IN (SELECT machine FROM devicereference WHERE device = ?) ' \ - 'ORDER BY description ASC', + 'SELECT machine.shortname AS shortname, machine.description AS description, sourcefile.filename AS sourcefile ' \ + 'FROM machine JOIN sourcefile ON machine.sourcefile = sourcefile.id ' \ + 'WHERE machine.id IN (SELECT machine FROM devicereference WHERE device = ?)', (shortname, )) + def get_sourcefile_id(self, filename): + return (self.dbcurs.execute('SELECT id FROM sourcefile WHERE filename = ?', (filename, )).fetchone() or (None, ))[0] + + def get_sourcefile_machines(self, id): + return self.dbcurs.execute( + 'SELECT machine.shortname AS shortname, machine.description AS description, machine.isdevice AS isdevice, machine.runnable AS runnable, sourcefile.filename AS sourcefile, system.year AS year, system.manufacturer AS manufacturer, cloneof.parent AS cloneof, romof.parent AS romof ' \ + 'FROM machine JOIN sourcefile ON machine.sourcefile = sourcefile.id LEFT JOIN system ON machine.id = system.id LEFT JOIN cloneof ON system.id = cloneof.id LEFT JOIN romof ON system.id = romof.id ' \ + 'WHERE machine.sourcefile = ?', + (id, )) + + def get_sourcefiles(self, pattern): + if pattern is not None: + return self.dbcurs.execute( + 'SELECT sourcefile.filename AS filename, COUNT(machine.id) AS machines ' \ + 'FROM sourcefile LEFT JOIN machine ON sourcefile.id = machine.sourcefile ' \ + 'WHERE sourcefile.filename GLOB ?' \ + 'GROUP BY sourcefile.id ', + (pattern, )) + else: + return self.dbcurs.execute( + 'SELECT sourcefile.filename AS filename, COUNT(machine.id) AS machines ' \ + 'FROM sourcefile LEFT JOIN machine ON sourcefile.id = machine.sourcefile ' \ + 'GROUP BY sourcefile.id') + + def count_sourcefiles(self, pattern): + if pattern is not None: + return self.dbcurs.execute('SELECT COUNT(*) FROM sourcefile WHERE filename GLOB ?', (pattern, )).fetchone()[0] + else: + return self.dbcurs.execute('SELECT COUNT(*) FROM sourcefile').fetchone()[0] + class UpdateCursor(object): def __init__(self, dbconn, **kwargs): diff --git a/scripts/minimaws/lib/htmltmpl.py b/scripts/minimaws/lib/htmltmpl.py index 2c71fea9fb9..106f9615e9d 100644 --- a/scripts/minimaws/lib/htmltmpl.py +++ b/scripts/minimaws/lib/htmltmpl.py @@ -6,17 +6,115 @@ import string +ERROR_PAGE = string.Template( + '\n' \ + '\n' \ + '\n' \ + ' \n' \ + ' ${code} ${message}\n' \ + '\n' \ + '\n' \ + '

${message}

\n' \ + '\n' \ + '\n') + + MACHINE_PROLOGUE = string.Template( '\n' \ '\n' \ '\n' \ ' \n' \ - ' ${description} (${shortname})\n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' Machine: ${description} (${shortname})\n' \ '\n' \ '\n' \ '

${description}

\n' \ - '\n' \ - ' \n' \ - ' \n' \ - ' \n' \ - ' \n') + '
Short name:${shortname}
Is device:${isdevice}
Runnable:${runnable}
Source file:${sourcefile}
\n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n') + +MACHINE_ROW = string.Template( + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n') + +EXCL_MACHINE_ROW = string.Template( + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n') + + +SOURCEFILE_PROLOGUE = string.Template( + '\n' \ + '\n' \ + '\n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' Source File: ${filename}\n' \ + '\n' \ + '\n' \ + '

${title}

\n') + +SOURCEFILE_ROW_PARENT = string.Template( + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n') + +SOURCEFILE_ROW_CLONE = string.Template( + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n') + + +SOURCEFILE_LIST_PROLOGUE = string.Template( + '\n' \ + '\n' \ + '\n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' ${title}\n' \ + '\n' \ + '\n' \ + '

${heading}

\n' \ + '
Short name:${shortname}
Is device:${isdevice}
Runnable:${runnable}
Source file:${sourcefile}
${shortname}${description}${sourcefile}
${shortname}
${shortname}${description}${year}${manufacturer}${runnable}
${shortname}${description}${year}${manufacturer}${runnable}${parent}
\n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n') + +SOURCEFILE_LIST_ROW = string.Template( + ' \n' \ + ' \n' \ + ' \n' \ + ' \n') diff --git a/scripts/minimaws/lib/wsgiserve.py b/scripts/minimaws/lib/wsgiserve.py index 96ea43f35c5..641c535c54b 100644 --- a/scripts/minimaws/lib/wsgiserve.py +++ b/scripts/minimaws/lib/wsgiserve.py @@ -7,6 +7,9 @@ from . import dbaccess from . import htmltmpl import cgi +import inspect +import mimetypes +import os.path import sys import wsgiref.simple_server import wsgiref.util @@ -17,113 +20,323 @@ else: import urlparse -class MachineHandler(object): +class HandlerBase(object): + STATUS_MESSAGE = { + 400: 'Bad Request', + 401: 'Unauthorized', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', + 505: 'HTTP Version Not Supported' } + def __init__(self, app, application_uri, environ, start_response, **kwargs): - super(MachineHandler, self).__init__(**kwargs) + super(HandlerBase, self).__init__(**kwargs) + self.app = app self.application_uri = application_uri self.environ = environ self.start_response = start_response + + def error_page(self, code): + yield htmltmpl.ERROR_PAGE.substitute(code=cgi.escape('%d' % code), message=cgi.escape(self.STATUS_MESSAGE[code])).encode('utf-8') + + +class ErrorPageHandler(HandlerBase): + def __init__(self, code, app, application_uri, environ, start_response, **kwargs): + super(ErrorPageHandler, self).__init__(app=app, application_uri=application_uri, environ=environ, start_response=start_response, **kwargs) + self.code = code + self.start_response('%d %s' % (self.code, self.STATUS_MESSAGE[code]), [('Content-type', 'text/html; charset=utf-8')]) + + def __iter__(self): + return self.error_page(self.code) + + +class AssetHandler(HandlerBase): + def __init__(self, directory, app, application_uri, environ, start_response, **kwargs): + super(AssetHandler, self).__init__(app=app, application_uri=application_uri, environ=environ, start_response=start_response, **kwargs) + self.directory = directory + self.asset = wsgiref.util.shift_path_info(environ) + + def __iter__(self): + if not self.asset: + self.start_response('403 %s' % (self.STATUS_MESSAGE[403], ), [('Content-type', 'text/html; charset=utf-8')]) + return self.error_page(403) + elif self.environ['PATH_INFO']: + self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8')]) + return self.error_page(404) + else: + path = os.path.join(self.directory, self.asset) + if not os.path.isfile(path): + self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8')]) + return self.error_page(404) + elif self.environ['REQUEST_METHOD'] != 'GET': + self.start_response('405 %s' % (self.STATUS_MESSAGE[405], ), [('Content-type', 'text/html; charset=utf-8'), ('Accept', 'GET, HEAD, OPTIONS')]) + return self.error_page(405) + else: + try: + f = open(path, 'rb') + type, encoding = mimetypes.guess_type(path) + self.start_response('200 OK', [('Content-type', type or 'application/octet-stream')]) + return wsgiref.util.FileWrapper(f) + except: + self.start_response('500 %s' % (self.STATUS_MESSAGE[500], ), [('Content-type', 'text/html; charset=utf-8')]) + return self.error_page(500) + + +class QueryPageHandler(HandlerBase): + def __init__(self, app, application_uri, environ, start_response, **kwargs): + super(QueryPageHandler, self).__init__(app=app, application_uri=application_uri, environ=environ, start_response=start_response, **kwargs) self.dbcurs = app.dbconn.cursor() + + def machine_href(self, shortname): + return cgi.escape(urlparse.urljoin(self.application_uri, 'machine/%s' % (shortname, )), True) + + def sourcefile_href(self, sourcefile): + return cgi.escape(urlparse.urljoin(self.application_uri, 'sourcefile/%s' % (sourcefile, )), True) + + +class MachineHandler(QueryPageHandler): + def __init__(self, app, application_uri, environ, start_response, **kwargs): + super(MachineHandler, self).__init__(app=app, application_uri=application_uri, environ=environ, start_response=start_response, **kwargs) self.shortname = wsgiref.util.shift_path_info(environ) def __iter__(self): if not self.shortname: # could probably list machines here or something - self.start_response('403 Forbidden', [('Content-type', 'text/plain')]) - yield '403 Forbidden'.encode('utf-8') + self.start_response('403 %s' % (self.STATUS_MESSAGE[403], ), [('Content-type', 'text/html; charset=utf-8')]) + return self.error_page(403) elif self.environ['PATH_INFO']: # subdirectory of a machine - self.start_response('404 Not Found', [('Content-type', 'text/plain')]) - yield '404 Not Found'.encode('utf-8') + self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8')]) + return self.error_page(404) else: machine_info = self.dbcurs.get_machine_info(self.shortname).fetchone() if not machine_info: - self.start_response('404 Not Found', [('Content-type', 'text/plain')]) - yield '404 Not Found'.encode('utf-8') + self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8')]) + return self.error_page(404) + elif self.environ['REQUEST_METHOD'] != 'GET': + self.start_response('405 %s' % (self.STATUS_MESSAGE[405], ), [('Content-type', 'text/html; charset=utf-8'), ('Accept', 'GET, HEAD, OPTIONS')]) + return self.error_page(405) else: self.start_response('200 OK', [('Content-type', 'text/html; chearset=utf-8')]) - description = machine_info['description'] - yield htmltmpl.MACHINE_PROLOGUE.substitute( - description=cgi.escape(description), - shortname=cgi.escape(self.shortname), - isdevice=cgi.escape('Yes' if machine_info['isdevice'] else 'No'), - runnable=cgi.escape('Yes' if machine_info['runnable'] else 'No'), - sourcefile=cgi.escape(machine_info['sourcefile'])).encode('utf-8') - if machine_info['year'] is not None: - yield ( - ' \n' \ - ' \n' % - (cgi.escape(machine_info['year']), cgi.escape(machine_info['Manufacturer']))).encode('utf-8') - if machine_info['cloneof'] is not None: - parent = self.dbcurs.listfull(machine_info['cloneof']).fetchone() - if parent: - yield ( - ' \n' % - (cgi.escape('%s/machine/%s' % (self.application_uri, machine_info['cloneof']), True), cgi.escape(parent[1]), cgi.escape(machine_info['cloneof']))).encode('utf-8') + return self.machine_page(machine_info) + + def machine_page(self, machine_info): + description = machine_info['description'] + yield htmltmpl.MACHINE_PROLOGUE.substitute( + assets=cgi.escape(urlparse.urljoin(self.application_uri, 'static'), True), + sourcehref=self.sourcefile_href(machine_info['sourcefile']), + description=cgi.escape(description), + shortname=cgi.escape(self.shortname), + isdevice=cgi.escape('Yes' if machine_info['isdevice'] else 'No'), + runnable=cgi.escape('Yes' if machine_info['runnable'] else 'No'), + sourcefile=cgi.escape(machine_info['sourcefile'])).encode('utf-8') + if machine_info['year'] is not None: + yield ( + ' \n' \ + ' \n' % + (cgi.escape(machine_info['year']), cgi.escape(machine_info['Manufacturer']))).encode('utf-8') + if machine_info['cloneof'] is not None: + parent = self.dbcurs.listfull(machine_info['cloneof']).fetchone() + if parent: + yield ( + ' \n' % + (cgi.escape('%s/machine/%s' % (self.application_uri, machine_info['cloneof']), True), cgi.escape(parent[1]), cgi.escape(machine_info['cloneof']))).encode('utf-8') + else: + yield ( + ' \n' % + (cgi.escape('%s/machine/%s' % (self.application_uri, machine_info['cloneof']), True), cgi.escape(machine_info['cloneof']))).encode('utf-8') + if (machine_info['romof'] is not None) and (machine_info['romof'] != machine_info['cloneof']): + parent = self.dbcurs.listfull(machine_info['romof']).fetchone() + if parent: + yield ( + ' \n' % + (cgi.escape('%s/machine/%s' % (self.application_uri, machine_info['romof']), True), cgi.escape(parent[1]), cgi.escape(machine_info['romof']))).encode('utf-8') + else: + yield ( + ' \n' % + (cgi.escape('%s/machine/%s' % (self.application_uri, machine_info['romof']), True), cgi.escape(machine_info['romof']))).encode('utf-8') + yield '
Source fileMachines
${sourcefile}${machines}
Year:%s
Manufacturer:%s
Parent Machine:%s (%s)
Year:%s
Manufacturer:%s
Parent Machine:%s (%s)
Parent Machine:%s
Parent ROM set:%s (%s)
Parent Machine:%s
\n'.encode('utf-8') + + first = True + for name, desc, src in self.dbcurs.get_devices_referenced(machine_info['id']): + if first: + yield \ + '

Devices Referenced

\n' \ + '\n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n'.encode('utf-8') + first = False + yield self.machine_row(name, desc, src) + if not first: + yield ' \n
Short nameDescriptionSource file
\n\n'.encode('utf-8') + + first = True + for name, desc, src in self.dbcurs.get_device_references(self.shortname): + if first: + yield \ + '

Referenced By

\n' \ + '\n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n'.encode('utf-8') + first = False + yield self.machine_row(name, desc, src) + if not first: + yield ' \n
Short nameDescriptionSource file
\n\n'.encode('utf-8') + + yield '\n'.encode('utf-8') + + def machine_row(self, shortname, description, sourcefile): + return (htmltmpl.MACHINE_ROW if description is not None else htmltmpl.EXCL_MACHINE_ROW).substitute( + machinehref=self.machine_href(shortname), + sourcehref=self.sourcefile_href(sourcefile), + shortname=cgi.escape(shortname), + description=cgi.escape(description or ''), + sourcefile=cgi.escape(sourcefile or '')).encode('utf-8') + + +class SourceFileHandler(QueryPageHandler): + def __init__(self, app, application_uri, environ, start_response, **kwargs): + super(SourceFileHandler, self).__init__(app=app, application_uri=application_uri, environ=environ, start_response=start_response, **kwargs) + + def __iter__(self): + self.filename = self.environ['PATH_INFO'] + if self.filename and (self.filename[0] == '/'): + self.filename = self.filename[1:] + if not self.filename: + if self.environ['REQUEST_METHOD'] != 'GET': + self.start_response('405 %s' % (self.STATUS_MESSAGE[405], ), [('Content-type', 'text/html; charset=utf-8'), ('Accept', 'GET, HEAD, OPTIONS')]) + return self.error_page(405) + else: + self.start_response('200 OK', [('Content-type', 'text/html; chearset=utf-8')]) + return self.sourcefile_listing_page(None) + else: + id = self.dbcurs.get_sourcefile_id(self.filename) + if id is None: + if ('*' not in self.filename) and ('?' not in self.filename) and ('?' not in self.filename): + self.filename += '*' if self.filename[-1] == '/' else '/*' + if not self.dbcurs.count_sourcefiles(self.filename): + self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8')]) + return self.error_page(404) + elif self.environ['REQUEST_METHOD'] != 'GET': + self.start_response('405 %s' % (self.STATUS_MESSAGE[405], ), [('Content-type', 'text/html; charset=utf-8'), ('Accept', 'GET, HEAD, OPTIONS')]) + return self.error_page(405) else: - yield ( - ' Parent Machine:%s\n' % - (cgi.escape('%s/machine/%s' % (self.application_uri, machine_info['cloneof']), True), cgi.escape(machine_info['cloneof']))).encode('utf-8') - if (machine_info['romof'] is not None) and (machine_info['romof'] != machine_info['cloneof']): - parent = self.dbcurs.listfull(machine_info['romof']).fetchone() - if parent: - yield ( - ' Parent ROM set:%s (%s)\n' % - (cgi.escape('%s/machine/%s' % (self.application_uri, machine_info['romof']), True), cgi.escape(parent[1]), cgi.escape(machine_info['romof']))).encode('utf-8') - else: - yield ( - ' Parent Machine:%s\n' % - (cgi.escape('%s/machine/%s' % (self.application_uri, machine_info['romof']), True), cgi.escape(machine_info['romof']))).encode('utf-8') - yield '\n'.encode('utf-8') + self.start_response('200 OK', [('Content-type', 'text/html; chearset=utf-8')]) + return self.sourcefile_listing_page(self.filename) + else: + self.start_response('404 %s' % (self.STATUS_MESSAGE[404], ), [('Content-type', 'text/html; charset=utf-8')]) + return self.error_page(404) + elif self.environ['REQUEST_METHOD'] != 'GET': + self.start_response('405 %s' % (self.STATUS_MESSAGE[405], ), [('Content-type', 'text/html; charset=utf-8'), ('Accept', 'GET, HEAD, OPTIONS')]) + return self.error_page(405) + else: + self.start_response('200 OK', [('Content-type', 'text/html; chearset=utf-8')]) + return self.sourcefile_page(id) - devices = self.dbcurs.get_devices_referenced(machine_info['id']) - first = True - for name, desc in devices: - if first: - yield '

Devices Referenced

\n
    \n'.encode('utf-8') - first = False - if desc is not None: - yield ( - '
  • %s (%s)
  • \n' % - (self.machine_href(name), cgi.escape(desc), cgi.escape(name))).encode('utf-8') - else: - yield ( - '
  • %s
  • \n' % - (self.machine_href(name), cgi.escape(name))).encode('utf-8') - if not first: - yield '
\n'.encode('utf-8') + def sourcefile_listing_page(self, pattern): + if not pattern: + title = heading = 'All Source Files' + else: + heading = self.linked_title(pattern) + title = 'Source Files: ' + cgi.escape(pattern) + yield htmltmpl.SOURCEFILE_LIST_PROLOGUE.substitute( + assets=cgi.escape(urlparse.urljoin(self.application_uri, 'static'), True), + title=title, + heading=heading).encode('utf-8') + for filename, machines in self.dbcurs.get_sourcefiles(pattern): + yield htmltmpl.SOURCEFILE_LIST_ROW.substitute( + sourcefile=self.linked_title(filename, True), + machines=cgi.escape('%d' % machines)).encode('utf-8') + yield ' \n\n\n\n\n'.encode('utf-8') - devices = self.dbcurs.get_device_references(self.shortname) - first = True - for name, desc in devices: - if first: - yield '

Referenced By

\n
    \n'.encode('utf-8') - first = False - yield ( - '
  • %s (%s)
  • \n' % - (self.machine_href(name), cgi.escape(desc), cgi.escape(name))).encode('utf-8') - if not first: - yield '
\n'.encode('utf-8') + def sourcefile_page(self, id): + yield htmltmpl.SOURCEFILE_PROLOGUE.substitute( + assets=cgi.escape(urlparse.urljoin(self.application_uri, 'static'), True), + filename=cgi.escape(self.filename), + title=self.linked_title(self.filename)).encode('utf-8') - yield '\n'.encode('utf-8') + first = True + for machine_info in self.dbcurs.get_sourcefile_machines(id): + if first: + yield \ + '\n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n' \ + ' \n'.encode('utf-8') + first = False + yield self.machine_row(machine_info) + if first: + yield '

No machines found.

\n'.encode('utf-8') + else: + yield ' \n
Short nameDescriptionYearManufacturerRunnableParent
\n\n'.encode('utf-8') - def machine_href(self, shortname): - return cgi.escape(urlparse.urljoin(self.application_uri, 'machine/%s' % (shortname, )), True) + yield '\n\n'.encode('utf-8') + + def linked_title(self, filename, linkfinal=False): + parts = filename.split('/') + final = parts[-1] + del parts[-1] + uri = urlparse.urljoin(self.application_uri, 'sourcefile') + title = '' + for part in parts: + uri = urlparse.urljoin(uri + '/', part) + title += '{1}/'.format(cgi.escape(uri, True), cgi.escape(part)) + if linkfinal: + uri = urlparse.urljoin(uri + '/', final) + return title + '{1}'.format(cgi.escape(uri, True), cgi.escape(final)) + else: + return title + final + + def machine_row(self, machine_info): + return (htmltmpl.SOURCEFILE_ROW_PARENT if machine_info['cloneof'] is None else htmltmpl.SOURCEFILE_ROW_CLONE).substitute( + machinehref=self.machine_href(machine_info['shortname']), + parenthref=self.machine_href(machine_info['cloneof'] or '__invalid'), + shortname=cgi.escape(machine_info['shortname']), + description=cgi.escape(machine_info['description']), + year=cgi.escape(machine_info['year'] or ''), + manufacturer=cgi.escape(machine_info['manufacturer'] or ''), + runnable=cgi.escape('Yes' if machine_info['runnable'] else 'No'), + parent=cgi.escape(machine_info['cloneof'] or '')).encode('utf-8') class MiniMawsApp(object): def __init__(self, dbfile, **kwargs): super(MiniMawsApp, self).__init__(**kwargs) self.dbconn = dbaccess.QueryConnection(dbfile) + self.assetsdir = os.path.join(os.path.dirname(inspect.getfile(self.__class__)), 'assets') + if not mimetypes.inited: + mimetypes.init() def __call__(self, environ, start_response): application_uri = wsgiref.util.application_uri(environ) module = wsgiref.util.shift_path_info(environ) if module == 'machine': return MachineHandler(self, application_uri, environ, start_response) + elif module == 'sourcefile': + return SourceFileHandler(self, application_uri, environ, start_response) + elif module == 'static': + return AssetHandler(self.assetsdir, self, application_uri, environ, start_response) + elif not module: + return ErrorPageHandler(403, self, application_uri, environ, start_response) else: - start_response('200 OK', [('Content-type', 'text/plain')]) - return ('Module is %s\n' % (module, ), ) + return ErrorPageHandler(404, self, application_uri, environ, start_response) def run_server(options):