Mercurial > p > roundup > code
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: |
