diff --git a/roundup/cgi/client.py b/roundup/cgi/client.py
index 6ee905f53e7b2b65ae0e114b2da8f6b17e4edf4e..fe8831371b8590243b153b644e93d7bd4bef0411 100644 (file)
--- a/roundup/cgi/client.py
+++ b/roundup/cgi/client.py
-# $Id: client.py,v 1.127 2003-08-10 10:30:56 jlgijsbers Exp $
+# $Id: client.py,v 1.149 2003-12-05 03:28:38 richard Exp $
__doc__ = """
WWW request handler (also used in the stand-alone server).
import os, os.path, cgi, StringIO, urlparse, re, traceback, mimetypes, urllib
import binascii, Cookie, time, random, MimeWriter, smtplib, socket, quopri
-import stat, rfc822, string
+import stat, rfc822
-from roundup import roundupdb, date, hyperdb, password, token
+from roundup import roundupdb, date, hyperdb, password, token, rcsv
from roundup.i18n import _
from roundup.cgi.templating import Templates, HTMLRequest, NoTemplate
from roundup.cgi import cgitb
from roundup.cgi.PageTemplates import PageTemplate
from roundup.rfc2822 import encode_header
-from roundup.mailgw import uidFromAddress, openSMTPConnection
+from roundup.mailgw import uidFromAddress
+from roundup.mailer import Mailer, MessageSendError
class HTTPException(Exception):
- pass
-class Unauthorised(HTTPException):
- pass
-class NotFound(HTTPException):
- pass
-class Redirect(HTTPException):
- pass
-class NotModified(HTTPException):
- pass
-
-# set to indicate to roundup not to actually _send_ email
-# this var must contain a file to write the mail to
-SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '')
+ pass
+class Unauthorised(HTTPException):
+ pass
+class NotFound(HTTPException):
+ pass
+class Redirect(HTTPException):
+ pass
+class NotModified(HTTPException):
+ pass
# used by a couple of routines
-chars = string.letters+string.digits
+chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
-# XXX actually _use_ FormError
class FormError(ValueError):
- ''' An "expected" exception occurred during form parsing.
+ """ An "expected" exception occurred during form parsing.
- ie. something we know can go wrong, and don't want to alarm the
user with
We trap this at the user interface level and feed back a nice error
to the user.
- '''
+ """
pass
class SendFile(Exception):
self.instance = instance
self.request = request
self.env = env
+ self.mailer = Mailer(instance.config)
# save off the path
self.path = env['PATH_INFO']
except NotFound:
# pass through
raise
+ except FormError, e:
+ self.error_message.append(_('Form Error: ') + str(e))
+ self.write(self.renderContext())
except:
# everything else
self.write(cgitb.html())
def clean_sessions(self):
- ''' Age sessions, remove when they haven't been used for a week.
+ """Age sessions, remove when they haven't been used for a week.
- Do it only once an hour.
+ Do it only once an hour.
- Note: also cleans One Time Keys, and other "session" based
- stuff.
- '''
+ Note: also cleans One Time Keys, and other "session" based stuff.
+ """
sessions = self.db.sessions
last_clean = sessions.get('last_clean', 'last_use') or 0
sessions.set('last_clean', last_use=time.time())
def determine_user(self):
- ''' Determine who the user is
+ '''Determine who the user is.
'''
# open the database as admin
self.opendb('admin')
self.opendb(self.user)
def determine_context(self, dre=re.compile(r'([^\d]+)(\d+)')):
- ''' Determine the context of this page from the URL:
+ """ 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.
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
self.nodeid = None
else:
self.template = ''
return
- elif path[0] == '_file':
+ elif path[0] in ('_file', '@@file'):
raise SendStaticFile, os.path.join(*path[1:])
else:
self.classname = path[0]
if classname != 'file':
raise NotFound, designator
- # we just want to serve up the file named
+ self.opendb('admin')
file = self.db.file
- self.additional_headers['Content-Type'] = file.get(nodeid, 'type')
- self.write(file.get(nodeid, 'content'))
+
+ mime_type = file.get(nodeid, 'type')
+ content = file.get(nodeid, 'content')
+ lmt = file.get(nodeid, 'activity').timestamp()
+
+ self._serve_file(lmt, mime_type, content)
def serve_static_file(self, file):
+ ''' Serve up the file named from the templates dir
+ '''
+ filename = os.path.join(self.instance.config.TEMPLATES, file)
+
+ # last-modified time
+ lmt = os.stat(filename)[stat.ST_MTIME]
+
+ # detemine meta-type
+ file = str(file)
+ mime_type = mimetypes.guess_type(file)[0]
+ if not mime_type:
+ if file.endswith('.css'):
+ mime_type = 'text/css'
+ else:
+ mime_type = 'text/plain'
+
+ # snarf the content
+ f = open(filename, 'rb')
+ try:
+ content = f.read()
+ finally:
+ f.close()
+
+ self._serve_file(lmt, mime_type, content)
+
+ def _serve_file(self, last_modified, mime_type, content):
+ ''' guts of serve_file() and serve_static_file()
+ '''
ims = None
# see if there's an if-modified-since...
if hasattr(self.request, 'headers'):
elif self.env.has_key('HTTP_IF_MODIFIED_SINCE'):
# cgi will put the header in the env var
ims = self.env['HTTP_IF_MODIFIED_SINCE']
- filename = os.path.join(self.instance.config.TEMPLATES, file)
- lmt = os.stat(filename)[stat.ST_MTIME]
if ims:
ims = rfc822.parsedate(ims)[:6]
lmtt = time.gmtime(lmt)[:6]
if lmtt <= ims:
raise NotModified
- # we just want to serve up the file named
- file = str(file)
- mt = mimetypes.guess_type(file)[0]
- if not mt:
- if file.endswith('.css'):
- mt = 'text/css'
- else:
- mt = 'text/plain'
- self.additional_headers['Content-Type'] = mt
- self.additional_headers['Last-Modifed'] = rfc822.formatdate(lmt)
- self.write(open(filename, 'rb').read())
+ # spit out headers
+ self.additional_headers['Content-Type'] = mime_type
+ self.additional_headers['Content-Length'] = len(content)
+ lmt = rfc822.formatdate(last_modified)
+ self.additional_headers['Last-Modifed'] = lmt
+ self.write(content)
def renderContext(self):
''' Return a PageTemplate for the named page
self.headers_sent = headers
def set_cookie(self, user):
- ''' Set up a session cookie for the user and store away the user's
- login info against the session.
- '''
+ """Set up a session cookie for the user.
+
+ Also store away the user's login info against the session.
+ """
# TODO generate a much, much stronger session key ;)
self.session = binascii.b2a_base64(repr(random.random())).strip()
return 1 on successful login
'''
- # parse the props from the form
- try:
- props = self.parsePropsFromForm()[0][('user', None)]
- except (ValueError, KeyError), message:
- self.error_message.append(_('Error: ') + str(message))
- return
+ props = self.parsePropsFromForm()[0][('user', None)]
# make sure we're allowed to register
if not self.registerPermission(props):
# send the email
tracker_name = self.db.config.TRACKER_NAME
- subject = 'Complete your registration to %s'%tracker_name
- body = '''
-To complete your registration of the user "%(name)s" with %(tracker)s,
-please visit the following URL:
+ tracker_email = self.db.config.TRACKER_EMAIL
+ subject = 'Complete your registration to %s -- key %s' % (tracker_name,
+ otk)
+ body = """To complete your registration of the user "%(name)s" with
+%(tracker)s, please do one of the following:
+
+- send a reply to %(tracker_email)s and maintain the subject line as is (the
+reply's additional "Re:" is ok),
+
+- or visit the following URL:
%(url)s?@action=confrego&otk=%(otk)s
-'''%{'name': props['username'], 'tracker': tracker_name, 'url': self.base,
- 'otk': otk}
- if not self.sendEmail(props['address'], subject, body):
+""" % {'name': props['username'], 'tracker': tracker_name, 'url': self.base,
+ 'otk': otk, 'tracker_email': tracker_email}
+ if not self.standard_message([props['address']], subject, body,
+ tracker_email):
return
# commit changes to the database
# redirect to the "you're almost there" page
raise Redirect, '%suser?@template=rego_progress'%self.base
- def sendEmail(self, to, subject, content):
- # send email to the user's email address
- message = StringIO.StringIO()
- writer = MimeWriter.MimeWriter(message)
- tracker_name = self.db.config.TRACKER_NAME
- writer.addheader('Subject', encode_header(subject))
- writer.addheader('To', to)
- writer.addheader('From', roundupdb.straddr((tracker_name,
- self.db.config.ADMIN_EMAIL)))
- writer.addheader('Date', time.strftime("%a, %d %b %Y %H:%M:%S +0000",
- time.gmtime()))
- # add a uniquely Roundup header to help filtering
- writer.addheader('X-Roundup-Name', tracker_name)
- # avoid email loops
- writer.addheader('X-Roundup-Loop', 'hello')
- writer.addheader('Content-Transfer-Encoding', 'quoted-printable')
- body = writer.startbody('text/plain; charset=utf-8')
-
- # message body, encoded quoted-printable
- content = StringIO.StringIO(content)
- quopri.encode(content, body, 0)
-
- if SENDMAILDEBUG:
- # don't send - just write to a file
- open(SENDMAILDEBUG, 'a').write('FROM: %s\nTO: %s\n%s\n'%(
- self.db.config.ADMIN_EMAIL,
- ', '.join(to),message.getvalue()))
- else:
- # now try to send the message
- try:
- # send the message as admin so bounces are sent there
- # instead of to roundup
- smtp = openSMTPConnection(self.db.config)
- smtp.sendmail(self.db.config.ADMIN_EMAIL, [to],
- message.getvalue())
- except socket.error, value:
- self.error_message.append("Error: couldn't send email: "
- "mailhost %s"%value)
- return 0
- except smtplib.SMTPException, msg:
- self.error_message.append("Error: couldn't send email: %s"%msg)
- return 0
- return 1
-
+ def standard_message(self, to, subject, body, author=None):
+ try:
+ self.mailer.standard_message(to, subject, body, author)
+ return 1
+ except MessageSendError, e:
+ self.error_message.append(str(e))
+
def registerPermission(self, props):
''' Determine whether the user has permission to register
def confRegoAction(self):
''' Grab the OTK, use it to load up the new user details
'''
- # pull the rego information out of the otk database
- otk = self.form['otk'].value
- props = self.db.otks.getall(otk)
- for propname, proptype in self.db.user.getprops().items():
- value = props.get(propname, None)
- if value is None:
- pass
- elif isinstance(proptype, hyperdb.Date):
- props[propname] = date.Date(value)
- elif isinstance(proptype, hyperdb.Interval):
- props[propname] = date.Interval(value)
- elif isinstance(proptype, hyperdb.Password):
- props[propname] = password.Password()
- props[propname].unpack(value)
-
- # re-open the database as "admin"
- if self.user != 'admin':
- self.opendb('admin')
-
- # create the new user
- cl = self.db.user
-# XXX we need to make the "default" page be able to display errors!
try:
- props['roles'] = self.instance.config.NEW_WEB_USER_ROLES
- del props['__time']
- self.userid = cl.create(**props)
- # clear the props from the otk database
- self.db.otks.destroy(otk)
- self.db.commit()
+ # pull the rego information out of the otk database
+ self.userid = self.db.confirm_registration(self.form['otk'].value)
except (ValueError, KeyError), message:
+ # XXX: we need to make the "default" page be able to display errors!
self.error_message.append(str(message))
return
-
+
# log the new user in
- self.user = cl.get(self.userid, 'username')
+ self.user = self.db.user.get(self.userid, 'username')
# re-open the database for real, using the user
self.opendb(self.user)
otk = self.form['otk'].value
uid = self.db.otks.get(otk, 'uid')
if uid is None:
- self.error_message.append('Invalid One Time Key!')
+ self.error_message.append("""Invalid One Time Key!
+(a Mozilla bug may cause this message to show up erroneously,
+ please check your email)""")
return
# re-open the database as "admin"
Your password is now: %(password)s
'''%{'name': name, 'password': newpw}
- if not self.sendEmail(address, subject, body):
+ if not self.standard_message([address], subject, body):
return
- self.ok_message.append('Password reset and email sent to %s'%address)
+ self.ok_message.append('Password reset and email sent to %s' %
+ address)
return
# no OTK, so now figure the user
You should then receive another email with the new password.
'''%{'name': name, 'tracker': tracker_name, 'url': self.base, 'otk': otk}
- if not self.sendEmail(address, subject, body):
+ if not self.standard_message([address], subject, body):
return
self.ok_message.append('Email sent to %s'%address)
See parsePropsFromForm and _editnodes for special variables
'''
- # parse the props from the form
- try:
- props, links = self.parsePropsFromForm()
- except (ValueError, KeyError), message:
- self.error_message.append(_('Error: ') + str(message))
- return
+ props, links = self.parsePropsFromForm()
# handle the props
try:
message = self._editnodes(props, links)
except (ValueError, KeyError, IndexError), message:
- self.error_message.append(_('Error: ') + str(message))
+ self.error_message.append(_('Apply Error: ') + str(message))
return
# commit now that all the tricky stuff is done
self.classname, self.nodeid, urllib.quote(message),
urllib.quote(self.template))
+ newItemAction = editItemAction
+
def editItemPermission(self, props):
- ''' Determine whether the user has permission to edit this item.
+ """Determine whether the user has permission to edit this item.
- Base behaviour is to check the user can edit this class. If we're
- editing the "user" class, users are allowed to edit their own
- details. Unless it's the "roles" property, which requires the
- special Permission "Web Roles".
- '''
+ Base behaviour is to check the user can edit this class. If we're
+ editing the"user" class, users are allowed to edit their own details.
+ Unless it's the "roles" property, which requires the special Permission
+ "Web Roles".
+ """
# if this is a user node and the user is editing their own node, then
# we're OK
has = self.db.security.hasPermission
'user'):
return 0
# if the item being edited is the current user, we're ok
- if self.nodeid == self.userid:
+ if (self.nodeid == self.userid
+ and self.db.user.get(self.nodeid, 'username') != 'anonymous'):
return 1
if self.db.security.hasPermission('Edit', self.userid, self.classname):
return 1
return 0
- def newItemAction(self):
- ''' Add a new item to the database.
-
- This follows the same form as the editItemAction, with the same
- special form values.
- '''
- # parse the props from the form
- try:
- props, links = self.parsePropsFromForm()
- except (ValueError, KeyError), message:
- self.error_message.append(_('Error: ') + str(message))
- return
-
- # handle the props - edit or create
- try:
- # when it hits the None element, it'll set self.nodeid
- messages = self._editnodes(props, links)
-
- except (ValueError, KeyError, IndexError), message:
- # these errors might just be indicative of user dumbness
- self.error_message.append(_('Error: ') + str(message))
- return
-
- # commit now that all the tricky stuff is done
- self.db.commit()
-
- # redirect to the new item's page
- raise Redirect, '%s%s%s?@ok_message=%s&@template=%s'%(self.base,
- self.classname, self.nodeid, urllib.quote(messages),
- urllib.quote(self.template))
-
def newItemPermission(self, props):
''' Determine whether the user has permission to create (edit) this
item.
# More actions
#
def editCSVAction(self):
- ''' Performs an edit of all of a class' items in one go.
+ """ 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.
- '''
+ """
# this is per-class only
if not self.editCSVPermission():
self.error_message.append(
- _('You do not have permission to edit %s' %self.classname))
+ _('You do not have permission to edit %s' %self.classname))
+ return
# get the CSV module
- try:
- import csv
- except ImportError:
- self.error_message.append(_(
- 'Sorry, you need the csv module to use this function.<br>\n'
- 'Get it from: <a href="http://www.object-craft.com.au/projects/csv/">http://www.object-craft.com.au/projects/csv/'))
+ if rcsv.error:
+ self.error_message.append(_(rcsv.error))
return
cl = self.db.classes[self.classname]
props = ['id'] + idlessprops
# do the edit
- rows = self.form['rows'].value.splitlines()
- p = csv.parser()
+ rows = StringIO.StringIO(self.form['rows'].value)
+ reader = rcsv.reader(rows, rcsv.comma_separated)
found = {}
line = 0
- for row in rows[1:]:
+ for values in reader:
line += 1
- values = p.parse(row)
- # not a complete row, keep going
- if not values: continue
-
+ if line == 1: continue
# skip property names header
if values == props:
continue
found[nodeid] = 1
# see if the node exists
- if cl.hasnode(nodeid):
- exists = 1
- else:
+ if nodeid in ('x', 'X') or not cl.hasnode(nodeid):
exists = 0
+ else:
+ exists = 1
# confirm correct weight
if len(idlessprops) != len(values):
# if it's a multilink, split it
if isinstance(prop, hyperdb.Multilink):
value = value.split(':')
+ elif isinstance(prop, hyperdb.Password):
+ value = password.Password(value)
+ elif isinstance(prop, hyperdb.Interval):
+ value = date.Interval(value)
+ elif isinstance(prop, hyperdb.Date):
+ value = date.Date(value)
+ elif isinstance(prop, hyperdb.Boolean):
+ value = value.lower() in ('yes', 'true', 'on', '1')
+ elif isinstance(prop, hyperdb.Number):
+ value = float(value)
d[name] = value
elif exists:
# nuke the existing value
if not self.searchPermission():
self.error_message.append(
_('You do not have permission to search %s' %self.classname))
+ return
# add a faked :filter form variable for each filtering prop
props = self.db.classes[self.classname].getprops()
raise Redirect, url
def parsePropsFromForm(self, num_re=re.compile('^\d+$')):
- ''' Item properties and their values are edited with html FORM
+ """ Item properties and their values are edited with html FORM
variables and their values. You can:
- Change the value of some property of the current item.
This is equivalent to::
@link@messages=msg-1
- @msg-1@content=value
+ msg-1@content=value
except that in addition, the "author" and "date"
properties of "msg-1" are set to the userid of the
This is equivalent to::
@link@files=file-1
- @file-1@content=value
+ file-1@content=value
The String content value is handled as described above for
file uploads.
doesn't result in any changes would return {('issue','123'): {}})
The id may be None, which indicates that an item should be
created.
- '''
+ """
# some very useful variables
db = self.db
form = self.form
for entry in extractFormList(form[key]):
m = self.FV_DESIGNATOR.match(entry)
if not m:
- raise ValueError, \
+ raise FormError, \
'link "%s" value "%s" not a designator'%(key, entry)
value.append((m.group(1), m.group(2)))
# make sure the link property is valid
if (not isinstance(propdef[propname], hyperdb.Multilink) and
not isinstance(propdef[propname], hyperdb.Link)):
- raise ValueError, '%s %s is not a link or '\
+ raise FormError, '%s %s is not a link or '\
'multilink property'%(cn, propname)
all_links.append((cn, nodeid, propname, value))
# does the property exist?
if not propdef.has_key(propname):
if mlaction != 'set':
- raise ValueError, 'You have submitted a %s action for'\
+ raise FormError, 'You have submitted a %s action for'\
' the property "%s" which doesn\'t exist'%(mlaction,
propname)
# the form element is probably just something we don't care
else:
# multiple values are not OK
if isinstance(value, type([])):
- raise ValueError, 'You have submitted more than one value'\
+ raise FormError, 'You have submitted more than one value'\
' for the %s property'%propname
# value might be a file upload...
if not hasattr(value, 'filename') or value.filename is None:
confirm = form[key]
break
else:
- raise ValueError, 'Password and confirmation text do '\
+ raise FormError, 'Password and confirmation text do '\
'not match'
if isinstance(confirm, type([])):
- raise ValueError, 'You have submitted more than one value'\
+ raise FormError, 'You have submitted more than one value'\
' for the %s property'%propname
if value != confirm.value:
- raise ValueError, 'Password and confirmation text do '\
+ raise FormError, 'Password and confirmation text do '\
'not match'
- value = password.Password(value)
-
- elif isinstance(proptype, hyperdb.Link):
- # see if it's the "no selection" choice
- if value == '-1' or not value:
- # if we're creating, just don't include this property
- if not nodeid or nodeid.startswith('-'):
- continue
- value = None
- else:
- # handle key values
- link = proptype.classname
- if not num_re.match(value):
- try:
- value = db.classes[link].lookup(value)
- except KeyError:
- raise ValueError, _('property "%(propname)s": '
- '%(value)s not a %(classname)s')%{
- 'propname': propname, 'value': value,
- 'classname': link}
- except TypeError, message:
- raise ValueError, _('you may only enter ID values '
- 'for property "%(propname)s": %(message)s')%{
- 'propname': propname, 'message': message}
+ try:
+ value = password.Password(value)
+ except hyperdb.HyperdbValueError, msg:
+ raise FormError, msg
+
elif isinstance(proptype, hyperdb.Multilink):
- # perform link class key value lookup if necessary
- link = proptype.classname
- link_cl = db.classes[link]
- l = []
- for entry in value:
- if not entry: continue
- if not num_re.match(entry):
- try:
- entry = link_cl.lookup(entry)
- except KeyError:
- raise ValueError, _('property "%(propname)s": '
- '"%(value)s" not an entry of %(classname)s')%{
- 'propname': propname, 'value': entry,
- 'classname': link}
- except TypeError, message:
- raise ValueError, _('you may only enter ID values '
- 'for property "%(propname)s": %(message)s')%{
- 'propname': propname, 'message': message}
- l.append(entry)
- l.sort()
+ # convert input to list of ids
+ try:
+ l = hyperdb.rawToHyperdb(self.db, cl, nodeid,
+ propname, value)
+ except hyperdb.HyperdbValueError, msg:
+ raise FormError, msg
# now use that list of ids to modify the multilink
if mlaction == 'set':
try:
existing.remove(entry)
except ValueError:
- raise ValueError, _('property "%(propname)s": '
+ raise FormError, _('property "%(propname)s": '
'"%(value)s" not currently in list')%{
'propname': propname, 'value': entry}
else:
value.sort()
elif value == '':
- # if we're creating, just don't include this property
- if not nodeid or nodeid.startswith('-'):
- continue
# other types should be None'd if there's no value
value = None
else:
- # handle ValueErrors for all these in a similar fashion
+ # handle all other types
try:
if isinstance(proptype, hyperdb.String):
if (hasattr(value, 'filename') and
props['type'] = mimetypes.guess_type(fn)[0]
if not props['type']:
props['type'] = "application/octet-stream"
- # finally, read the content
+ # finally, read the content RAW
value = value.value
else:
- # normal String fix the CRLF/CR -> LF stuff
- value = fixNewlines(value)
+ value = hyperdb.rawToHyperdb(self.db, cl,
+ nodeid, propname, value)
- elif isinstance(proptype, hyperdb.Date):
- value = date.Date(value, offset=timezone)
- elif isinstance(proptype, hyperdb.Interval):
- value = date.Interval(value)
- elif isinstance(proptype, hyperdb.Boolean):
- value = value.lower() in ('yes', 'true', 'on', '1')
- elif isinstance(proptype, hyperdb.Number):
- value = float(value)
- except ValueError, msg:
- raise ValueError, _('Error with %s property: %s')%(
- propname, msg)
+ else:
+ value = hyperdb.rawToHyperdb(self.db, cl, nodeid,
+ propname, value)
+ except hyperdb.HyperdbValueError, msg:
+ raise FormError, msg
# register that we got this property
if value:
# no existing value
if not propdef.has_key(propname):
raise
+ except IndexError, message:
+ raise FormError(str(message))
# make sure the existing multilink is sorted
if isinstance(proptype, hyperdb.Multilink):
s.append('Required %s %s %s not supplied'%(thing[0], p,
', '.join(required)))
if s:
- raise ValueError, '\n'.join(s)
+ raise FormError, '\n'.join(s)
- # check that FileClass entries have a "content" property with
- # content, otherwise remove them
+ # When creating a FileClass node, it should have a non-empty content
+ # property to be created. When editing a FileClass node, it should
+ # either have a non-empty content property or no property at all. In
+ # the latter case, nothing will change.
for (cn, id), props in all_props.items():
- cl = self.db.classes[cn]
- if not isinstance(cl, hyperdb.FileClass):
- continue
- # we also don't want to create FileClass items with no content
- if not props.get('content', ''):
- del all_props[(cn, id)]
+ if isinstance(self.db.classes[cn], hyperdb.FileClass):
+ if id == '-1':
+ if not props.get('content', ''):
+ del all_props[(cn, id)]
+ elif props.has_key('content') and not props['content']:
+ raise FormError, _('File is empty')
return all_props, all_links
-def fixNewlines(text):
- ''' Homogenise line endings.
-
- Different web clients send different line ending values, but
- other systems (eg. email) don't necessarily handle those line
- endings. Our solution is to convert all line endings to LF.
- '''
- text = text.replace('\r\n', '\n')
- return text.replace('\r', '\n')
-
def extractFormList(value):
''' Extract a list of values from the form value.