Skip to content

Commit 34cebf9

Browse files
author
Richard Jones
committed
Class help and generic class editing done.
1 parent 2d9b3ae commit 34cebf9

File tree

6 files changed

+159
-56
lines changed

6 files changed

+159
-56
lines changed

TODO.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,6 @@ pending web: search "refinement"
4949
pending web: have roundup.cgi pick up instance config from the environment
5050

5151
New templating TODO:
52-
. generic class editing
53-
. classhelp
5452
. rewritten documentation (can come after the beta though so stuff is settled)
5553

5654
ongoing: any bugs

roundup/cgi/client.py

Lines changed: 52 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# $Id: client.py,v 1.10 2002-09-03 07:42:38 richard Exp $
1+
# $Id: client.py,v 1.11 2002-09-04 04:31:51 richard Exp $
22

33
__doc__ = """
44
WWW request handler (also used in the stand-alone server).
@@ -110,11 +110,11 @@ def main(self):
110110
self.determine_user()
111111
# figure out the context and desired content template
112112
self.determine_context()
113-
# possibly handle a form submit action (may change self.message
114-
# and self.template_name)
113+
# possibly handle a form submit action (may change self.message,
114+
# self.classname and self.template)
115115
self.handle_action()
116116
# now render the page
117-
self.write(self.template('page', ok_message=self.ok_message,
117+
self.write(self.renderTemplate('page', '', ok_message=self.ok_message,
118118
error_message=self.error_message))
119119
except Redirect, url:
120120
# let's redirect - if the url isn't None, then we need to do
@@ -127,8 +127,7 @@ def main(self):
127127
except SendStaticFile, file:
128128
self.serve_static_file(str(file))
129129
except Unauthorised, message:
130-
self.write(self.template('page.unauthorised',
131-
error_message=message))
130+
self.write(self.renderTemplate('page', '', error_message=message))
132131
except:
133132
# everything else
134133
self.write(cgitb.html())
@@ -207,9 +206,9 @@ def determine_context(self, dre=re.compile(r'([^\d]+)(\d+)')):
207206
full item designator supplied: "item"
208207
209208
We set:
210-
self.classname
211-
self.nodeid
212-
self.template_name
209+
self.classname - the class to display, can be None
210+
self.template - the template to render the current context with
211+
self.nodeid - the nodeid of the class we're displaying
213212
'''
214213
# default the optional variables
215214
self.classname = None
@@ -219,11 +218,9 @@ def determine_context(self, dre=re.compile(r'([^\d]+)(\d+)')):
219218
path = self.split_path
220219
if not path or path[0] in ('', 'home', 'index'):
221220
if self.form.has_key(':template'):
222-
self.template_type = self.form[':template'].value
223-
self.template_name = 'home' + '.' + self.template_type
221+
self.template = self.form[':template'].value
224222
else:
225-
self.template_type = ''
226-
self.template_name = 'home'
223+
self.template = ''
227224
return
228225
elif path[0] == '_file':
229226
raise SendStaticFile, path[1]
@@ -239,14 +236,14 @@ def determine_context(self, dre=re.compile(r'([^\d]+)(\d+)')):
239236
self.classname = m.group(1)
240237
self.nodeid = m.group(2)
241238
# with a designator, we default to item view
242-
self.template_type = 'item'
239+
self.template = 'item'
243240
else:
244241
# with only a class, we default to index view
245-
self.template_type = 'index'
242+
self.template = 'index'
246243

247244
# see if we have a template override
248245
if self.form.has_key(':template'):
249-
self.template_type = self.form[':template'].value
246+
self.template = self.form[':template'].value
250247

251248

252249
# see if we were passed in a message
@@ -255,9 +252,6 @@ def determine_context(self, dre=re.compile(r'([^\d]+)(\d+)')):
255252
if self.form.has_key(':error_message'):
256253
self.error_message.append(self.form[':error_message'].value)
257254

258-
# we have the template name now
259-
self.template_name = self.classname + '.' + self.template_type
260-
261255
def serve_file(self, designator, dre=re.compile(r'([^\d]+)(\d+)')):
262256
''' Serve the file from the content property of the designated item.
263257
'''
@@ -279,10 +273,10 @@ def serve_static_file(self, file):
279273
self.header({'Content-Type': mt})
280274
self.write(open(os.path.join(self.instance.TEMPLATES, file)).read())
281275

282-
def template(self, name, **kwargs):
276+
def renderTemplate(self, name, extension, **kwargs):
283277
''' Return a PageTemplate for the named page
284278
'''
285-
pt = getTemplate(self.instance.TEMPLATES, name)
279+
pt = getTemplate(self.instance.TEMPLATES, name, extension)
286280
# XXX handle PT rendering errors here more nicely
287281
try:
288282
# let the template render figure stuff out
@@ -297,14 +291,23 @@ def template(self, name, **kwargs):
297291
def content(self):
298292
''' Callback used by the page template to render the content of
299293
the page.
294+
295+
If we don't have a specific class to display, that is none was
296+
determined in determine_context(), then we display a "home"
297+
template.
300298
'''
301299
# now render the page content using the template we determined in
302300
# determine_context
303-
return self.template(self.template_name)
301+
if self.classname is None:
302+
name = 'home'
303+
else:
304+
name = self.classname
305+
return self.renderTemplate(self.classname, self.template)
304306

305307
# these are the actions that are available
306308
actions = {
307309
'edit': 'editItemAction',
310+
'editCSV': 'editCSVAction',
308311
'new': 'newItemAction',
309312
'register': 'registerAction',
310313
'login': 'login_action',
@@ -631,14 +634,17 @@ def newItemAction(self):
631634
self.error_message.append(
632635
_('You do not have permission to create %s' %self.classname))
633636

634-
# XXX
635-
# cl = self.db.classes[cn]
636-
# if self.form.has_key(':multilink'):
637-
# link = self.form[':multilink'].value
638-
# designator, linkprop = link.split(':')
639-
# xtra = ' for <a href="%s">%s</a>' % (designator, designator)
640-
# else:
641-
# xtra = ''
637+
# create a little extra message for anticipated :link / :multilink
638+
if self.form.has_key(':multilink'):
639+
link = self.form[':multilink'].value
640+
elif self.form.has_key(':link'):
641+
link = self.form[':multilink'].value
642+
else:
643+
link = None
644+
xtra = ''
645+
if link:
646+
designator, linkprop = link.split(':')
647+
xtra = ' for <a href="%s">%s</a>'%(designator, designator)
642648

643649
try:
644650
# do the create
@@ -654,7 +660,7 @@ def newItemAction(self):
654660
self.nodeid = nid
655661

656662
# and some nice feedback for the user
657-
message = _('%(classname)s created ok')%self.__dict__
663+
message = _('%(classname)s created ok')%self.__dict__ + xtra
658664
except (ValueError, KeyError), message:
659665
self.error_message.append(_('Error: ') + str(message))
660666
return
@@ -686,15 +692,15 @@ def newItemPermission(self, props):
686692
return 1
687693
return 0
688694

689-
def genericEditAction(self):
695+
def editCSVAction(self):
690696
''' Performs an edit of all of a class' items in one go.
691697
692698
The "rows" CGI var defines the CSV-formatted entries for the
693699
class. New nodes are identified by the ID 'X' (or any other
694700
non-existent ID) and removed lines are retired.
695701
'''
696-
# generic edit is per-class only
697-
if not self.genericEditPermission():
702+
# this is per-class only
703+
if not self.editCSVPermission():
698704
self.error_message.append(
699705
_('You do not have permission to edit %s' %self.classname))
700706

@@ -709,26 +715,32 @@ def genericEditAction(self):
709715

710716
cl = self.db.classes[self.classname]
711717
idlessprops = cl.getprops(protected=0).keys()
718+
idlessprops.sort()
712719
props = ['id'] + idlessprops
713720

714721
# do the edit
715722
rows = self.form['rows'].value.splitlines()
716723
p = csv.parser()
717724
found = {}
718725
line = 0
719-
for row in rows:
726+
for row in rows[1:]:
720727
line += 1
721728
values = p.parse(row)
722729
# not a complete row, keep going
723730
if not values: continue
724731

732+
# skip property names header
733+
if values == props:
734+
continue
735+
725736
# extract the nodeid
726737
nodeid, values = values[0], values[1:]
727738
found[nodeid] = 1
728739

729740
# confirm correct weight
730741
if len(idlessprops) != len(values):
731-
message=(_('Not enough values on line %(line)s'%{'line':line}))
742+
self.error_message.append(
743+
_('Not enough values on line %(line)s')%{'line':line})
732744
return
733745

734746
# extract the new values
@@ -755,13 +767,12 @@ def genericEditAction(self):
755767
if not found.has_key(nodeid):
756768
cl.retire(nodeid)
757769

758-
message = _('items edited OK')
770+
# all OK
771+
self.db.commit()
759772

760-
# redirect to the class' edit page
761-
raise Redirect, '%s/%s?:ok_message=%s'%(self.base, self.classname,
762-
urllib.quote(message))
773+
self.ok_message.append(_('Items edited OK'))
763774

764-
def genericEditPermission(self):
775+
def editCSVPermission(self):
765776
''' Determine whether the user has permission to edit this class.
766777
767778
Base behaviour is to check the user can edit this class.

roundup/cgi/templating.py

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import sys, cgi, urllib, os, re, os.path, time
1+
import sys, cgi, urllib, os, re, os.path, time, errno
22

33
from roundup import hyperdb, date
44
from roundup.i18n import _
@@ -80,14 +80,37 @@
8080

8181
templates = {}
8282

83-
def getTemplate(dir, name, classname=None, request=None):
83+
def getTemplate(dir, name, extension, classname=None, request=None):
8484
''' Interface to get a template, possibly loading a compiled template.
85+
86+
"name" and "extension" indicate the template we're after, which in
87+
most cases will be "name.extension". If "extension" is None, then
88+
we look for a template just called "name" with no extension.
89+
90+
If the file "name.extension" doesn't exist, we look for
91+
"_generic.extension" as a fallback.
8592
'''
86-
# find the source, figure the time it was last modified
87-
src = os.path.join(dir, name)
88-
stime = os.stat(src)[os.path.stat.ST_MTIME]
93+
# default the name to "home"
94+
if name is None:
95+
name = 'home'
8996

90-
key = (dir, name)
97+
# find the source, figure the time it was last modified
98+
if extension:
99+
filename = '%s.%s'%(name, extension)
100+
else:
101+
filename = name
102+
src = os.path.join(dir, filename)
103+
try:
104+
stime = os.stat(src)[os.path.stat.ST_MTIME]
105+
except os.error, error:
106+
if error.errno != errno.ENOENT or not extension:
107+
raise
108+
# try for a generic template
109+
filename = '_generic.%s'%extension
110+
src = os.path.join(dir, filename)
111+
stime = os.stat(src)[os.path.stat.ST_MTIME]
112+
113+
key = (dir, filename)
91114
if templates.has_key(key) and stime < templates[key].mtime:
92115
# compiled template is up to date
93116
return templates[key]
@@ -262,6 +285,40 @@ def list(self):
262285
l = [HTMLItem(self.db, self.classname, x) for x in self.klass.list()]
263286
return l
264287

288+
def csv(self):
289+
''' Return the items of this class as a chunk of CSV text.
290+
'''
291+
# get the CSV module
292+
try:
293+
import csv
294+
except ImportError:
295+
return 'Sorry, you need the csv module to use this function.\n'\
296+
'Get it from: http://www.object-craft.com.au/projects/csv/'
297+
298+
props = self.propnames()
299+
p = csv.parser()
300+
s = StringIO.StringIO()
301+
s.write(p.join(props) + '\n')
302+
for nodeid in self.klass.list():
303+
l = []
304+
for name in props:
305+
value = self.klass.get(nodeid, name)
306+
if value is None:
307+
l.append('')
308+
elif isinstance(value, type([])):
309+
l.append(':'.join(map(str, value)))
310+
else:
311+
l.append(str(self.klass.get(nodeid, name)))
312+
s.write(p.join(l) + '\n')
313+
return s.getvalue()
314+
315+
def propnames(self):
316+
''' Return the list of the names of the properties of this class.
317+
'''
318+
idlessprops = self.klass.getprops(protected=0).keys()
319+
idlessprops.sort()
320+
return ['id'] + idlessprops
321+
265322
def filter(self, request=None):
266323
''' Return a list of items from this class, filtered and sorted
267324
by the current requested filterspec/filter/sort/group args
@@ -285,7 +342,7 @@ def classhelp(self, properties, label='?', width='400', height='400'):
285342
You may optionally override the label displayed, the width and
286343
height. The popup window will be resizable and scrollable.
287344
'''
288-
return '<a href="javascript:help_window(\'classhelp?classname=%s&' \
345+
return '<a href="javascript:help_window(\'%s?:template=help&' \
289346
'properties=%s\', \'%s\', \'%s\')"><b>(%s)</b></a>'%(self.classname,
290347
properties, width, height, label)
291348

@@ -307,8 +364,7 @@ def renderWith(self, name, **kwargs):
307364
req.update(kwargs)
308365

309366
# new template, using the specified classname and request
310-
name = self.classname + '.' + name
311-
pt = getTemplate(self.db.config.TEMPLATES, name)
367+
pt = getTemplate(self.db.config.TEMPLATES, self.classname, name)
312368

313369
# XXX handle PT rendering errors here nicely
314370
try:
@@ -946,7 +1002,7 @@ class HTMLRequest:
9461002
"base" the base URL for this instance
9471003
"user" a HTMLUser instance for this user
9481004
"classname" the current classname (possibly None)
949-
"template_type" the current template type (suffix, also possibly None)
1005+
"template" the current template (suffix, also possibly None)
9501006
9511007
Index args:
9521008
"columns" dictionary of the columns to display in an index page
@@ -971,7 +1027,7 @@ def __init__(self, client):
9711027

9721028
# store the current class name and action
9731029
self.classname = client.classname
974-
self.template_type = client.template_type
1030+
self.template = client.template
9751031

9761032
# extract the index display information from the form
9771033
self.columns = []
@@ -1055,7 +1111,7 @@ def __str__(self):
10551111
url: %(url)r
10561112
base: %(base)r
10571113
classname: %(classname)r
1058-
template_type: %(template_type)r
1114+
template: %(template)r
10591115
columns: %(columns)r
10601116
sort: %(sort)r
10611117
group: %(group)r
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<table tal:define="props python:request.form['properties'].value.split(',')"
2+
border=1 cellspacing=0 cellpadding=2>
3+
<tr>
4+
<th align=left tal:repeat="prop props" tal:content="prop"></th>
5+
</tr>
6+
<tr tal:repeat="item klass/list">
7+
<td align="left" valign="top" tal:repeat="prop props"
8+
tal:content="python:item[prop]"></td>
9+
</tr>
10+
</table>
11+

0 commit comments

Comments
 (0)