1 #$Id: actions.py,v 1.15 2004-03-26 00:44:11 richard Exp $
3 import re, cgi, StringIO, urllib, Cookie, time, random
5 from roundup import hyperdb, token, date, password, rcsv
6 from roundup.i18n import _
7 from roundup.cgi import templating
8 from roundup.cgi.exceptions import Redirect, Unauthorised, SeriousError
9 from roundup.mailgw import uidFromAddress
11 __all__ = ['Action', 'ShowAction', 'RetireAction', 'SearchAction',
12 'EditCSVAction', 'EditItemAction', 'PassResetAction',
13 'ConfRegoAction', 'RegisterAction', 'LoginAction', 'LogoutAction',
14 'NewItemAction', 'ExportCSVAction']
16 # used by a couple of routines
17 chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
19 class Action:
20 def __init__(self, client):
21 self.client = client
22 self.form = client.form
23 self.db = client.db
24 self.nodeid = client.nodeid
25 self.template = client.template
26 self.classname = client.classname
27 self.userid = client.userid
28 self.base = client.base
29 self.user = client.user
31 def execute(self):
32 """Execute the action specified by this object."""
33 self.permission()
34 self.handle()
36 name = ''
37 permissionType = None
38 def permission(self):
39 """Check whether the user has permission to execute this action.
41 True by default. If the permissionType attribute is a string containing
42 a simple permission, check whether the user has that permission.
43 Subclasses must also define the name attribute if they define
44 permissionType.
46 Despite having this permission, users may still be unauthorised to
47 perform parts of actions. It is up to the subclasses to detect this.
48 """
49 if (self.permissionType and
50 not self.hasPermission(self.permissionType)):
51 info = {'action': self.name, 'classname': self.classname}
52 raise Unauthorised, _('You do not have permission to '
53 '%(action)s the %(classname)s class.')%info
55 def hasPermission(self, permission):
56 """Check whether the user has 'permission' on the current class."""
57 return self.db.security.hasPermission(permission, self.client.userid,
58 self.client.classname)
60 class ShowAction(Action):
61 def handle(self, typere=re.compile('[@:]type'),
62 numre=re.compile('[@:]number')):
63 """Show a node of a particular class/id."""
64 t = n = ''
65 for key in self.form.keys():
66 if typere.match(key):
67 t = self.form[key].value.strip()
68 elif numre.match(key):
69 n = self.form[key].value.strip()
70 if not t:
71 raise ValueError, 'No type specified'
72 if not n:
73 raise SeriousError, _('No ID entered')
74 try:
75 int(n)
76 except ValueError:
77 d = {'input': n, 'classname': t}
78 raise SeriousError, _(
79 '"%(input)s" is not an ID (%(classname)s ID required)')%d
80 url = '%s%s%s'%(self.db.config.TRACKER_WEB, t, n)
81 raise Redirect, url
83 class RetireAction(Action):
84 name = 'retire'
85 permissionType = 'Edit'
87 def handle(self):
88 """Retire the context item."""
89 # if we want to view the index template now, then unset the nodeid
90 # context info (a special-case for retire actions on the index page)
91 nodeid = self.nodeid
92 if self.template == 'index':
93 self.client.nodeid = None
95 # make sure we don't try to retire admin or anonymous
96 if self.classname == 'user' and \
97 self.db.user.get(nodeid, 'username') in ('admin', 'anonymous'):
98 raise ValueError, _('You may not retire the admin or anonymous user')
100 # do the retire
101 self.db.getclass(self.classname).retire(nodeid)
102 self.db.commit()
104 self.client.ok_message.append(
105 _('%(classname)s %(itemid)s has been retired')%{
106 'classname': self.classname.capitalize(), 'itemid': nodeid})
108 class SearchAction(Action):
109 name = 'search'
110 permissionType = 'View'
112 def handle(self, wcre=re.compile(r'[\s,]+')):
113 """Mangle some of the form variables.
115 Set the form ":filter" variable based on the values of the filter
116 variables - if they're set to anything other than "dontcare" then add
117 them to :filter.
119 Handle the ":queryname" variable and save off the query to the user's
120 query list.
122 Split any String query values on whitespace and comma.
124 """
125 self.fakeFilterVars()
126 queryname = self.getQueryName()
128 # handle saving the query params
129 if queryname:
130 # parse the environment and figure what the query _is_
131 req = templating.HTMLRequest(self.client)
133 # The [1:] strips off the '?' character, it isn't part of the
134 # query string.
135 url = req.indexargs_href('', {})[1:]
137 # handle editing an existing query
138 try:
139 qid = self.db.query.lookup(queryname)
140 self.db.query.set(qid, klass=self.classname, url=url)
141 except KeyError:
142 # create a query
143 qid = self.db.query.create(name=queryname,
144 klass=self.classname, url=url)
146 # and add it to the user's query multilink
147 queries = self.db.user.get(self.userid, 'queries')
148 if qid not in queries:
149 queries.append(qid)
150 self.db.user.set(self.userid, queries=queries)
152 # commit the query change to the database
153 self.db.commit()
155 def fakeFilterVars(self):
156 """Add a faked :filter form variable for each filtering prop."""
157 props = self.db.classes[self.classname].getprops()
158 for key in self.form.keys():
159 if not props.has_key(key):
160 continue
161 if isinstance(self.form[key], type([])):
162 # search for at least one entry which is not empty
163 for minifield in self.form[key]:
164 if minifield.value:
165 break
166 else:
167 continue
168 else:
169 if not self.form[key].value:
170 continue
171 if isinstance(props[key], hyperdb.String):
172 v = self.form[key].value
173 l = token.token_split(v)
174 if len(l) > 1 or l[0] != v:
175 self.form.value.remove(self.form[key])
176 # replace the single value with the split list
177 for v in l:
178 self.form.value.append(cgi.MiniFieldStorage(key, v))
180 self.form.value.append(cgi.MiniFieldStorage('@filter', key))
182 FV_QUERYNAME = re.compile(r'[@:]queryname')
183 def getQueryName(self):
184 for key in self.form.keys():
185 if self.FV_QUERYNAME.match(key):
186 return self.form[key].value.strip()
187 return ''
189 class EditCSVAction(Action):
190 name = 'edit'
191 permissionType = 'Edit'
193 def handle(self):
194 """Performs an edit of all of a class' items in one go.
196 The "rows" CGI var defines the CSV-formatted entries for the class. New
197 nodes are identified by the ID 'X' (or any other non-existent ID) and
198 removed lines are retired.
200 """
201 # get the CSV module
202 if rcsv.error:
203 self.client.error_message.append(_(rcsv.error))
204 return
206 cl = self.db.classes[self.classname]
207 idlessprops = cl.getprops(protected=0).keys()
208 idlessprops.sort()
209 props = ['id'] + idlessprops
211 # do the edit
212 rows = StringIO.StringIO(self.form['rows'].value)
213 reader = rcsv.reader(rows, rcsv.comma_separated)
214 found = {}
215 line = 0
216 for values in reader:
217 line += 1
218 if line == 1: continue
219 # skip property names header
220 if values == props:
221 continue
223 # extract the nodeid
224 nodeid, values = values[0], values[1:]
225 found[nodeid] = 1
227 # see if the node exists
228 if nodeid in ('x', 'X') or not cl.hasnode(nodeid):
229 exists = 0
230 else:
231 exists = 1
233 # confirm correct weight
234 if len(idlessprops) != len(values):
235 self.client.error_message.append(
236 _('Not enough values on line %(line)s')%{'line':line})
237 return
239 # extract the new values
240 d = {}
241 for name, value in zip(idlessprops, values):
242 prop = cl.properties[name]
243 value = value.strip()
244 # only add the property if it has a value
245 if value:
246 # if it's a multilink, split it
247 if isinstance(prop, hyperdb.Multilink):
248 value = value.split(':')
249 elif isinstance(prop, hyperdb.Password):
250 value = password.Password(value)
251 elif isinstance(prop, hyperdb.Interval):
252 value = date.Interval(value)
253 elif isinstance(prop, hyperdb.Date):
254 value = date.Date(value)
255 elif isinstance(prop, hyperdb.Boolean):
256 value = value.lower() in ('yes', 'true', 'on', '1')
257 elif isinstance(prop, hyperdb.Number):
258 value = float(value)
259 d[name] = value
260 elif exists:
261 # nuke the existing value
262 if isinstance(prop, hyperdb.Multilink):
263 d[name] = []
264 else:
265 d[name] = None
267 # perform the edit
268 if exists:
269 # edit existing
270 cl.set(nodeid, **d)
271 else:
272 # new node
273 found[cl.create(**d)] = 1
275 # retire the removed entries
276 for nodeid in cl.list():
277 if not found.has_key(nodeid):
278 cl.retire(nodeid)
280 # all OK
281 self.db.commit()
283 self.client.ok_message.append(_('Items edited OK'))
285 class _EditAction(Action):
286 def isEditingSelf(self):
287 """Check whether a user is editing his/her own details."""
288 return (self.nodeid == self.userid
289 and self.db.user.get(self.nodeid, 'username') != 'anonymous')
291 def editItemPermission(self, props):
292 """Determine whether the user has permission to edit this item.
294 Base behaviour is to check the user can edit this class. If we're
295 editing the "user" class, users are allowed to edit their own details.
296 Unless it's the "roles" property, which requires the special Permission
297 "Web Roles".
298 """
299 if self.classname == 'user':
300 if props.has_key('roles') and not self.hasPermission('Web Roles'):
301 raise Unauthorised, _("You do not have permission to edit user roles")
302 if self.isEditingSelf():
303 return 1
304 if self.hasPermission('Edit'):
305 return 1
306 return 0
308 def newItemPermission(self, props):
309 """Determine whether the user has permission to create (edit) this item.
311 Base behaviour is to check the user can edit this class. No additional
312 property checks are made. Additionally, new user items may be created
313 if the user has the "Web Registration" Permission.
315 """
316 if (self.classname == 'user' and self.hasPermission('Web Registration')
317 or self.hasPermission('Edit')):
318 return 1
319 return 0
321 #
322 # Utility methods for editing
323 #
324 def _editnodes(self, all_props, all_links, newids=None):
325 ''' Use the props in all_props to perform edit and creation, then
326 use the link specs in all_links to do linking.
327 '''
328 # figure dependencies and re-work links
329 deps = {}
330 links = {}
331 for cn, nodeid, propname, vlist in all_links:
332 if not all_props.has_key((cn, nodeid)):
333 # link item to link to doesn't (and won't) exist
334 continue
335 for value in vlist:
336 if not all_props.has_key(value):
337 # link item to link to doesn't (and won't) exist
338 continue
339 deps.setdefault((cn, nodeid), []).append(value)
340 links.setdefault(value, []).append((cn, nodeid, propname))
342 # figure chained dependencies ordering
343 order = []
344 done = {}
345 # loop detection
346 change = 0
347 while len(all_props) != len(done):
348 for needed in all_props.keys():
349 if done.has_key(needed):
350 continue
351 tlist = deps.get(needed, [])
352 for target in tlist:
353 if not done.has_key(target):
354 break
355 else:
356 done[needed] = 1
357 order.append(needed)
358 change = 1
359 if not change:
360 raise ValueError, 'linking must not loop!'
362 # now, edit / create
363 m = []
364 for needed in order:
365 props = all_props[needed]
366 if not props:
367 # nothing to do
368 continue
369 cn, nodeid = needed
371 if nodeid is not None and int(nodeid) > 0:
372 # make changes to the node
373 props = self._changenode(cn, nodeid, props)
375 # and some nice feedback for the user
376 if props:
377 info = ', '.join(props.keys())
378 m.append('%s %s %s edited ok'%(cn, nodeid, info))
379 else:
380 m.append('%s %s - nothing changed'%(cn, nodeid))
381 else:
382 assert props
384 # make a new node
385 newid = self._createnode(cn, props)
386 if nodeid is None:
387 self.nodeid = newid
388 nodeid = newid
390 # and some nice feedback for the user
391 m.append('%s %s created'%(cn, newid))
393 # fill in new ids in links
394 if links.has_key(needed):
395 for linkcn, linkid, linkprop in links[needed]:
396 props = all_props[(linkcn, linkid)]
397 cl = self.db.classes[linkcn]
398 propdef = cl.getprops()[linkprop]
399 if not props.has_key(linkprop):
400 if linkid is None or linkid.startswith('-'):
401 # linking to a new item
402 if isinstance(propdef, hyperdb.Multilink):
403 props[linkprop] = [newid]
404 else:
405 props[linkprop] = newid
406 else:
407 # linking to an existing item
408 if isinstance(propdef, hyperdb.Multilink):
409 existing = cl.get(linkid, linkprop)[:]
410 existing.append(nodeid)
411 props[linkprop] = existing
412 else:
413 props[linkprop] = newid
415 return '<br>'.join(m)
417 def _changenode(self, cn, nodeid, props):
418 """Change the node based on the contents of the form."""
419 # check for permission
420 if not self.editItemPermission(props):
421 raise Unauthorised, 'You do not have permission to edit %s'%cn
423 # make the changes
424 cl = self.db.classes[cn]
425 return cl.set(nodeid, **props)
427 def _createnode(self, cn, props):
428 """Create a node based on the contents of the form."""
429 # check for permission
430 if not self.newItemPermission(props):
431 raise Unauthorised, 'You do not have permission to create %s'%cn
433 # create the node and return its id
434 cl = self.db.classes[cn]
435 return cl.create(**props)
437 class EditItemAction(_EditAction):
438 def lastUserActivity(self):
439 if self.form.has_key(':lastactivity'):
440 return date.Date(self.form[':lastactivity'].value)
441 elif self.form.has_key('@lastactivity'):
442 return date.Date(self.form['@lastactivity'].value)
443 else:
444 return None
446 def lastNodeActivity(self):
447 cl = getattr(self.client.db, self.classname)
448 return cl.get(self.nodeid, 'activity')
450 def detectCollision(self, userActivity, nodeActivity):
451 # Result from lastUserActivity may be None. If it is, assume there's no
452 # conflict, or at least not one we can detect.
453 if userActivity:
454 return userActivity < nodeActivity
456 def handleCollision(self):
457 self.client.template = 'collision'
459 def handle(self):
460 """Perform an edit of an item in the database.
462 See parsePropsFromForm and _editnodes for special variables.
464 """
465 if self.detectCollision(self.lastUserActivity(), self.lastNodeActivity()):
466 self.handleCollision()
467 return
469 props, links = self.client.parsePropsFromForm()
471 # handle the props
472 try:
473 message = self._editnodes(props, links)
474 except (ValueError, KeyError, IndexError), message:
475 self.client.error_message.append(_('Apply Error: ') + str(message))
476 return
478 # commit now that all the tricky stuff is done
479 self.db.commit()
481 # redirect to the item's edit page
482 # redirect to finish off
483 url = self.base + self.classname
484 # note that this action might have been called by an index page, so
485 # we will want to include index-page args in this URL too
486 if self.nodeid is not None:
487 url += self.nodeid
488 url += '?@ok_message=%s&@template=%s'%(urllib.quote(message),
489 urllib.quote(self.template))
490 if self.nodeid is None:
491 req = templating.HTMLRequest(self)
492 url += '&' + req.indexargs_href('', {})[1:]
493 raise Redirect, url
495 class NewItemAction(_EditAction):
496 def handle(self):
497 ''' Add a new item to the database.
499 This follows the same form as the EditItemAction, with the same
500 special form values.
501 '''
502 # parse the props from the form
503 try:
504 props, links = self.client.parsePropsFromForm(create=1)
505 except (ValueError, KeyError), message:
506 self.client.error_message.append(_('Error: ') + str(message))
507 return
509 # handle the props - edit or create
510 try:
511 # when it hits the None element, it'll set self.nodeid
512 messages = self._editnodes(props, links)
514 except (ValueError, KeyError, IndexError), message:
515 # these errors might just be indicative of user dumbness
516 self.client.error_message.append(_('Error: ') + str(message))
517 return
519 # commit now that all the tricky stuff is done
520 self.db.commit()
522 # redirect to the new item's page
523 raise Redirect, '%s%s%s?@ok_message=%s&@template=%s'%(self.base,
524 self.classname, self.nodeid, urllib.quote(messages),
525 urllib.quote(self.template))
527 class PassResetAction(Action):
528 def handle(self):
529 """Handle password reset requests.
531 Presence of either "name" or "address" generates email. Presence of
532 "otk" performs the reset.
534 """
535 if self.form.has_key('otk'):
536 # pull the rego information out of the otk database
537 otk = self.form['otk'].value
538 otks = self.db.getOTKManager()
539 uid = otks.get(otk, 'uid')
540 if uid is None:
541 self.client.error_message.append("""Invalid One Time Key!
542 (a Mozilla bug may cause this message to show up erroneously,
543 please check your email)""")
544 return
546 # re-open the database as "admin"
547 if self.user != 'admin':
548 self.client.opendb('admin')
549 self.db = self.client.db
551 # change the password
552 newpw = password.generatePassword()
554 cl = self.db.user
555 # XXX we need to make the "default" page be able to display errors!
556 try:
557 # set the password
558 cl.set(uid, password=password.Password(newpw))
559 # clear the props from the otk database
560 otks.destroy(otk)
561 self.db.commit()
562 except (ValueError, KeyError), message:
563 self.client.error_message.append(str(message))
564 return
566 # user info
567 address = self.db.user.get(uid, 'address')
568 name = self.db.user.get(uid, 'username')
570 # send the email
571 tracker_name = self.db.config.TRACKER_NAME
572 subject = 'Password reset for %s'%tracker_name
573 body = '''
574 The password has been reset for username "%(name)s".
576 Your password is now: %(password)s
577 '''%{'name': name, 'password': newpw}
578 if not self.client.standard_message([address], subject, body):
579 return
581 self.client.ok_message.append(
582 'Password reset and email sent to %s'%address)
583 return
585 # no OTK, so now figure the user
586 if self.form.has_key('username'):
587 name = self.form['username'].value
588 try:
589 uid = self.db.user.lookup(name)
590 except KeyError:
591 self.client.error_message.append('Unknown username')
592 return
593 address = self.db.user.get(uid, 'address')
594 elif self.form.has_key('address'):
595 address = self.form['address'].value
596 uid = uidFromAddress(self.db, ('', address), create=0)
597 if not uid:
598 self.client.error_message.append('Unknown email address')
599 return
600 name = self.db.user.get(uid, 'username')
601 else:
602 self.client.error_message.append('You need to specify a username '
603 'or address')
604 return
606 # generate the one-time-key and store the props for later
607 otk = ''.join([random.choice(chars) for x in range(32)])
608 while otks.exists(otk):
609 otk = ''.join([random.choice(chars) for x in range(32)])
610 otks.set(otk, uid=uid)
611 self.db.commit()
613 # send the email
614 tracker_name = self.db.config.TRACKER_NAME
615 subject = 'Confirm reset of password for %s'%tracker_name
616 body = '''
617 Someone, perhaps you, has requested that the password be changed for your
618 username, "%(name)s". If you wish to proceed with the change, please follow
619 the link below:
621 %(url)suser?@template=forgotten&@action=passrst&otk=%(otk)s
623 You should then receive another email with the new password.
624 '''%{'name': name, 'tracker': tracker_name, 'url': self.base, 'otk': otk}
625 if not self.client.standard_message([address], subject, body):
626 return
628 self.client.ok_message.append('Email sent to %s'%address)
630 class ConfRegoAction(Action):
631 def handle(self):
632 """Grab the OTK, use it to load up the new user details."""
633 try:
634 # pull the rego information out of the otk database
635 self.userid = self.db.confirm_registration(self.form['otk'].value)
636 except (ValueError, KeyError), message:
637 self.client.error_message.append(str(message))
638 return
640 # log the new user in
641 self.client.user = self.db.user.get(self.userid, 'username')
642 # re-open the database for real, using the user
643 self.client.opendb(self.client.user)
645 # if we have a session, update it
646 if hasattr(self, 'session'):
647 self.client.db.sessions.set(self.session, user=self.user,
648 last_use=time.time())
649 else:
650 # new session cookie
651 self.client.set_cookie(self.user)
653 # nice message
654 message = _('You are now registered, welcome!')
655 url = '%suser%s?@ok_message=%s'%(self.base, self.userid,
656 urllib.quote(message))
658 # redirect to the user's page (but not 302, as some email clients seem
659 # to want to reload the page, or something)
660 return '''<html><head><title>%s</title></head>
661 <body><p><a href="%s">%s</a></p>
662 <script type="text/javascript">
663 window.setTimeout('window.location = "%s"', 1000);
664 </script>'''%(message, url, message, url)
666 class RegisterAction(Action):
667 name = 'register'
668 permissionType = 'Web Registration'
670 def handle(self):
671 """Attempt to create a new user based on the contents of the form
672 and then set the cookie.
674 Return 1 on successful login.
675 """
676 props = self.client.parsePropsFromForm(create=1)[0][('user', None)]
678 # registration isn't allowed to supply roles
679 if props.has_key('roles'):
680 raise Unauthorised, _("It is not permitted to supply roles "
681 "at registration.")
683 username = props['username']
684 try:
685 self.db.user.lookup(username)
686 self.client.error_message.append(_('Error: A user with the '
687 'username "%(username)s" already exists')%props)
688 return
689 except KeyError:
690 pass
692 # generate the one-time-key and store the props for later
693 for propname, proptype in self.db.user.getprops().items():
694 value = props.get(propname, None)
695 if value is None:
696 pass
697 elif isinstance(proptype, hyperdb.Date):
698 props[propname] = str(value)
699 elif isinstance(proptype, hyperdb.Interval):
700 props[propname] = str(value)
701 elif isinstance(proptype, hyperdb.Password):
702 props[propname] = str(value)
703 otks = self.db.getOTKManager()
704 while otks.exists(otk):
705 otk = ''.join([random.choice(chars) for x in range(32)])
706 otks.set(otk, **props)
708 # send the email
709 tracker_name = self.db.config.TRACKER_NAME
710 tracker_email = self.db.config.TRACKER_EMAIL
711 subject = 'Complete your registration to %s -- key %s'%(tracker_name,
712 otk)
713 body = """To complete your registration of the user "%(name)s" with
714 %(tracker)s, please do one of the following:
716 - send a reply to %(tracker_email)s and maintain the subject line as is (the
717 reply's additional "Re:" is ok),
719 - or visit the following URL:
721 %(url)s?@action=confrego&otk=%(otk)s
723 """ % {'name': props['username'], 'tracker': tracker_name, 'url': self.base,
724 'otk': otk, 'tracker_email': tracker_email}
725 if not self.client.standard_message([props['address']], subject, body,
726 tracker_email):
727 return
729 # commit changes to the database
730 self.db.commit()
732 # redirect to the "you're almost there" page
733 raise Redirect, '%suser?@template=rego_progress'%self.base
735 class LogoutAction(Action):
736 def handle(self):
737 """Make us really anonymous - nuke the cookie too."""
738 # log us out
739 self.client.make_user_anonymous()
741 # construct the logout cookie
742 now = Cookie._getdate()
743 self.client.additional_headers['Set-Cookie'] = \
744 '%s=deleted; Max-Age=0; expires=%s; Path=%s;'%(self.client.cookie_name,
745 now, self.client.cookie_path)
747 # Let the user know what's going on
748 self.client.ok_message.append(_('You are logged out'))
750 class LoginAction(Action):
751 def handle(self):
752 """Attempt to log a user in.
754 Sets up a session for the user which contains the login credentials.
756 """
757 # we need the username at a minimum
758 if not self.form.has_key('__login_name'):
759 self.client.error_message.append(_('Username required'))
760 return
762 # get the login info
763 self.client.user = self.form['__login_name'].value
764 if self.form.has_key('__login_password'):
765 password = self.form['__login_password'].value
766 else:
767 password = ''
769 # make sure the user exists
770 try:
771 self.client.userid = self.db.user.lookup(self.client.user)
772 except KeyError:
773 name = self.client.user
774 self.client.error_message.append(_('No such user "%(name)s"')%locals())
775 self.client.make_user_anonymous()
776 return
778 # verify the password
779 if not self.verifyPassword(self.client.userid, password):
780 self.client.make_user_anonymous()
781 self.client.error_message.append(_('Incorrect password'))
782 return
784 # Determine whether the user has permission to log in.
785 # Base behaviour is to check the user has "Web Access".
786 if not self.hasPermission("Web Access"):
787 self.client.make_user_anonymous()
788 self.client.error_message.append(_("You do not have permission to login"))
789 return
791 # now we're OK, re-open the database for real, using the user
792 self.client.opendb(self.client.user)
794 # set the session cookie
795 self.client.set_cookie(self.client.user)
797 def verifyPassword(self, userid, password):
798 ''' Verify the password that the user has supplied
799 '''
800 stored = self.db.user.get(self.client.userid, 'password')
801 if password == stored:
802 return 1
803 if not password and not stored:
804 return 1
805 return 0
807 class ExportCSVAction(Action):
808 name = 'export'
809 permissionType = 'View'
811 def handle(self):
812 ''' Export the specified search query as CSV. '''
813 # figure the request
814 request = HTMLRequest(self)
815 filterspec = request.filterspec
816 sort = request.sort
817 group = request.group
818 columns = request.columns
819 klass = self.db.getclass(request.classname)
821 # full-text search
822 if request.search_text:
823 matches = self.db.indexer.search(
824 re.findall(r'\b\w{2,25}\b', request.search_text), klass)
825 else:
826 matches = None
828 h = self.additional_headers
829 h['Content-Type'] = 'text/csv'
830 # some browsers will honor the filename here...
831 h['Content-Disposition'] = 'inline; filename=query.csv'
832 self.header()
833 writer = rcsv.writer(self.request.wfile)
834 writer.writerow(columns)
836 # and search
837 for itemid in klass.filter(matches, filterspec, sort, group):
838 writer.writerow([str(klass.get(itemid, col)) for col in columns])
840 return '\n'
842 # vim: set filetype=python ts=4 sw=4 et si