diff --git a/roundup/cgi/client.py b/roundup/cgi/client.py
index 9c786f99376c60c31ab7bb438c4ea9d493024829..122c0bc07a936cbfe03d271bd0155e5a94ea4b3d 100644 (file)
--- a/roundup/cgi/client.py
+++ b/roundup/cgi/client.py
-# $Id: client.py,v 1.8 2002-09-03 03:23:56 richard Exp $
+# $Id: client.py,v 1.19 2002-09-06 07:21:31 richard Exp $
__doc__ = """
WWW request handler (also used in the stand-alone server).
__doc__ = """
WWW request handler (also used in the stand-alone server).
self.debug = 0
def main(self):
self.debug = 0
def main(self):
- ''' Wrap the request and handle unauthorised requests
+ ''' Process a request.
+
+ The most common requests are handled like so:
+ 1. figure out who we are, defaulting to the "anonymous" user
+ see determine_user
+ 2. figure out what the request is for - the context
+ see determine_context
+ 3. handle any requested action (item edit, search, ...)
+ see handle_action
+ 4. render a template, resulting in HTML output
+
+ In some situations, exceptions occur:
+ - HTTP Redirect (generally raised by an action)
+ - SendFile (generally raised by determine_context)
+ serve up a FileClass "content" property
+ - SendStaticFile (generally raised by determine_context)
+ serve up a file from the tracker "html" directory
+ - Unauthorised (generally raised by an action)
+ the action is cancelled, the request is rendered and an error
+ message is displayed indicating that permission was not
+ granted for the action to take place
+ - NotFound (raised wherever it needs to be)
+ percolates up to the CGI interface that called the client
'''
self.content_action = None
self.ok_message = []
'''
self.content_action = None
self.ok_message = []
self.determine_user()
# figure out the context and desired content template
self.determine_context()
self.determine_user()
# figure out the context and desired content template
self.determine_context()
- # possibly handle a form submit action (may change self.message
- # and self.template_name)
+ # possibly handle a form submit action (may change self.classname
+ # and self.template, and may also append error/ok_messages)
self.handle_action()
# now render the page
self.handle_action()
# now render the page
- self.write(self.template('page', ok_message=self.ok_message,
- error_message=self.error_message))
+ if self.form.has_key(':contentonly'):
+ # just the content
+ self.write(self.content())
+ else:
+ # render the content inside the page template
+ self.write(self.renderTemplate('page', '',
+ ok_message=self.ok_message,
+ error_message=self.error_message))
except Redirect, url:
# let's redirect - if the url isn't None, then we need to do
# the headers, otherwise the headers have been set before the
except Redirect, url:
# let's redirect - if the url isn't None, then we need to do
# the headers, otherwise the headers have been set before the
except SendFile, designator:
self.serve_file(designator)
except SendStaticFile, file:
except SendFile, designator:
self.serve_file(designator)
except SendStaticFile, file:
- self.serve_static_file(file)
+ self.serve_static_file(str(file))
except Unauthorised, message:
except Unauthorised, message:
- self.write(self.template('page.unauthorised',
- error_message=message))
+ self.write(self.renderTemplate('page', '', error_message=message))
except:
# everything else
self.write(cgitb.html())
except:
# everything else
self.write(cgitb.html())
self.opendb(self.user)
def determine_context(self, dre=re.compile(r'([^\d]+)(\d+)')):
self.opendb(self.user)
def determine_context(self, dre=re.compile(r'([^\d]+)(\d+)')):
- ''' Determine the context of this page:
-
- home (default if no url is given)
- classname
- designator (classname and nodeid)
-
- The desired template to be rendered is also determined There
- are two exceptional contexts:
-
- _file - serve up a static file
- path len > 1 - serve up a FileClass content
- (the additional path gives the browser a
- nicer filename to save as)
+ ''' Determine the context of this page from the URL:
+
+ The URL path after the instance identifier is examined. The path
+ is generally only one entry long.
+
+ - if there is no path, then we are in the "home" context.
+ * if the path is "_file", then the additional path entry
+ specifies the filename of a static file we're to serve up
+ from the instance "html" directory. Raises a SendStaticFile
+ exception.
+ - if there is something in the path (eg "issue"), it identifies
+ the tracker class we're to display.
+ - if the path is an item designator (eg "issue123"), then we're
+ to display a specific item.
+ * if the path starts with an item designator and is longer than
+ one entry, then we're assumed to be handling an item of a
+ FileClass, and the extra path information gives the filename
+ that the client is going to label the download with (ie
+ "file123/image.png" is nicer to download than "file123"). This
+ raises a SendFile exception.
+
+ Both of the "*" types of contexts stop before we bother to
+ determine the template we're going to use. That's because they
+ don't actually use templates.
The template used is specified by the :template CGI variable,
which defaults to:
The template used is specified by the :template CGI variable,
which defaults to:
+
only classname suplied: "index"
full item designator supplied: "item"
We set:
only classname suplied: "index"
full item designator supplied: "item"
We set:
- self.classname
- self.nodeid
- self.template_name
+ self.classname - the class to display, can be None
+ self.template - the template to render the current context with
+ self.nodeid - the nodeid of the class we're displaying
'''
# default the optional variables
self.classname = None
'''
# default the optional variables
self.classname = None
path = self.split_path
if not path or path[0] in ('', 'home', 'index'):
if self.form.has_key(':template'):
path = self.split_path
if not path or path[0] in ('', 'home', 'index'):
if self.form.has_key(':template'):
- self.template_type = self.form[':template'].value
- self.template_name = 'home' + '.' + self.template_type
+ self.template = self.form[':template'].value
else:
else:
- self.template_type = ''
- self.template_name = 'home'
+ self.template = ''
return
elif path[0] == '_file':
raise SendStaticFile, path[1]
return
elif path[0] == '_file':
raise SendStaticFile, path[1]
self.classname = m.group(1)
self.nodeid = m.group(2)
# with a designator, we default to item view
self.classname = m.group(1)
self.nodeid = m.group(2)
# with a designator, we default to item view
- self.template_type = 'item'
+ self.template = 'item'
else:
# with only a class, we default to index view
else:
# with only a class, we default to index view
- self.template_type = 'index'
+ self.template = 'index'
# see if we have a template override
if self.form.has_key(':template'):
# see if we have a template override
if self.form.has_key(':template'):
- self.template_type = self.form[':template'].value
+ self.template = self.form[':template'].value
# see if we were passed in a message
# see if we were passed in a message
if self.form.has_key(':error_message'):
self.error_message.append(self.form[':error_message'].value)
if self.form.has_key(':error_message'):
self.error_message.append(self.form[':error_message'].value)
- # we have the template name now
- self.template_name = self.classname + '.' + self.template_type
-
def serve_file(self, designator, dre=re.compile(r'([^\d]+)(\d+)')):
''' Serve the file from the content property of the designated item.
'''
def serve_file(self, designator, dre=re.compile(r'([^\d]+)(\d+)')):
''' Serve the file from the content property of the designated item.
'''
self.header({'Content-Type': mt})
self.write(open(os.path.join(self.instance.TEMPLATES, file)).read())
self.header({'Content-Type': mt})
self.write(open(os.path.join(self.instance.TEMPLATES, file)).read())
- def template(self, name, **kwargs):
+ def renderTemplate(self, name, extension, **kwargs):
''' Return a PageTemplate for the named page
'''
''' Return a PageTemplate for the named page
'''
- pt = getTemplate(self.instance.TEMPLATES, name)
+ pt = getTemplate(self.instance.TEMPLATES, name, extension)
# XXX handle PT rendering errors here more nicely
try:
# let the template render figure stuff out
# XXX handle PT rendering errors here more nicely
try:
# let the template render figure stuff out
'<li>'.join(pt._v_errors))
except:
# everything else
'<li>'.join(pt._v_errors))
except:
# everything else
- return cgitb.html()
+ return cgitb.pt_html()
def content(self):
''' Callback used by the page template to render the content of
the page.
def content(self):
''' Callback used by the page template to render the content of
the page.
+
+ If we don't have a specific class to display, that is none was
+ determined in determine_context(), then we display a "home"
+ template.
'''
# now render the page content using the template we determined in
# determine_context
'''
# now render the page content using the template we determined in
# determine_context
- return self.template(self.template_name)
+ if self.classname is None:
+ name = 'home'
+ else:
+ name = self.classname
+ return self.renderTemplate(self.classname, self.template)
# these are the actions that are available
actions = {
'edit': 'editItemAction',
# these are the actions that are available
actions = {
'edit': 'editItemAction',
+ 'editCSV': 'editCSVAction',
'new': 'newItemAction',
'register': 'registerAction',
'new': 'newItemAction',
'register': 'registerAction',
- 'login': 'login_action',
+ 'login': 'loginAction',
'logout': 'logout_action',
'search': 'searchAction',
}
'logout': 'logout_action',
'search': 'searchAction',
}
"edit" -> self.editItemAction
"new" -> self.newItemAction
"register" -> self.registerAction
"edit" -> self.editItemAction
"new" -> self.newItemAction
"register" -> self.registerAction
- "login" -> self.login_action
+ "login" -> self.loginAction
"logout" -> self.logout_action
"search" -> self.searchAction
"logout" -> self.logout_action
"search" -> self.searchAction
getattr(self, self.actions[action])()
except Redirect:
raise
getattr(self, self.actions[action])()
except Redirect:
raise
+ except Unauthorised:
+ raise
except:
self.db.rollback()
s = StringIO.StringIO()
except:
self.db.rollback()
s = StringIO.StringIO()
#
# Actions
#
#
# Actions
#
- def login_action(self):
- ''' Attempt to log a user in and set the cookie
+ def loginAction(self):
+ ''' Attempt to log a user in.
+
+ Sets up a session for the user which contains the login
+ credentials.
'''
# we need the username at a minimum
if not self.form.has_key('__login_name'):
'''
# we need the username at a minimum
if not self.form.has_key('__login_name'):
self.error_message.append(_('Incorrect password'))
return
self.error_message.append(_('Incorrect password'))
return
+ # make sure we're allowed to be here
+ if not self.loginPermission():
+ self.make_user_anonymous()
+ raise Unauthorised, _("You do not have permission to login")
+
# set the session cookie
self.set_cookie(self.user, password)
# set the session cookie
self.set_cookie(self.user, password)
+ def loginPermission(self):
+ ''' Determine whether the user has permission to log in.
+
+ Base behaviour is to check the user has "Web Access".
+ '''
+ if not self.db.security.hasPermission('Web Access', self.userid):
+ return 0
+ return 1
+
def logout_action(self):
''' Make us really anonymous - nuke the cookie too
'''
def logout_action(self):
''' Make us really anonymous - nuke the cookie too
'''
Create a file and attach it to the current node's
"files" property. Attach the file to the message created from
the __note if it's supplied.
Create a file and attach it to the current node's
"files" property. Attach the file to the message created from
the __note if it's supplied.
+
+ :required=property,property,...
+ The named properties are required to be filled in the form.
+
'''
cl = self.db.classes[self.classname]
'''
cl = self.db.classes[self.classname]
def newItemAction(self):
''' Add a new item to the database.
def newItemAction(self):
''' Add a new item to the database.
- This follows the same form as the editItemAction
+ This follows the same form as the editItemAction, with the same
+ special form values.
'''
cl = self.db.classes[self.classname]
'''
cl = self.db.classes[self.classname]
self.error_message.append(
_('You do not have permission to create %s' %self.classname))
self.error_message.append(
_('You do not have permission to create %s' %self.classname))
- # XXX
-# cl = self.db.classes[cn]
-# if self.form.has_key(':multilink'):
-# link = self.form[':multilink'].value
-# designator, linkprop = link.split(':')
-# xtra = ' for <a href="%s">%s</a>' % (designator, designator)
-# else:
-# xtra = ''
+ # create a little extra message for anticipated :link / :multilink
+ if self.form.has_key(':multilink'):
+ link = self.form[':multilink'].value
+ elif self.form.has_key(':link'):
+ link = self.form[':multilink'].value
+ else:
+ link = None
+ xtra = ''
+ if link:
+ designator, linkprop = link.split(':')
+ xtra = ' for <a href="%s">%s</a>'%(designator, designator)
try:
# do the create
try:
# do the create
self.nodeid = nid
# and some nice feedback for the user
self.nodeid = nid
# and some nice feedback for the user
- message = _('%(classname)s created ok')%self.__dict__
+ message = _('%(classname)s created ok')%self.__dict__ + xtra
except (ValueError, KeyError), message:
self.error_message.append(_('Error: ') + str(message))
return
except (ValueError, KeyError), message:
self.error_message.append(_('Error: ') + str(message))
return
return 1
return 0
return 1
return 0
- def genericEditAction(self):
+ def editCSVAction(self):
''' Performs an edit of all of a class' items in one go.
The "rows" CGI var defines the CSV-formatted entries for the
class. New nodes are identified by the ID 'X' (or any other
non-existent ID) and removed lines are retired.
'''
''' Performs an edit of all of a class' items in one go.
The "rows" CGI var defines the CSV-formatted entries for the
class. New nodes are identified by the ID 'X' (or any other
non-existent ID) and removed lines are retired.
'''
- # generic edit is per-class only
- if not self.genericEditPermission():
+ # this is per-class only
+ if not self.editCSVPermission():
self.error_message.append(
_('You do not have permission to edit %s' %self.classname))
self.error_message.append(
_('You do not have permission to edit %s' %self.classname))
cl = self.db.classes[self.classname]
idlessprops = cl.getprops(protected=0).keys()
cl = self.db.classes[self.classname]
idlessprops = cl.getprops(protected=0).keys()
+ idlessprops.sort()
props = ['id'] + idlessprops
# do the edit
props = ['id'] + idlessprops
# do the edit
p = csv.parser()
found = {}
line = 0
p = csv.parser()
found = {}
line = 0
- for row in rows:
+ for row in rows[1:]:
line += 1
values = p.parse(row)
# not a complete row, keep going
if not values: continue
line += 1
values = p.parse(row)
# not a complete row, keep going
if not values: continue
+ # skip property names header
+ if values == props:
+ continue
+
# extract the nodeid
nodeid, values = values[0], values[1:]
found[nodeid] = 1
# confirm correct weight
if len(idlessprops) != len(values):
# extract the nodeid
nodeid, values = values[0], values[1:]
found[nodeid] = 1
# confirm correct weight
if len(idlessprops) != len(values):
- message=(_('Not enough values on line %(line)s'%{'line':line}))
+ self.error_message.append(
+ _('Not enough values on line %(line)s')%{'line':line})
return
# extract the new values
return
# extract the new values
if not found.has_key(nodeid):
cl.retire(nodeid)
if not found.has_key(nodeid):
cl.retire(nodeid)
- message = _('items edited OK')
+ # all OK
+ self.db.commit()
- # redirect to the class' edit page
- raise Redirect, '%s/%s?:ok_message=%s'%(self.base, self.classname,
- urllib.quote(message))
+ self.ok_message.append(_('Items edited OK'))
- def genericEditPermission(self):
+ def editCSVPermission(self):
''' Determine whether the user has permission to edit this class.
Base behaviour is to check the user can edit this class.
''' Determine whether the user has permission to edit this class.
Base behaviour is to check the user can edit this class.
# commit the query change to the database
self.db.commit()
# commit the query change to the database
self.db.commit()
-
def searchPermission(self):
''' Determine whether the user has permission to search this class.
def searchPermission(self):
''' Determine whether the user has permission to search this class.
return 0
return 1
return 0
return 1
- def XXXremove_action(self, dre=re.compile(r'([^\d]+)(\d+)')):
+ def remove_action(self, dre=re.compile(r'([^\d]+)(\d+)')):
# XXX I believe this could be handled by a regular edit action that
# just sets the multilink...
# XXX I believe this could be handled by a regular edit action that
# just sets the multilink...
- # XXX handle this !
target = self.index_arg(':target')[0]
m = dre.match(target)
if m:
target = self.index_arg(':target')[0]
m = dre.match(target)
if m:
def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
def parsePropsFromForm(db, cl, form, nodeid=0, num_re=re.compile('^\d+$')):
- '''Pull properties for the given class out of the form.
+ ''' Pull properties for the given class out of the form.
+
+ If a ":required" parameter is supplied, then the names property values
+ must be supplied or a ValueError will be raised.
'''
'''
+ required = []
+ print form.keys()
+ if form.has_key(':required'):
+ value = form[':required']
+ print 'required', value
+ if isinstance(value, type([])):
+ required = [i.value.strip() for i in value]
+ else:
+ required = [i.strip() for i in value.value.split(',')]
+
props = {}
keys = form.keys()
for key in keys:
if not cl.properties.has_key(key):
continue
proptype = cl.properties[key]
props = {}
keys = form.keys()
for key in keys:
if not cl.properties.has_key(key):
continue
proptype = cl.properties[key]
+
+ # Get the form value. This value may be a MiniFieldStorage or a list
+ # of MiniFieldStorages.
+ value = form[key]
+
+ # make sure non-multilinks only get one value
+ if not isinstance(proptype, hyperdb.Multilink):
+ if isinstance(value, type([])):
+ raise ValueError, 'You have submitted more than one value'\
+ ' for the %s property'%key
+ # we've got a MiniFieldStorage, so pull out the value and strip
+ # surrounding whitespace
+ value = value.value.strip()
+
if isinstance(proptype, hyperdb.String):
if isinstance(proptype, hyperdb.String):
- value = form[key].value.strip()
+ if not value:
+ continue
elif isinstance(proptype, hyperdb.Password):
elif isinstance(proptype, hyperdb.Password):
- value = form[key].value.strip()
if not value:
# ignore empty password values
continue
value = password.Password(value)
elif isinstance(proptype, hyperdb.Date):
if not value:
# ignore empty password values
continue
value = password.Password(value)
elif isinstance(proptype, hyperdb.Date):
- value = form[key].value.strip()
if value:
value = date.Date(form[key].value.strip())
else:
value = None
elif isinstance(proptype, hyperdb.Interval):
if value:
value = date.Date(form[key].value.strip())
else:
value = None
elif isinstance(proptype, hyperdb.Interval):
- value = form[key].value.strip()
if value:
value = date.Interval(form[key].value.strip())
else:
value = None
elif isinstance(proptype, hyperdb.Link):
if value:
value = date.Interval(form[key].value.strip())
else:
value = None
elif isinstance(proptype, hyperdb.Link):
- value = form[key].value.strip()
# see if it's the "no selection" choice
if value == '-1':
value = None
# see if it's the "no selection" choice
if value == '-1':
value = None
'%(value)s not a %(classname)s')%{'propname':key,
'value': value, 'classname': link}
elif isinstance(proptype, hyperdb.Multilink):
'%(value)s not a %(classname)s')%{'propname':key,
'value': value, 'classname': link}
elif isinstance(proptype, hyperdb.Multilink):
- value = form[key]
- if not isinstance(value, type([])):
- value = [i.strip() for i in value.split(',')]
- else:
+ if isinstance(value, type([])):
+ # it's a list of MiniFieldStorages
value = [i.value.strip() for i in value]
value = [i.value.strip() for i in value]
+ else:
+ # it's a MiniFieldStorage, but may be a comma-separated list
+ # of values
+ value = [i.strip() for i in value.value.split(',')]
link = cl.properties[key].classname
l = []
for entry in map(str, value):
link = cl.properties[key].classname
l = []
for entry in map(str, value):
l.sort()
value = l
elif isinstance(proptype, hyperdb.Boolean):
l.sort()
value = l
elif isinstance(proptype, hyperdb.Boolean):
- value = form[key].value.strip()
props[key] = value = value.lower() in ('yes', 'true', 'on', '1')
elif isinstance(proptype, hyperdb.Number):
props[key] = value = value.lower() in ('yes', 'true', 'on', '1')
elif isinstance(proptype, hyperdb.Number):
- value = form[key].value.strip()
props[key] = value = int(value)
props[key] = value = int(value)
+ # register this as received if required
+ if key in required:
+ required.remove(key)
+
# get the old value
if nodeid:
try:
# get the old value
if nodeid:
try:
props[key] = value
else:
props[key] = value
props[key] = value
else:
props[key] = value
+
+ # see if all the required properties have been supplied
+ if required:
+ raise ValueError, 'Required properties %s not supplied'%(
+ ', '.join(required))
+
return props
return props