1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196
|
Chapter 2: Object Publishing
Introduction
Zope puts your objects on the web. This is called *object
publishing*. One of Zope's unique characteristics is the way it
allows you to walk up to your objects and call methods on them
with simple URLs. In addition to HTTP, Zope makes your objects
available to other network protocols including FTP, WebDAV and
XML-RPC.
In this chapter you'll find out exactly how Zope publishes
objects. You'll learn all you need to know in order to design your
objects for web publishing.
HTTP Publishing
When you contact Zope with a web browser, your browser sends an
HTTP request to Zope's web server. After the request is completely
received, it is processed by 'ZPublisher', which is Zope's object
publisher. 'ZPublisher' is a kind of light-weight ORB (Object
Request Broker). It takes the request and locates an object to
handle the request. The publisher uses the request URL as a map to
locate the published object. Finding an object to handle the
request is called *traversal*, since the publisher moves from
object to object as it looks for the right one. Once the published
object is found, the publisher calls a method on the published
object, passing it parameters as necessary. The publisher uses
information in the request to determine which method to call, and
what parameters to pass. The process of extracting parameters from
the request is called *argument marshalling*. The published object
then returns a response, which is passed back to Zope's web
server. The web server, then passes the response back to your web
browser.
The publishing process is summarized in [2-1]
"Object publishing":img:2-1:Figures/2-1.png
Typically the published object is a persistent object that the
published module loads from the ZODB. See Chapter 4 for more
information on the ZODB.
This chapter will cover all the steps of object publishing in
detail. To summarize, object publishing consists of the main
steps:
1. The client sends a request to the publisher
2. The publisher locates the published object using the request
URL as a map.
3. The publisher calls the published object with arguments from
the request.
4. The publisher interprets and returns the results to the
client.
The chapter will also cover all the technical details, special
cases and extra-steps that this list glosses over.
URL Traversal
Traversal is the process the publisher uses to locate the published
object. Typically the publisher locates the published object by
walking along the URL. Take for example a collection of objects::
class Classification:
...
class Animal:
...
def screech(self, ...):
...
vertebrates=Classification(...)
vertebrates.mammals=Classification(...)
vertebrates.reptiles=Classification(...)
vertebrates.mammals.monkey=Animal(...)
vertebrates.mammals.dog=Animal(...)
vertebrates.reptiles.lizard=Animal(...)
This collection of objects forms an object hierarchy. Using Zope
you can publish objects with URLs. For example, the URL
'http://zope/vertebrates/mammals/monkey/screech', will traverse
the object hierarchy, find the 'monkey' object and call its
'screech' method.
"Traversal path through an object hierarchy":img:2-2:Figures/2-2.png
The publisher starts from the root object and takes each step in
the URL as a key to locate the next object. It moves to the next
object and continues to move from object to object using the URL
as a guide.
Typically the next object is a sub-object of the current object
that is named by the path segment. So in the example above, when
the publisher gets to the 'vertebrates' object, the next path
segment is "mammals", and this tells the publisher to look for a
sub-object of the current object with that name. Traversal stops
when Zope comes to the end of the URL. If the final object is
found, then it is published, otherwise an error is returned.
Now let's take a more rigorous look at traversal.
Traversal Interfaces
Zope defines interfaces for publishable objects, and publishable
modules.
When you are developing for Zope you almost always use the
'Zope' package as your published module. However, if you are
using 'ZPublisher' outside of Zope you'll be interested in the
published module interface.
Publishable Object Requirements
Zope has few restrictions on publishable objects. The basic
rule is that the object must have a doc string. This
requirement goes for method objects too.
Another requirement is that a publishable object must not have
a name that begin with an underscore. These two restrictions
are designed to keep private objects from being published.
Finally, published objects cannot be Python module objects.
Traversal Methods
During traversal, 'ZPublisher' cuts the URL into path elements
delimited by slashes, and uses each path element to traverse
from the current object to the next object. 'ZPublisher'
locates the next object in one of three ways:
1. Using '__bobo_traverse__'
2. Using 'getattr'
3. Using dictionary access.
First the publisher attempts to call the traversal hook
method, '__bobo_traverse__'. If the current object has this
method it is called with the request and the current path
element. The method should return the next object or 'None' to
indicate that a next object can't be found. You can also
return a tuple of objects from '__bobo_traverse__' indicating
a sequence of sub-objects. This allows you to add additional
parent objects into the request. This is almost never
necessary.
Here's an example of how to use '__bobo_traverse__'::
def __bobo_traverse__(self, request, key):
# if there is a special cookie set, return special
# subobjects, otherwise return normal subobjects
if request.cookies.has_key('special'):
# return a subobject from the special dict
return self.special_subobjects.get(key, None)
# otherwise return a subobject from the normal dict
return self.normal_subobjects.get(key, None)
This example shows how you can examine the request during
the traversal process.
If the current object does not define a '__bobo_traverse__'
method, then the next object is searched for using 'getattr'.
This locates sub-objects in the normal Python sense.
If the next object can't be found with 'getattr', 'ZPublisher'
calls on the current object as though it were a
dictionary. Note: the path element will be a string, not an
integer, so you cannot traverse sequences using index numbers
in the URL.
For example, suppose 'a' is the current object, and 'next' is
the name of the path element. Here are the three things that
'ZPublisher' will try in order to find the next object:
1. 'a.__bobo_traverse__("next")'
2. 'a.next'
3. 'a["next"]'
If the next object isn't found by any of these means
'ZPublisher' returns a HTTP 404 (Not Found) exception.
Publishing Methods
Once the published object is located with traversal, Zope
*publishes* it in one of three possible ways.
Calling the published object -- If the published object is a
function or method or other callable object, the publisher
calls it. Later in the chapter you'll find out how the
publisher figures out what arguments to pass when calling.
Calling the default method -- If the published object is not
callable, the publisher uses the default method. For HTTP
'GET' and 'POST' requests the default method is
'index_html'. For other HTTP requests such as 'PUT' the
publisher looks for a method named by the HTTP method. So for
an HTTP 'HEAD' request, the publisher would call the 'HEAD'
method on the published object.
Stringifying the published object -- If the published object
isn't callable, and doesn't have a default method, the
publisher publishes it using the Python 'str' function to turn
it into a string.
After the response method has been determined and called, the
publisher must interpret the results.
HTTP Responses
Normally the published method returns a string which is
considered the body of the HTTP response. The response
headers can be controlled by calling methods on the response
object, which is described later in the chapter. Optionally,
the published method can return a tuple with the title, and
body of the response. In this case, the publisher returns an
generated HTML page, with the first item of the tuple used
for the HTML 'title' of the page, and the second item as the
contents of the HTML 'body' tag. For example a response of::
('response', 'the response')
is turned into this HTML page::
<html>
<head><title>response</title></head>
<body>the response</body>
</html>
Controlling Base HREF
When you publish an object that returns HTML relative links
should allow you to navigate between methods. Consider this
example::
class Example:
"example"
def one(self):
"method one"
return """<html>
<head>
<title>one</title>
</head>
<body>
<a href="two">two</a>
</body>
</html>"""
def two(self):
"method two"
return """<html>
<head>
<title>two</title>
</head>
<body>
<a href="one">one</a>
</body>
</html>"""
The relative links in methods 'one' and 'two' allow you to
navigate between the methods.
However, the default method, 'index_html' presents a
problem. Since you can access the 'index_html' method
without specifying the method name in the URL, relative
links returned by the 'index_html' method won't work
right. For example::
class Example:
"example"
def index_html(self):
return """<html>
<head>
<title>one</title>
</head>
<body>
<a href="one">one</a><br>
<a href="two">two</a>
</body>
</html>"""
...
If you publish an instance of the 'Example' class with the
URL 'http://zope/example', then the relative link to method
'one' will be 'http://zope/one', instead of the correct
link, 'http://zope/example/one'.
Zope solves this problem for you by inserting a 'base' tag
inside the 'head' tag in the HTML output of 'index_html'
method when it is accessed as the default method. You will
probably never notice this, but if you see a mysterious
'base' tag in your HTML output, know you know where it came
from. You can avoid this behavior by manually setting your
own base with a 'base' tag in your 'index_html' method
output.
Response Headers
The publisher and the web server take care of setting response
headers such as 'Content-Length' and 'Content-Type'. Later in
the chapter you'll find out how to control these headers.
Later you'll also find out how exceptions are used to set the
HTTP response code.
Pre-Traversal Hook
The pre-traversal hook allows your objects to take special
action before they are traversed. This is useful for doing
things like changing the request. Applications of this include
special authentication controls, and virtual hosting support.
If your object has a method named
'__before_publishing_traverse__', the publisher will call it
with the current object and the request, before traversing
your object. Most often your method will change the
request. The publisher ignores anything you return from the
pre-traversal hook method.
The 'ZPublisher.BeforeTraverse' module contains some functions
that help you register pre-traversal callbacks. This allows
you to perform fairly complex callbacks to multiple objects
when a given object is about to be traversed.
Traversal and Acquisition
Acquisition affects traversal in several ways. See Chapter 5,
"Acquisition" for more information on acquisition. The most
obvious way in which acquisition affects traversal is in
locating the next object in a path. As we discussed earlier, the
next object during traversal is often found using
'getattr'. Since acquisition affects 'getattr', it will affect
traversal. The upshot is that when you are traversing objects
that support implicit acquisition, you can use traversal to walk
over acquired objects. Consider the object hierarchy rooted in
'fruit'::
from Acquisition import Implicit
class Node(Implicit):
...
fruit=Node()
fruit.apple=Node()
fruit.orange=Node()
fruit.apple.strawberry=Node()
fruit.orange.banana=Node()
When publishing these objects, acquisition can come into
play. For example, consider the URL */fruit/apple/orange*. The
publisher would traverse from 'fruit', to 'apple', and then
using acquisition, it would traverse to 'orange'.
Mixing acquisition and traversal can get complex. Consider the
URL */fruit/apple/orange/strawberry/banana*. This URL is
functional but confusing. Here's an even more perverse but legal
URL */fruit/apple/orange/orange/apple/apple/banana*.
In general you should limit yourself to constructing URLs which
use acquisition to acquire along containment, rather than
context lines. It's reasonable to publish an object or method
that you acquire from your container, but it's probably a bad
idea to publish an object or method that your acquire from
outside your container. For example::
from Acquisition import Implicit
class Basket(Implicit):
...
def numberOfItems(self):
"Returns the number of contained items"
...
class Vegetable(Implicit):
...
def texture(self):
"Returns the texture of the vegetable."
class Fruit(Implicit):
...
def color(self):
"Returns the color of the fruit."
basket=Basket()
basket.apple=Fruit()
basket.carrot=Vegetable()
The URL */basket/apple/numberOfItems* uses acquisition along
containment lines to publish the 'numberOfItems' method
(assuming that 'apple' doesn't have a 'numberOfItems'
attribute). However, the URL */basket/carrot/apple/texture*
uses acquisition to locate the 'texture' method from the 'apple'
object's context, rather than from its container. While this
distinction may be obscure, the guiding idea is to keep URLs as
simple as possible. By keeping acquisition simple and along
containment lines your application increases in clarity, and
decreases in fragility.
A second usage of acquisition in traversal concerns the
request. The publisher tries to make the request available to
the published object via acquisition. It does this by wrapping
the first object in an acquisition wrapper that allows it to
acquire the request with the name 'REQUEST'. This means that you
can normally acquire the request in the published object like
so::
request=self.REQUEST # for implicit acquirers
or like so::
request=self.aq_acquire('REQUEST') # for explicit acquirers
Of course, this will not work if your objects do not support
acquisition, or if any traversed objects have an attribute named
'REQUEST'.
Finally, acquisition has a totally different role in object
publishing related to security which we'll examine next.
Traversal and Security
As the publisher moves from object to object during traversal it
makes security checks. The current user must be authorized to
access each object along the traversal path. The publisher
controls access in a number of ways. For more information about
Zope security, see Chapter 6, "Security".
Basic Publisher Security
The publisher imposes a few basic restrictions on traversable
objects. These restrictions are the same of those for
publishable objects. As previously stated, publishable objects
must have doc strings and must not have names beginning with
underscore.
The following details are not important if you are using the
Zope framework. However, if your are publishing your own
modules, the rest of this section will be helpful.
The publisher checks authorization by examining the
'__roles__' attribute of each object as it performs
traversal. If present, the '__roles__' attribute should be
'None' or a list of role names. If it is None, the object is
considered public. Otherwise the access to the object requires
validation.
Some objects such as functions and methods do not support
creating attributes (at least they didn't before Python
2). Consequently, if the object has no '__roles__' attribute,
the publisher will look for an attribute on the object's
parent with the name of the object followed by
'__roles__'. For example, a function named 'getInfo' would
store its roles in its parent's 'getInfo__roles__' attribute.
If an object has a '__roles__' attribute that is not empty and
not 'None', the publisher tries to find a user database to
authenticate the user. It searches for user databases by
looking for an '__allow_groups__' attribute, first in the
published object, then in the previously traversed object, and
so on until a user database is found.
When a user database is found, the publisher attempts to
validate the user against the user database. If validation
fails, then the publisher will continue searching for user
databases until the user can be validated or until no more
user databases can be found.
The user database may be an object that provides a validate
method::
validate(request, http_authorization, roles)
where 'request' is a mapping object that contains request
information, 'http_authorization' is the value of the HTTP
'Authorization' header or 'None' if no authorization header
was provided, and 'roles' is a list of user role names.
The validate method returns a user object if succeeds, and
'None' if it cannot validate the user. See Chapter 6 for more
information on user objects. Normally, if the validate method
returns 'None', the publisher will try to use other user
databases, however, a user database can prevent this by
raising an exception.
If validation fails, Zope will return an HTTP header that
causes your browser to display a user name and password
dialog. You can control the realm name used for basic
authentication by providing a module variable named
'__bobo_realm__'. Most web browsers display the realm name in
the user name and password dialog box.
If validation succeeds the publisher assigns the user object
to the request variable, 'AUTHENTICATED_USER'. The publisher
places no restriction on user objects.
Zope Security
When using Zope rather than publishing your own modules, the
publisher uses acquisition to locate user folders and perform
security checks. The upshot of this is that your published
objects must inherit from 'Acquisition.Implicit' or
'Acquisition.Explicit'. See Chapter 5, "Acquisition", for more
information about these classes. Also when traversing each
object must be returned in an acquisition context. This is
done automatically when traversing via 'getattr', but you must
wrap traversed objects manually when using '__getitem__' and
'__bobo_traverse__'. For example::
class Example(Acquisition.Explicit):
...
def __bobo_traverse__(self, name, request):
...
next_object=self._get_next_object(name)
return next_object.__of__(self)
Additionally you will need to make security declarations on
your traversed object using 'ClassSecurityInfo' as described
in Chapter 6, "Security".
Finally, traversal security can be circumvented with the
'__allow_access_to_unprotected_subobjects__' attribute as
described in Chapter 6, "Security".
Environment Variables
You can control some facets of the publisher's operation by
setting environment variables.
'Z_DEBUG_MODE' -- Sets debug mode. In debug mode tracebacks are
not hidden in error pages. Also debug mode causes 'DTMLFile'
objects, External Methods and help topics to reload their
contents from disk when changed. You can also set debug mode
with the '-D' switch when starting Zope.
'Z_REALM' -- Sets the basic authorization realm. This controls
the realm name as it appears in the web browser's username and
password dialog. You can also set the realm with the
'__bobo_realm__' module variable, as mentioned previously.
'PROFILE_PUBLISHER' -- Turns on profiling and sets the name of
the profile file. See the Python documentation for more
information about the Python profiler.
Many more options can be set using switches on the startup
script. See the *Zope Administrator's Guide* for more
information.
Testing
ZPublisher comes with built-in support for testing and working
with the Python debugger. This topic is covered in more detail
in Chapter 7, "Testing and Debugging".
Publishable Module
If you are using the Zope framework, this section will be
irrelevant to you. However, if you are publishing your own
modules with 'ZPublisher' read on.
The publisher begins the traversal process by locating an object
in the module's global namespace that corresponds to the first
element of the path. Alternately the first object can be located
by one of two hooks.
If the module defines a 'web_objects' or 'bobo_application'
object, the first object is searched for in those objects. The
search happens according to the normal rules of traversal, using
'__bobo_traverse__', 'getattr', and '__getitem__'.
The module can receive callbacks before and after traversal. If
the module defines a '__bobo_before__' object, it will be called
with no arguments before traversal. Its return value is
ignored. Likewise, if the module defines a '__bobo_after__'
object, it will be called after traversal with no
arguments. These callbacks can be used for things like acquiring
and releasing locks.
Calling the Published Object
Now that we've covered how the publisher located the published
object and what it does with the results of calling it, let's take
a closer look at how the published object is called.
The publisher marshals arguments from the request and
automatically makes them available to the published object. This
allows you to accept parameters from web forms without having to
parse the forms. Your objects usually don't have to do anything
special to be called from the web. Consider this function::
def greet(name):
"greet someone"
return "Hello, %s" % name
You can provide the 'name' argument to this function by calling it
with a URL like *greet?name=World*. You can also call it with a
HTTP 'POST' request which includes 'name' as a form variable.
In the next sections we'll take a closer look at how the publisher
marshals arguments.
Marshalling Arguments from the Request
The publisher marshals form data from GET and POST
requests. Simple form fields are made available as Python
strings. Multiple fields such as form check boxes and multiple
selection lists become sequences of strings. File upload fields
are represented with 'FileUpload' objects. File upload objects
behave like normal Python file objects and additionally have a
'filename' attribute which is the name of the file and a
'headers' attribute which is a dictionary of file upload
headers.
The publisher also marshals arguments from CGI environment
variables and cookies. When locating arguments, the publisher
first looks in CGI environment variables, then other request
variables, then form data, and finally cookies. Once a variable
is found, no further searching is done. So for example, if your
published object expects to be called with a form variable named
'SERVER_URL', it will fail, since this argument will be
marshaled from the CGI environment first, before the form data.
The publisher provides a number of additional special variables
such as 'URL0' which are derived from the request. These are
covered in the 'HTTPRequest' API documentation.
Argument Conversion
The publisher supports argument conversion. For example consider
this function::
def onethird(number):
"returns the number divided by three"
return number / 3.0
This function cannot be called from the web because by default
the publisher marshals arguments into strings, not numbers. This
is why the publisher provides a number of converters. To signal
an argument conversion you name your form variables with a colon
followed by a type conversion code. For example, to call the
above function with 66 as the argument you can use this URL
*onethird?number:int=66* The publisher supports many
converters:
boolean -- Converts a variable to true or false. Variables
that are 0, None, an empty string, or an empty sequence are
false, all others are true.
int -- Converts a variable to a Python integer.
long -- Converts a variable to a Python long integer.
float -- Converts a variable to a Python floating point
number.
string -- Converts a variable to a Python string.
required -- Raises an exception if the variable is not present
or is an empty string.
ignore_empty -- Excludes a variable from the request if the
variable is an empty string.
date -- Converts a string to a *DateTime* object. The formats
accepted are fairly flexible, for example '10/16/2000',
'12:01:13 pm'.
list -- Converts a variable to a Python list of values, even
if there is only one value.
tuple -- Converts a variable to a Python tuple of values, even
if there is only one value.
lines -- Converts a string to a Python list of values by
splitting the string on line breaks.
tokens -- Converts a string to a Python list of values by
splitting the string on spaces.
text -- Converts a variable to a string with normalized line
breaks. Different browsers on various platforms encode line
endings differently, so this converter makes sure the line
endings are consistent, regardless of how they were encoded by
the browser.
If the publisher cannot coerce a request variable into the type
required by the type converter it will raise an error. This is
useful for simple applications, but restricts your ability to
tailor error messages. If you wish to provide your own error
messages, you should convert arguments manually in your
published objects rather than relying on the publisher for
coercion. Another possibility is to use JavaScript to validate
input on the client-side before it is submitted to the server.
You can combine type converters to a limited extent. For example
you could create a list of integers like so::
<input type="checkbox" name="numbers:list:int" value="1">
<input type="checkbox" name="numbers:list:int" value="2">
<input type="checkbox" name="numbers:list:int" value="3">
In addition to these type converters, the publisher also supports
method and record arguments.
Method Arguments
Sometimes you may wish to control which object is published
based on form data. For example, you might want to have a form
with a select list that calls different methods depending on
the item chosen. Similarly, you might want to have multiple
submit buttons which invoke a different method for each
button.
The publisher provides a way to select methods using form
variables through use of the *method* argument type. The
method type allows the request 'PATH_INFO' to be augmented
using information from a form item name or value.
If the name of a form field is ':method', then the value of
the field is added to 'PATH_INFO'. For example, if the
original 'PATH_INFO' is 'foo/bar' and the value of a ':method'
field is 'x/y', then 'PATH_INFO' is transformed to
'foo/bar/x/y'. This is useful when presenting a select
list. Method names can be placed in the select option values.
If the name of a form field ends in ':method' then the part of
the name before ':method' is added to 'PATH_INFO'. For
example, if the original 'PATH_INFO' is 'foo/bar' and there is
a 'x/y:method' field, then 'PATH_INFO' is transformed to
'foo/bar/x/y'. In this case, the form value is ignored. This
is useful for mapping submit buttons to methods, since submit
button values are displayed and should, therefore, not contain
method names.
Only one method field should be provided. If more than one
method field is included in the request, the behavior is
undefined.
Record Arguments
Sometimes you may wish to consolidate form data into a
structure rather than pass arguments individually. Record
arguments allow you to do this.
The 'record' type converter allows you to combine multiple
form variables into a single input variable. For example::
<input name="date.year:record:int">
<input name="date.month:record:int">
<input name="date.day:record:int">
This form will result in a single variable, 'date', with
attributes 'year', 'month', and 'day'.
You can skip empty record elements with the 'ignore_empty'
converter. For example::
<input type="text" name="person.email:record:ignore_empty">
When the email form field is left blank the publisher skips
over the variable rather than returning a null string as its
value. When the record 'person' is returned it will not have
an 'email' attribute if the user did not enter one.
You can also provide default values for record elements with
the 'default' converter. For example::
<input type="hidden"
name="pizza.toppings:record:list:default"
value="All">
<select multiple name="pizza.toppings:record:list:ignore_empty">
<option>Cheese</option>
<option>Onions</option>
<option>Anchovies</option>
<option>Olives</option>
<option>Garlic<option>
</select>
The 'default' type allows a specified value to be inserted
when the form field is left blank. In the above example, if
the user does not select values from the list of toppings, the
default value will be used. The record 'pizza' will have the
attribute 'toppings' and its value will be the list containing
the word "All" (if the field is empty) or a list containing
the selected toppings.
You can even marshal large amounts of form data into multiple
records with the 'records' type converter. Here's an example::
<h2>Member One</h2>
Name:
<input type="text" name="members.name:records"><BR>
Email:
<input type="text" name="members.email:records"><BR>
Age:
<input type="text" name="members.age:int:records"><BR>
<H2>Member Two</H2>
Name:
<input type="text" name="members.name:records"><BR>
Email:
<input type="text" name="members.email:records"><BR>
Age:
<input type="text" name="members.age:int:records"><BR>
This form data will be marshaled into a list of records named
'members'. Each record will have a 'name', 'email', and 'age'
attribute.
Record marshalling provides you with the ability to create
complex forms. However, it is a good idea to keep your web
interfaces as simple as possible.
Exceptions
Unhandled exceptions are caught by the object publisher and are
translated automatically to nicely formatted HTTP output.
When an exception is raised, the exception type is mapped to an
HTTP code by matching the value of the exception type with a list
of standard HTTP status names. Any exception types that do not
match standard HTTP status names are mapped to "Internal Error"
(500). The standard HTTP status names are: "OK", "Created",
"Accepted", "No Content", "Multiple Choices", "Redirect", "Moved
Permanently", "Moved Temporarily", "Not Modified", "Bad Request",
"Unauthorized", "Forbidden", "Not Found", "Internal Error", "Not
Implemented", "Bad Gateway", and "Service Unavailable". Variations
on these names with different cases and without spaces are also
valid.
An attempt is made to use the exception value as the body of the
returned response. The object publisher will examine the exception
value. If the value is a string that contains some white space,
then it will be used as the body of the return error message. If
it appears to be HTML, the error content type will be set to
'text/html', otherwise, it will be set to 'text/plain'. If the
exception value is not a string containing white space, then the
object publisher will generate its own error message.
There are two exceptions to the
above rule:
1. If the exception type is: "Redirect", "Multiple Choices"
"Moved Permanently", "Moved Temporarily", or "Not
Modified", and the exception value is an absolute URI, then
no body will be provided and a 'Location' header will be
included in the output with the given URI.
2. If the exception type is "No Content", then no body will be
returned.
When a body is returned, traceback information will be included in
a comment in the output. As mentioned earlier, the environment
variable 'Z_DEBUG_MODE' can be used to control how tracebacks are
included. If this variable is set then tracebacks are included in
'PRE' tags, rather than in comments. This is very handy during
debugging.
Exceptions and Transactions
When Zope receives a request it begins a transaction. Then it
begins the process of traversal. Zope automatically commits the
transaction after the published object is found and called. So
normally each web request constitutes one transaction which Zope
takes care of for you. See Chapter 4. for more information on
transactions.
If an unhandled exception is raised during the publishing
process, Zope aborts the transaction. As detailed in Chapter
4. Zope handles 'ConflictErrors' by re-trying the request up to
three times. This is done with the 'zpublisher_exception_hook'.
In addition, the error hook is used to return an error message
to the user. In Zope the error hook creates error messages by
calling the 'raise_standardErrorMessage' method. This method is
implemented by 'SimpleItem.Item'. It acquires the
'standard_error_message' DTML object, and calls it with
information about the exception.
You will almost never need to override the
'raise_standardErrorMessage' method in your own classes, since
it is only needed to handle errors that are raised by other
components. For most errors, you can simply catch the exceptions
normally in your code and log error messages as needed. If you
need to, you should be able to customize application error
reporting by overriding the 'standard_error_message' DTML object
in your application.
Manual Access to Request and Response
You do not need to access the request and response directly most
of the time. In fact, it is a major design goal of the publisher
that most of the time your objects need not even be aware that
they are being published on the web. However, you have the ability
to exert more precise control over reading the request and
returning the response.
Normally published objects access the request and response by
listing them in the signature of the published method. If this is
not possible you can usually use acquisition to get a reference to
the request. Once you have the request, you can always get the
response from the request like so::
response=REQUEST.RESPONSE
The APIs of the request and response are covered in the API
documentation. Here we'll look at a few common uses of the request
and response.
One reason to access the request is to get more precise
information about form data. As we mentioned earlier, argument
marshalling comes from a number of places including cookies, form
data, and the CGI environment. For example, you can use the
request to differentiate between form and cookie data::
cookies=REQUEST.cookies # a dictionary of cookie data
form=REQUEST.form # a dictionary of form data
One common use of the response object is to set response headers.
Normally the publisher in concert with the web server will take
care of response headers for you. However, sometimes you may wish
manually control headers::
RESPONSE.setHeader('Pragma', 'No-Cache')
Another reason to access the response is to stream response
data. You can do this with the 'write' method::
while 1:
data=getMoreData() #this call may block for a while
if not data:
break
RESPONSE.write(data)
Here's a final example that shows how to detect if your method is
being called from the web. Consider this function::
def feedParrot(parrot_id, REQUEST=None):
...
if REQUEST is not None:
return "<html><p>Parrot %s fed</p></html>" % parrot_id
The 'feedParrot' function can be called from Python, and also from
the web. By including 'REQUEST=None' in the signature you can
differentiate between being called from Python and being called
form the web. When the function is called from Python nothing is
returned, but when it is called from the web the function returns
an HTML confirmation message.
Other Network Protocols
FTP
Zope comes with an FTP server which allows users to treat the
Zope object hierarchy like a file server. As covered in Chapter
3, Zope comes with base classes ('SimpleItem' and
'ObjectManager') which provide simple FTP support for all Zope
objects. The FTP API is covered in the API reference.
To support FTP in your objects you'll need to find a way to
represent your object's state as a file. This is not possible or
reasonable for all types of objects. You should also consider
what users will do with your objects once they access them via
FTP. You should find out which tools users are likely to edit
your object files. For example, XML may provide a good way to
represent your object's state, but it may not be easily editable
by your users. Here's an example class that represents itself
as a file using RFC 822 format::
from rfc822 import Message
from cStringIO import StringIO
class Person(...):
def __init__(self, name, email, age):
self.name=name
self.email=email
self.age=age
def writeState(self):
"Returns object state as a string"
return "Name: %s\nEmail: %s\nAge: %s" % (self.name,
self.email,
self.age)
def readState(self, data):
"Sets object state given a string"
m=Message(StringIO(data))
self.name=m['name']
self.email=m['email']
self.age=int(m['age'])
The 'writeState' and 'readState' methods serialize and
unserialize the 'name', 'age', and 'email' attributes to and
from a string. There are more efficient ways besides RFC 822 to
store instance attributes in a file, however RFC 822 is a simple
format for users to edit with text editors.
To support FTP all you need to do at this point is implement the
'manage_FTPget' and 'PUT' methods. For example::
def manage_FTPget(self):
"Returns state for FTP"
return self.writeState()
def PUT(self, REQUEST):
"Sets state from FTP"
self.readState(REQUEST['BODY'])
You may also choose to implement a 'get_size' method which
returns the size of the string returned by 'manage_FTPget'. This
is only necessary if calling 'manage_FTPget' is expensive, and
there is a more efficient way to get the size of the file. In
the case of this example, there is no reason to implement a
'get_size' method.
One side effect of implementing 'PUT' is that your object now
supports HTTP PUT publishing. See the next section on WebDAV for
more information on HTTP PUT.
That's all there is to making your object work with FTP. As
you'll see next WebDAV support is similar.
WebDAV
WebDAV is a protocol for collaboratively edit and manage files
on remote servers. It provides much the same functionality as
FTP, but it works over HTTP.
It is not difficult to implement WebDAV support for your
objects. Like FTP, the most difficult part is to figure out how
to represent your objects as files.
Your class must inherit from 'webdav.Resource' to get basic DAV
support. However, since 'SimpleItem' inherits from 'Resource',
your class probably already inherits from 'Resource'. For
container classes you must inherit from
'webdav.Collection'. However, since 'ObjectManager' inherits
from 'Collection' you are already set so long as you inherit
from 'ObjectManager'.
In addition to inheriting from basic DAV classes, your classes
must implement 'PUT' and 'manage_FTPget'. These two methods are
also required for FTP support. So by implementing WebDAV
support, you also implement FTP support.
The permissions that you assign to these two methods will
control the ability to read and write to your class through
WebDAV, but the ability to see your objects is controlled
through the "WebDAV access" permission.
Supporting Write Locking
Write locking is a feature of WebDAV that allows users to put
a lock on objects they are working on. Support write locking
is easy. To implement write locking you must assert that your
class implements the 'WriteLockInterface'. For example::
from webdav.WriteLockInterface import WriteLockInterface
class MyContentClass(OFS.SimpleItem.Item, Persistent):
__implements__ = (WriteLockInterface,)
It's sufficient to inherit from 'SimpleItem.Item', since it
inherits from 'webdav.Resource', which provides write locking
along with other DAV support.
In addition, your 'PUT' method should begin with calls to
'dav__init' and 'dav_simpleifhandler'. For example::
def PUT(self, REQUEST, RESPONSE):
"""
Implement WebDAV/HTTP PUT/FTP put method for this object.
"""
self.dav__init(REQUEST, RESPONSE)
self.dav__simpleifhandler(REQUEST, RESPONSE)
...
Finally your class's edit methods should check to determine
whether your object is locked using the 'ws_isLocked'
method. If someone attempts to change your object when it is
locked you should raise the 'ResourceLockedError'. For
example::
from webdav import ResourceLockedError
class MyContentClass(...):
...
def edit(self, ...):
if self.ws_isLocked():
raise ResourceLockedError
...
WebDAV support is not difficult to implement, and as more
WebDAV editors become available, it will become more
valuable. If you choose to add FTP support to your class you
should probably go ahead and support WebDAV too since it is
so easy once you've added FTP support.
XML-RPC
"XML-RPC":http://www.xmlrpc.com is a light-weight Remote
Procedure Call protocol that uses XML for encoding and HTTP for
transport. Fredrick Lund maintains a Python "XML-RPC
module":http://www.pthonware.com/products/xmlrpc.
All objects in Zope support XML-RPC publishing. Generally you
will select a published object as the end-point and select one
of its methods as the method. For example you can call the
'getId' method on a Zope folder at 'http://example.com/myfolder'
like so::
import xmlrpclib
folder=xmlrpclib.Server('http://example.com/myfolder')
ids=folder.getId()
You can also do traversal via a dotted method name. For
example::
import xmlrpclib
# traversal via dotted method name
app=xmlrpclib.Server('http://example.com/app')
id1=app.folderA.folderB.getId()
# walking directly up to the published object
folderB=xmlrpclib.Server('http://example.com/app/folderA/folderB')
id2=folderB.getId()
print id1==id2
This example shows different routes to the same object
publishing call.
XML-RPC supports marshalling of basic Python types for both
publishing requests and responses. The upshot of this
arrangement is that when you are designing methods for use via
XML-RPC you should limit your arguments and return values to
simple values such as Python strings, lists, numbers and
dictionaries. You should not accept or return Zope objects from
methods that will be called via XML-RPC.
XML-RPC does not support keyword arguments. This is a problem if
your method expect keyword arguments. This problem is
noticeable when calling DTMLMethods and DTMLDocuments with XML-RPC.
Normally a DTML object should be called with the request as the
first argument, and additional variables as keyword arguments.
You can get around this problem by passing a dictionary as the
first argument. This will allow your DTML methods and documents
to reference your variables with the 'var' tag.
However, you cannot do the following::
<dtml-var expr="REQUEST['argument']">
Although the following will work::
<dtml-var expr="_['argument']">
This is because in this case arguments *are* in the DTML
namespace, but they are not coming from the web request.
In general it is not a good idea to call DTML from XML-RPC since
DTML usually expects to be called from normal HTTP requests.
One thing to be aware of is that Zope returns 'false' for
published objects which return None since XML-RPC has no concept
of null.
Another issue you may run into is that 'xmlrpclib' does not yet
support HTTP basic authentication. This makes it difficult to
call protected web resources. One solution is to patch
'xmlrpclib'. Another solution is to accept authentication
credentials in the signature of your published method.
Summary
Object publishing is a simple and powerful way to bring objects to
the web. Two of Zope's most appealing qualities is how it maps
objects to URLs, and you don't need to concern yourself with web
plumbing. If you wish, there are quite a few details that you can
use to customize how your objects are located and published.
|