comparison roundup/cgi/form_parser.py @ 5065:47ab150b7325

Allow multiple file uploads If the html template specifies multiple="multiple" for a file upload the user can attach multiple files and the form parser now handles this.
author Ralf Schlatterbeck <rsc@runtux.com>
date Mon, 30 May 2016 17:23:35 +0200
parents 9792b18e0b19
children d2256fcfd81f
comparison
equal deleted inserted replaced
5064:46da0db55545 5065:47ab150b7325
161 @link@files=file-1 161 @link@files=file-1
162 file-1@content=value 162 file-1@content=value
163 163
164 The String content value is handled as described above for 164 The String content value is handled as described above for
165 file uploads. 165 file uploads.
166 If "multiple" is turned on for file uploads in the html
167 template, multiple links are generated::
168
169 @link@files=file-2
170 file-2@content=value
171 ...
172
173 depending on how many files the user has attached.
166 174
167 If both the "@note" and "@file" form variables are 175 If both the "@note" and "@file" form variables are
168 specified, the action:: 176 specified, the action::
169 177
170 @link@msg-1@files=file-1 178 @link@msg-1@files=file-1
171 179
172 is also performed. 180 is also performed. If "multiple" is specified this is
181 carried out for each of the attached files.
173 182
174 We also check that FileClass items have a "content" property with 183 We also check that FileClass items have a "content" property with
175 actual content, otherwise we remove them from all_props before 184 actual content, otherwise we remove them from all_props before
176 returning. 185 returning.
177 186
240 all_links.append((default_cn, default_nodeid, 'messages', 249 all_links.append((default_cn, default_nodeid, 'messages',
241 [('msg', '-1')])) 250 [('msg', '-1')]))
242 have_note = 1 251 have_note = 1
243 elif d['file']: 252 elif d['file']:
244 # the special file field 253 # the special file field
245 cn = 'file' 254 cn = default_cn
246 cl = self.db.classes[cn] 255 cl = default_cl
247 nodeid = '-1' 256 nodeid = default_nodeid
248 propname = 'content' 257 propname = 'files'
249 all_links.append((default_cn, default_nodeid, 'files',
250 [('file', '-1')]))
251 have_file = 1
252 else: 258 else:
253 # default 259 # default
254 cn = default_cn 260 cn = default_cn
255 cl = default_cl 261 cl = default_cl
256 nodeid = default_nodeid 262 nodeid = default_nodeid
278 if d['link']: 284 if d['link']:
279 value = [] 285 value = []
280 for entry in self.extractFormList(form[key]): 286 for entry in self.extractFormList(form[key]):
281 m = self.FV_DESIGNATOR.match(entry) 287 m = self.FV_DESIGNATOR.match(entry)
282 if not m: 288 if not m:
283 raise FormError, self._('link "%(key)s" ' 289 raise FormError (self._('link "%(key)s" '
284 'value "%(entry)s" not a designator') % locals() 290 'value "%(entry)s" not a designator') % locals())
285 value.append((m.group(1), m.group(2))) 291 value.append((m.group(1), m.group(2)))
286 292
287 # get details of linked class 293 # get details of linked class
288 lcn = m.group(1) 294 lcn = m.group(1)
289 lcl = self.db.classes[lcn] 295 lcl = self.db.classes[lcn]
296 got_props[(lcn, lnodeid)] = {} 302 got_props[(lcn, lnodeid)] = {}
297 303
298 # make sure the link property is valid 304 # make sure the link property is valid
299 if (not isinstance(propdef[propname], hyperdb.Multilink) and 305 if (not isinstance(propdef[propname], hyperdb.Multilink) and
300 not isinstance(propdef[propname], hyperdb.Link)): 306 not isinstance(propdef[propname], hyperdb.Link)):
301 raise FormError, self._('%(class)s %(property)s ' 307 raise FormError (self._('%(class)s %(property)s '
302 'is not a link or multilink property') % { 308 'is not a link or multilink property') % {
303 'class':cn, 'property':propname} 309 'class':cn, 'property':propname})
304 310
305 all_links.append((cn, nodeid, propname, value)) 311 all_links.append((cn, nodeid, propname, value))
306 continue 312 continue
307 313
308 # detect the special ":required" variable 314 # detect the special ":required" variable
309 if d['required']: 315 if d['required']:
310 for entry in self.extractFormList(form[key]): 316 for entry in self.extractFormList(form[key]):
311 m = self.FV_SPECIAL.match(entry) 317 m = self.FV_SPECIAL.match(entry)
312 if not m: 318 if not m:
313 raise FormError, self._('The form action claims to ' 319 raise FormError (self._('The form action claims to '
314 'require property "%(property)s" ' 320 'require property "%(property)s" '
315 'which doesn\'t exist') % { 321 'which doesn\'t exist') % {
316 'property':propname} 322 'property':propname})
317 if m.group('classname'): 323 if m.group('classname'):
318 this = (m.group('classname'), m.group('id')) 324 this = (m.group('classname'), m.group('id'))
319 entry = m.group('propname') 325 entry = m.group('propname')
320 if not all_required.has_key(this): 326 if not all_required.has_key(this):
321 all_required[this] = [] 327 all_required[this] = []
330 mlaction = 'add' 336 mlaction = 'add'
331 337
332 # does the property exist? 338 # does the property exist?
333 if not propdef.has_key(propname): 339 if not propdef.has_key(propname):
334 if mlaction != 'set': 340 if mlaction != 'set':
335 raise FormError, self._('You have submitted a %(action)s ' 341 raise FormError (self._('You have submitted a %(action)s '
336 'action for the property "%(property)s" ' 342 'action for the property "%(property)s" '
337 'which doesn\'t exist') % { 343 'which doesn\'t exist') % {
338 'action': mlaction, 'property':propname} 344 'action': mlaction, 'property':propname})
339 # the form element is probably just something we don't care 345 # the form element is probably just something we don't care
340 # about - ignore it 346 # about - ignore it
341 continue 347 continue
342 proptype = propdef[propname] 348 proptype = propdef[propname]
343 349
344 # Get the form value. This value may be a MiniFieldStorage 350 # Get the form value. This value may be a MiniFieldStorage
345 # or a list of MiniFieldStorages. 351 # or a list of MiniFieldStorages.
346 value = form[key] 352 value = form[key]
347 353
348 # handle unpacking of the MiniFieldStorage / list form value 354 # handle unpacking of the MiniFieldStorage / list form value
349 if isinstance(proptype, hyperdb.Multilink): 355 if d['file']:
356 assert isinstance(proptype, hyperdb.Multilink)
357 # value is a file upload... we *always* handle multiple
358 # files here (html5)
359 if not isinstance(value, type([])):
360 value = [value]
361 elif isinstance(proptype, hyperdb.Multilink):
350 value = self.extractFormList(value) 362 value = self.extractFormList(value)
351 else: 363 else:
352 # multiple values are not OK 364 # multiple values are not OK
353 if isinstance(value, type([])): 365 if isinstance(value, type([])):
354 raise FormError, self._('You have submitted more than one ' 366 raise FormError (self._('You have submitted more than one '
355 'value for the %s property') % propname 367 'value for the %s property') % propname)
356 # value might be a file upload... 368 # value might be a single file upload
357 if not hasattr(value, 'filename') or value.filename is None: 369 if not getattr(value, 'filename', None):
358 # nope, pull out the value and strip it
359 value = value.value.strip() 370 value = value.value.strip()
360 371
361 # now that we have the props field, we need a teensy little 372 # now that we have the props field, we need a teensy little
362 # extra bit of help for the old :note field... 373 # extra bit of help for the old :note field...
363 if d['note'] and value: 374 if d['note'] and value:
375 for key, d in matches: 386 for key, d in matches:
376 if d['confirm'] and d['propname'] == propname: 387 if d['confirm'] and d['propname'] == propname:
377 confirm = form[key] 388 confirm = form[key]
378 break 389 break
379 else: 390 else:
380 raise FormError, self._('Password and confirmation text ' 391 raise FormError (self._('Password and confirmation text '
381 'do not match') 392 'do not match'))
382 if isinstance(confirm, type([])): 393 if isinstance(confirm, type([])):
383 raise FormError, self._('You have submitted more than one ' 394 raise FormError (self._('You have submitted more than one '
384 'value for the %s property') % propname 395 'value for the %s property') % propname)
385 if value != confirm.value: 396 if value != confirm.value:
386 raise FormError, self._('Password and confirmation text ' 397 raise FormError (self._('Password and confirmation text '
387 'do not match') 398 'do not match'))
388 try: 399 try:
389 value = password.Password(value, scheme = proptype.scheme, 400 value = password.Password(value, scheme = proptype.scheme,
390 config=self.db.config) 401 config=self.db.config)
391 except hyperdb.HyperdbValueError, msg: 402 except hyperdb.HyperdbValueError, msg:
392 raise FormError, msg 403 raise FormError (msg)
393 404
405 if d['file']:
406 # This needs to be a Multilink and is checked above
407 fcn = 'file'
408 fcl = self.db.classes[fcn]
409 fpropname = 'content'
410 if not all_propdef.has_key(fcn):
411 all_propdef[fcn] = fcl.getprops()
412 fpropdef = all_propdef[fcn]
413 have_file = []
414 for n, v in enumerate(value):
415 if not hasattr(v, 'filename'):
416 raise FormError (self._('Not a file attachment'))
417 # skip if the upload is empty
418 if not v.filename:
419 continue
420 fnodeid = str (-(n+1))
421 have_file.append(fnodeid)
422 fthis = (fcn, fnodeid)
423 if fthis not in all_props:
424 all_props[fthis] = {}
425 fprops = all_props[fthis]
426 all_links.append((cn, nodeid, 'files', [('file', fnodeid)]))
427
428 fprops['content'] = self.parse_file(fpropdef, fprops, v)
429 value = None
430 nodeid = None
394 elif isinstance(proptype, hyperdb.Multilink): 431 elif isinstance(proptype, hyperdb.Multilink):
395 # convert input to list of ids 432 # convert input to list of ids
396 try: 433 try:
397 l = hyperdb.rawToHyperdb(self.db, cl, nodeid, 434 l = hyperdb.rawToHyperdb(self.db, cl, nodeid,
398 propname, value) 435 propname, value)
399 except hyperdb.HyperdbValueError, msg: 436 except hyperdb.HyperdbValueError, msg:
400 raise FormError, msg 437 raise FormError (msg)
401 438
402 # now use that list of ids to modify the multilink 439 # now use that list of ids to modify the multilink
403 if mlaction == 'set': 440 if mlaction == 'set':
404 value = l 441 value = l
405 else: 442 else:
417 # the list 454 # the list
418 for entry in l: 455 for entry in l:
419 try: 456 try:
420 existing.remove(entry) 457 existing.remove(entry)
421 except ValueError: 458 except ValueError:
422 raise FormError, self._('property ' 459 raise FormError (self._('property '
423 '"%(propname)s": "%(value)s" ' 460 '"%(propname)s": "%(value)s" '
424 'not currently in list') % { 461 'not currently in list') % {
425 'propname': propname, 'value': entry} 462 'propname': propname, 'value': entry})
426 else: 463 else:
427 # add - easy, just don't dupe 464 # add - easy, just don't dupe
428 for entry in l: 465 for entry in l:
429 if entry not in existing: 466 if entry not in existing:
430 existing.append(entry) 467 existing.append(entry)
437 # other types should be None'd if there's no value 474 # other types should be None'd if there's no value
438 value = None 475 value = None
439 else: 476 else:
440 # handle all other types 477 # handle all other types
441 try: 478 try:
442 if isinstance(proptype, hyperdb.String): 479 # Try handling file upload
443 if (hasattr(value, 'filename') and 480 if (isinstance(proptype, hyperdb.String) and
444 value.filename is not None): 481 hasattr(value, 'filename') and
445 # skip if the upload is empty 482 value.filename is not None):
446 if not value.filename: 483 value = self.parse_file(propdef, props, value)
447 continue
448 # this String is actually a _file_
449 # try to determine the file content-type
450 fn = value.filename.split('\\')[-1]
451 if propdef.has_key('name'):
452 props['name'] = fn
453 # use this info as the type/filename properties
454 if propdef.has_key('type'):
455 if hasattr(value, 'type') and value.type:
456 props['type'] = value.type
457 elif mimetypes.guess_type(fn)[0]:
458 props['type'] = mimetypes.guess_type(fn)[0]
459 else:
460 props['type'] = "application/octet-stream"
461 # finally, read the content RAW
462 value = value.value
463 else:
464 value = hyperdb.rawToHyperdb(self.db, cl,
465 nodeid, propname, value)
466
467 else: 484 else:
468 value = hyperdb.rawToHyperdb(self.db, cl, nodeid, 485 value = hyperdb.rawToHyperdb(self.db, cl, nodeid,
469 propname, value) 486 propname, value)
470 except hyperdb.HyperdbValueError, msg: 487 except hyperdb.HyperdbValueError, msg:
471 raise FormError, msg 488 raise FormError (msg)
472 489
473 # register that we got this property 490 # register that we got this property
474 if isinstance(proptype, hyperdb.Multilink): 491 if isinstance(proptype, hyperdb.Multilink):
475 if value != []: 492 if value != []:
476 got_props[this][propname] = 1 493 got_props[this][propname] = 1
524 elif isinstance(proptype, hyperdb.String) and value == '': 541 elif isinstance(proptype, hyperdb.String) and value == '':
525 continue 542 continue
526 543
527 props[propname] = value 544 props[propname] = value
528 545
529 # check to see if we need to specially link a file to the note 546 # check to see if we need to specially link files to the note
530 if have_note and have_file: 547 if have_note and have_file:
531 all_links.append(('msg', '-1', 'files', [('file', '-1')])) 548 for fid in have_file:
549 all_links.append(('msg', '-1', 'files', [('file', fid)]))
532 550
533 # see if all the required properties have been supplied 551 # see if all the required properties have been supplied
534 s = [] 552 s = []
535 for thing, required in all_required.items(): 553 for thing, required in all_required.items():
536 # register the values we got 554 # register the values we got
564 ) % { 582 ) % {
565 'class': self._(thing[0]), 583 'class': self._(thing[0]),
566 'property': ', '.join(map(self.gettext, required)) 584 'property': ', '.join(map(self.gettext, required))
567 }) 585 })
568 if s: 586 if s:
569 raise FormError, '\n'.join(s) 587 raise FormError ('\n'.join(s))
570 588
571 # When creating a FileClass node, it should have a non-empty content 589 # When creating a FileClass node, it should have a non-empty content
572 # property to be created. When editing a FileClass node, it should 590 # property to be created. When editing a FileClass node, it should
573 # either have a non-empty content property or no property at all. In 591 # either have a non-empty content property or no property at all. In
574 # the latter case, nothing will change. 592 # the latter case, nothing will change.
579 elif isinstance(self.db.classes[cn], hyperdb.FileClass): 597 elif isinstance(self.db.classes[cn], hyperdb.FileClass):
580 if id is not None and id.startswith('-'): 598 if id is not None and id.startswith('-'):
581 if not props.get('content', ''): 599 if not props.get('content', ''):
582 del all_props[(cn, id)] 600 del all_props[(cn, id)]
583 elif props.has_key('content') and not props['content']: 601 elif props.has_key('content') and not props['content']:
584 raise FormError, self._('File is empty') 602 raise FormError (self._('File is empty'))
585 return all_props, all_links 603 return all_props, all_links
604
605 def parse_file(self, fpropdef, fprops, v):
606 # try to determine the file content-type
607 fn = v.filename.split('\\')[-1]
608 if fpropdef.has_key('name'):
609 fprops['name'] = fn
610 # use this info as the type/filename properties
611 if fpropdef.has_key('type'):
612 if hasattr(v, 'type') and v.type:
613 fprops['type'] = v.type
614 elif mimetypes.guess_type(fn)[0]:
615 fprops['type'] = mimetypes.guess_type(fn)[0]
616 else:
617 fprops['type'] = "application/octet-stream"
618 # finally, read the content RAW
619 return v.value
586 620
587 def extractFormList(self, value): 621 def extractFormList(self, value):
588 ''' Extract a list of values from the form value. 622 ''' Extract a list of values from the form value.
589 623
590 It may be one of: 624 It may be one of:

Roundup Issue Tracker: http://roundup-tracker.org/