Mercurial > p > roundup > code
view roundup/cgi_client.py @ 167:a49c8a2ddd26
Added time logging and file uploading to the templates.
| author | Richard Jones <richard@users.sourceforge.net> |
|---|---|
| date | Mon, 30 Jul 2001 08:12:17 +0000 |
| parents | 8f9a65510139 |
| children | 1573a02b3254 |
line wrap: on
line source
# $Id: cgi_client.py,v 1.13 2001-07-30 08:12:17 richard Exp $ import os, cgi, pprint, StringIO, urlparse, re, traceback, mimetypes import roundupdb, htmltemplate, date class Unauthorised(ValueError): pass class Client: def __init__(self, out, db, env, user): self.out = out self.db = db self.env = env self.user = user self.path = env['PATH_INFO'] self.split_path = self.path.split('/') self.headers_done = 0 self.form = cgi.FieldStorage(environ=env) self.headers_done = 0 self.debug = 0 def header(self, headers={'Content-Type':'text/html'}): if not headers.has_key('Content-Type'): headers['Content-Type'] = 'text/html' for entry in headers.items(): self.out.write('%s: %s\n'%entry) self.out.write('\n') self.headers_done = 1 def pagehead(self, title, message=None): url = self.env['SCRIPT_NAME'] + '/' #self.env.get('PATH_INFO', '/') machine = self.env['SERVER_NAME'] port = self.env['SERVER_PORT'] if port != '80': machine = machine + ':' + port base = urlparse.urlunparse(('http', machine, url, None, None, None)) if message is not None: message = '<div class="system-msg">%s</div>'%message else: message = '' style = open(os.path.join(self.TEMPLATES, 'style.css')).read() userid = self.db.user.lookup(self.user) self.write('''<html><head> <title>%s</title> <style type="text/css">%s</style> </head> <body bgcolor=#ffffff> %s <table width=100%% border=0 cellspacing=0 cellpadding=2> <tr class="location-bar"><td><big><strong>%s</strong></big> (login: <a href="user%s">%s</a>)</td></tr> </table> '''%(title, style, message, title, userid, self.user)) def pagefoot(self): if self.debug: self.write('<hr><small><dl>') self.write('<dt><b>Path</b></dt>') self.write('<dd>%s</dd>'%(', '.join(map(repr, self.split_path)))) keys = self.form.keys() keys.sort() if keys: self.write('<dt><b>Form entries</b></dt>') for k in self.form.keys(): v = str(self.form[k].value) self.write('<dd><em>%s</em>:%s</dd>'%(k, cgi.escape(v))) keys = self.env.keys() keys.sort() self.write('<dt><b>CGI environment</b></dt>') for k in keys: v = self.env[k] self.write('<dd><em>%s</em>:%s</dd>'%(k, cgi.escape(v))) self.write('</dl></small>') self.write('</body></html>') def write(self, content): if not self.headers_done: self.header() self.out.write(content) def index_arg(self, arg): ''' handle the args to index - they might be a list from the form (ie. submitted from a form) or they might be a command-separated single string (ie. manually constructed GET args) ''' if self.form.has_key(arg): arg = self.form[arg] if type(arg) == type([]): return [arg.value for arg in arg] return arg.value.split(',') return [] def index_filterspec(self): ''' pull the index filter spec from the form Links and multilinks want to be lists - the rest are straight strings. ''' props = self.db.classes[self.classname].getprops() # all the form args not starting with ':' are filters filterspec = {} for key in self.form.keys(): if key[0] == ':': continue prop = props[key] value = self.form[key] if prop.isLinkType or prop.isMultilinkType: if type(value) == type([]): value = [arg.value for arg in value] else: value = value.value.split(',') l = filterspec.get(key, []) l = l + value filterspec[key] = l else: filterspec[key] = value.value return filterspec default_index_sort = ['-activity'] default_index_group = ['priority'] default_index_filter = [] default_index_columns = ['id','activity','title','status','assignedto'] default_index_filterspec = {'status': ['1', '2', '3', '4', '5', '6', '7']} def index(self): ''' put up an index ''' self.classname = 'issue' if self.form.has_key(':sort'): sort = self.index_arg(':sort') else: sort = self.default_index_sort if self.form.has_key(':group'): group = self.index_arg(':group') else: group = self.default_index_group if self.form.has_key(':filter'): filter = self.index_arg(':filter') else: filter = self.default_index_filter if self.form.has_key(':columns'): columns = self.index_arg(':columns') else: columns = self.default_index_columns filterspec = self.index_filterspec() if not filterspec: filterspec = self.default_index_filterspec return self.list(columns=columns, filter=filter, group=group, sort=sort, filterspec=filterspec) # XXX deviates from spec - loses the '+' (that's a reserved character # in URLS def list(self, sort=None, group=None, filter=None, columns=None, filterspec=None): ''' call the template index with the args :sort - sort by prop name, optionally preceeded with '-' to give descending or nothing for ascending sorting. :group - group by prop name, optionally preceeded with '-' or to sort in descending or nothing for ascending order. :filter - selects which props should be displayed in the filter section. Default is all. :columns - selects the columns that should be displayed. Default is all. ''' cn = self.classname self.pagehead('Index of %s'%cn) if sort is None: sort = self.index_arg(':sort') if group is None: group = self.index_arg(':group') if filter is None: filter = self.index_arg(':filter') if columns is None: columns = self.index_arg(':columns') if filterspec is None: filterspec = self.index_filterspec() htmltemplate.index(self, self.TEMPLATES, self.db, cn, filterspec, filter, columns, sort, group) self.pagefoot() def shownode(self, message=None): ''' display an item ''' cn = self.classname cl = self.db.classes[cn] # possibly perform an edit keys = self.form.keys() num_re = re.compile('^\d+$') if keys: changed = [] props = {} try: keys = self.form.keys() for key in keys: if not cl.properties.has_key(key): continue proptype = cl.properties[key] if proptype.isStringType: value = str(self.form[key].value).strip() elif proptype.isDateType: value = date.Date(str(self.form[key].value)) elif proptype.isIntervalType: value = date.Interval(str(self.form[key].value)) elif proptype.isLinkType: value = str(self.form[key].value).strip() # handle key values link = cl.properties[key].classname if not num_re.match(value): try: value = self.db.classes[link].lookup(value) except: raise ValueError, 'property "%s": %s not a %s'%( key, value, link) elif proptype.isMultilinkType: value = self.form[key] if type(value) != type([]): value = [i.strip() for i in str(value.value).split(',')] else: value = [str(i.value).strip() for i in value] link = cl.properties[key].classname l = [] for entry in map(str, value): if not num_re.match(entry): try: entry = self.db.classes[link].lookup(entry) except: raise ValueError, \ 'property "%s": %s not a %s'%(key, entry, link) l.append(entry) l.sort() value = l # if changed, set it if value != cl.get(self.nodeid, key): changed.append(key) props[key] = value cl.set(self.nodeid, **props) self._post_editnode(self.nodeid) # and some nice feedback for the user message = '%s edited ok'%', '.join(changed) except: s = StringIO.StringIO() traceback.print_exc(None, s) message = '<pre>%s</pre>'%cgi.escape(s.getvalue()) # now the display id = self.nodeid if cl.getkey(): id = cl.get(id, cl.getkey()) self.pagehead('%s: %s'%(self.classname.capitalize(), id), message) nodeid = self.nodeid # use the template to display the item htmltemplate.item(self, self.TEMPLATES, self.db, self.classname, nodeid) self.pagefoot() showissue = shownode showmsg = shownode def showuser(self, message=None): ''' display an item ''' if self.user in ('admin', self.db.user.get(self.nodeid, 'username')): self.shownode(message) else: raise Unauthorised def showfile(self): ''' display a file ''' nodeid = self.nodeid cl = self.db.file type = cl.get(nodeid, 'type') if type == 'message/rfc822': type = 'text/plain' self.header(headers={'Content-Type': type}) self.write(cl.get(nodeid, 'content')) def _createnode(self): ''' create a node based on the contents of the form ''' cn = self.classname cl = self.db.classes[cn] props = {} keys = self.form.keys() num_re = re.compile('^\d+$') for key in keys: if not cl.properties.has_key(key): continue proptype = cl.properties[key] if proptype.isStringType: value = self.form[key].value.strip() elif proptype.isDateType: value = date.Date(self.form[key].value.strip()) elif proptype.isIntervalType: value = date.Interval(self.form[key].value.strip()) elif proptype.isLinkType: value = self.form[key].value.strip() # handle key values link = cl.properties[key].classname if not num_re.match(value): try: value = self.db.classes[link].lookup(value) except: raise ValueError, 'property "%s": %s not a %s'%( key, value, link) elif proptype.isMultilinkType: value = self.form[key] if type(value) != type([]): value = [i.strip() for i in value.value.split(',')] else: value = [i.value.strip() for i in value] link = cl.properties[key].classname l = [] for entry in map(str, value): if not num_re.match(entry): try: entry = self.db.classes[link].lookup(entry) except: raise ValueError, \ 'property "%s": %s not a %s'%(key, entry, link) l.append(entry) l.sort() value = l props[key] = value return cl.create(**props) def _post_editnode(self, nid): ''' do the linking and message sending part of the node creation ''' cn = self.classname cl = self.db.classes[cn] # link if necessary keys = self.form.keys() for key in keys: if key == ':multilink': value = self.form[key].value if type(value) != type([]): value = [value] for value in value: designator, property = value.split(':') link, nodeid = roundupdb.splitDesignator(designator) link = self.db.classes[link] value = link.get(nodeid, property) value.append(nid) link.set(nodeid, **{property: value}) elif key == ':link': value = self.form[key].value if type(value) != type([]): value = [value] for value in value: designator, property = value.split(':') link, nodeid = roundupdb.splitDesignator(designator) link = self.db.classes[link] link.set(nodeid, **{property: nid}) # if this item has messages, if (cl.getprops().has_key('messages') and cl.getprops()['messages'].isMultilinkType and cl.getprops()['messages'].classname == 'msg'): # generate an edit message - nosyreactor will send it m = [] for name, prop in cl.getprops().items(): value = cl.get(nid, name) if prop.isLinkType: link = self.db.classes[prop.classname] key = link.getkey() if value is not None and key: value = link.get(value, key) else: value = '-' elif prop.isMultilinkType: l = [] link = self.db.classes[prop.classname] for entry in value: key = link.getkey() if key: l.append(link.get(entry, link.getkey())) else: l.append(entry) value = ', '.join(l) m.append('%s: %s'%(name, value)) # handle the note note = None if self.form.has_key('__note'): note = self.form['__note'] if note is not None and note.value: note = note.value if '\n' in note: summary = re.split(r'\n\r?', note)[0] else: summary = note m.append('\n%s\n'%note) else: summary = 'This %s has been created through the web.'%cn m.append('\n%s\s'%summary) # now create the message content = '\n'.join(m) message_id = self.db.msg.create(author='1', recipients=[], date=date.Date('.'), summary=summary, content=content) messages = cl.get(nid, 'messages') messages.append(message_id) props = {'messages': messages} cl.set(nid, **props) def newnode(self, message=None): ''' Add a new node to the database. The form works in two modes: blank form and submission (that is, the submission goes to the same URL). **Eventually this means that the form will have previously entered information in it if submission fails. The new node will be created with the properties specified in the form submission. For multilinks, multiple form entries are handled, as are prop=value,value,value. You can't mix them though. If the new node is to be referenced from somewhere else immediately (ie. the new node is a file that is to be attached to a support issue) then supply one of these arguments in addition to the usual form entries: :link=designator:property :multilink=designator:property ... which means that once the new node is created, the "property" on the node given by "designator" should now reference the new node's id. The node id will be appended to the multilink. ''' cn = self.classname cl = self.db.classes[cn] # possibly perform a create keys = self.form.keys() if [i for i in keys if i[0] != ':']: props = {} try: nid = self._createnode() self._post_editnode(nid) # and some nice feedback for the user message = '%s created ok'%cn except: s = StringIO.StringIO() traceback.print_exc(None, s) message = '<pre>%s</pre>'%cgi.escape(s.getvalue()) self.pagehead('New %s'%self.classname.capitalize(), message) htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname, self.form) self.pagefoot() newissue = newnode newuser = newnode def newfile(self, message=None): ''' Add a new file to the database. This form works very much the same way as newnode - it just has a file upload. ''' cn = self.classname cl = self.db.classes[cn] # possibly perform a create keys = self.form.keys() if [i for i in keys if i[0] != ':']: try: file = self.form['content'] self._post_editnode(cl.create(content=file.file.read(), type=mimetypes.guess_type(file.filename)[0], name=file.filename)) # and some nice feedback for the user message = '%s created ok'%cn except: s = StringIO.StringIO() traceback.print_exc(None, s) message = '<pre>%s</pre>'%cgi.escape(s.getvalue()) self.pagehead('New %s'%self.classname.capitalize(), message) htmltemplate.newitem(self, self.TEMPLATES, self.db, self.classname, self.form) self.pagefoot() def classes(self, message=None): ''' display a list of all the classes in the database ''' if self.user == 'admin': self.pagehead('Table of classes', message) classnames = self.db.classes.keys() classnames.sort() self.write('<table border=0 cellspacing=0 cellpadding=2>\n') for cn in classnames: cl = self.db.getclass(cn) self.write('<tr class="list-header"><th colspan=2 align=left>%s</th></tr>'%cn.capitalize()) for key, value in cl.properties.items(): if value is None: value = '' else: value = str(value) self.write('<tr><th align=left>%s</th><td>%s</td></tr>'%( key, cgi.escape(value))) self.write('</table>') self.pagefoot() else: raise Unauthorised def main(self, dre=re.compile(r'([^\d]+)(\d+)'), nre=re.compile(r'new(\w+)')): path = self.split_path if not path or path[0] in ('', 'index'): self.index() elif len(path) == 1: if path[0] == 'list_classes': self.classes() return m = dre.match(path[0]) if m: self.classname = m.group(1) self.nodeid = m.group(2) getattr(self, 'show%s'%self.classname)() return m = nre.match(path[0]) if m: self.classname = m.group(1) getattr(self, 'new%s'%self.classname)() return self.classname = path[0] self.list() else: raise 'ValueError', 'Path not understood' def __del__(self): self.db.close() # # $Log: not supported by cvs2svn $ # Revision 1.12 2001/07/30 06:26:31 richard # Added some documentation on how the newblah works. # # Revision 1.11 2001/07/30 06:17:45 richard # Features: # . Added ability for cgi newblah forms to indicate that the new node # should be linked somewhere. # Fixed: # . Fixed the agument handling for the roundup-admin find command. # . Fixed handling of summary when no note supplied for newblah. Again. # . Fixed detection of no form in htmltemplate Field display. # # Revision 1.10 2001/07/30 02:37:34 richard # Temporary measure until we have decent schema migration... # # Revision 1.9 2001/07/30 01:25:07 richard # Default implementation is now "classic" rather than "extended" as one would # expect. # # Revision 1.8 2001/07/29 08:27:40 richard # Fixed handling of passed-in values in form elements (ie. during a # drill-down) # # Revision 1.7 2001/07/29 07:01:39 richard # Added vim command to all source so that we don't get no steenkin' tabs :) # # Revision 1.6 2001/07/29 04:04:00 richard # Moved some code around allowing for subclassing to change behaviour. # # Revision 1.5 2001/07/28 08:16:52 richard # New issue form handles lack of note better now. # # Revision 1.4 2001/07/28 00:34:34 richard # Fixed some non-string node ids. # # Revision 1.3 2001/07/23 03:56:30 richard # oops, missed a config removal # # Revision 1.2 2001/07/22 12:09:32 richard # Final commit of Grande Splite # # Revision 1.1 2001/07/22 11:58:35 richard # More Grande Splite # # # vim: set filetype=python ts=4 sw=4 et si
