Anhang B: Code-Listings
Artikelaktionen
Anhang B
Code-Listings
Dieser Anhang enthält Listings von Code, wie er im restlichen Buch verwendet wird. Manche dieser Skripten, die extern ausgeführt werden, verlangen, dass bei Ihnen das Verzeichnis /Zope/lib/python auf Ihrem Python-Pfad liegt. In Anhang A wird das näher beschrieben. All diese Skripten werden im Buch detailliert beschrieben.
Kapitel 5
Die folgenden Code-Listings stammen aus Kapitel 5.
Page Template: test_context
Listing B.1 zeigt alle Variablen, die in Page Templates verfügbar sind. Es kann verwendet werden, um nützliche Informationen bei der Fehlersuche auszugeben.
Listing B.1. test_context
<html>
<head />
<body>
<h1>Debug information</h1>
<h2>CONTEXTS</h2>
<ul>
<tal:block
tal:repeat="item CONTEXTS">
<li
tal:condition="python: item != 'request'"
tal:define="context CONTEXTS;">
<b tal:content="item" />
<span tal:replace="python: context[item]" />
</li>
</tal:block>
</ul>
<h2>REQUEST</h2>
<p tal:replace="structure request" />
</body>
</html>
Page Template: user_info (1)
user_info (1) ist ein rohes Page Template, das nicht dem Plone-Stil folgt und Benutzerinformationen ausgibt, wie in Listing B.2 zu sehen ist. Um dieses Template zu benutzen, müssen Sie die Benutzer-ID, die Sie untersuchen möchten, an den Abfrage-String übergeben. Beispiel: user_info?userName=andy.
Listing B.2. user_info (1)
<html>
<body>
<div
tal:omit-tag=""
tal:define="
userName request/userName|nothing;
userObj python: here.portal_membership.getMemberById(userName);
getPortrait nocall: here/portal_membership/getPersonalPortrait;
getFolder nocall: here/portal_membership/getHomeFolder
">
<p tal:condition="not: userName">
No username selected.
</p>
<p tal:condition="not: userObj">
That username does not exist.
</p>
<table tal:condition="userObj">
<tr>
<td>
<img src=""
tal:replace="structure python: getPortrait(userName)" />
</td>
<td>
<ul>
<li>
<i>Username:</i>
<span tal:replace="userName" />
</li>
<li>
<i>Full name:</i>
<span tal:replace="userObj/fullname" />
</li>
<li
tal:define="home python: getFolder(userName)"
tal:condition="home">
<i>Home folder:</i>
<a href=""
tal:attributes="href home/absolute_url"
tal:content="home/absolute_url">Folder</a>
</li>
<li>
<i>Email:</i>
<a href=""
tal:define="email userObj/email"
tal:attributes="href string:mailto:$email"
tal:content="email">Email</a>
</li>
<li>
<i>Last login time:</i>
<span tal:replace="userObj/last_login_time" />
</li>
</ul>
</td>
</tr>
</table>
</div>
</body>
</html>
Kapitel 6
Die folgenden Code-Listings stammen aus Kapitel 6.
Page Template: user_info (2)
Listing B.3 enthält eine verfeinerte Version des Page Templates user_info aus Kapitel 5. Den Benutzernamen müssen Sie nicht übergeben, da es über alle Mitglieder Ihrer Site iteriert.
Listing B.3. user_info (2)
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US"
lang="en-US"
i18n:domain="plone"
metal:use-macro="here/main_template/macros/master">
<body>
<div metal:fill-slot="main">
<tal:block
tal:define="
getPortrait nocall: here/portal_membership/getPersonalPortrait;
getFolder nocall: here/portal_membership/getHomeFolder
">
<table>
<tal:block
tal:repeat="userObj here/portal_membership/listMembers">
<metal:block
metal:use-macro="here/user_section/macros/userSection" />
</tal:block>
</table>
</tal:block>
</div>
</body>
</html>
Page Template: user_section
Listing B.4 enthält ein Page Template-Makro zum vorherigen Page Template user_info und zeigt bei jedem Aufruf die einzelnen Benutzerangaben.
Listing B.4. user_section
<div metal:define-macro="userSection"
tal:define="userName userObj/getUserName">
<tr>
<td>
<img src=""
tal:replace="structure python: getPortrait(userName)" />
</td>
<td tal:define="prop nocall: userObj/getProperty">
<ul>
<li>
<i>Username:</i>
<span tal:replace="userName" />
</li>
<li>
<i>Full name:</i>
<span tal:replace="python: prop('fullname')" />
</li>
<li
tal:define="home python: getFolder(userName)"
tal:condition="home">
<a href=""
tal:attributes="href home/absolute_url">Home Folder</a>
</li>
<li>
<i>Email:</i>
<a href=""
tal:define="email python: prop('email')"
tal:attributes="href string:mailto:$email"
tal:content="email">Email</a>
</li>
<li>
<i>Last login time:</i>
<span tal:replace="python: prop('last_login_time')" />
</li>
</ul>
</td>
</tr>
</div>
Script (Python): google_ad_portlet
Listing B.5 enthält ein Page Template, das Google-Anzeigen in einem Portlet darstellt.
Listing B.5. google_ad_portlet
<div metal:define-macro="portlet">
<div class="portlet">
<script type="text/javascript"><!--
google_ad_client = "yourUniqueValue";
google_ad_width = 120;
google_ad_height = 600;
google_ad_format = "120x600_as";
//--></script>
<script type="text/javascript"
src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
</script>
</div>
</div>
Script (Python): recently_changed
Listing B.6 enthält ein Skript, das alle ihm übergebenen Objekte untersucht und dann herausfindet, was es Neues gibt. Die Objekte müssen mit dem Parameter objects übergeben werden.
Listing B.6. recently_changed
##title=recentlyChanged
##parameters=objects
from DateTime import DateTime
now = DateTime()
difference = 5 # in Tagen
result = []
for object in objects:
diff = now - object.bobobase_modification_time()
if diff < difference:
dct = {"object":object,"diff":int(diff)}
result.append(dct)
return result
Externe Methode: readFile
Listing B.7 zeigt ein Beispiel für eine externe Methode, die eine Datei liest.
Listing B.7. readFile
def readFile(self): fh = open(r'c:\Program Files\Plone\Data\Extensions\README.txt', 'rb') data = fh.read() return data
Python-Skript: zpt.py
Listing B.8 enthält ein Skript, das völlig unabhängig von Plone ist und das eine Syntax-Prüfung eines Page Templates vornimmt. Das ist hilfreich bei der Fehlersuche von Page Templates, die außerhalb von Plone geschrieben werden.
Listing B.8. zpt.py
#!/usr/bin/python
from Products.PageTemplates.PageTemplate import PageTemplate
import sys
def test(file):
raw_data = open(file, 'r').read()
pt = PageTemplate()
pt.write(raw_data)
if pt._v_errors:
print "*** Error in:", file
for error in pt._v_errors[1:]:
print error
if __name__=='__main__':
if len(sys.argv) < 2:
print "python check.py file [files...]"
sys.exit(1)
else:
for arg in sys.argv[1:]:
test(arg)
Page Template: feedbackForm
Listing B.9 zeigt das Formular, in dem Benutzer Feedback eingeben können.
Listing B.9. feedbackForm
<!--
$Id: appB.rst,v 1.1 2005/09/08 12:41:33 dinu_gherman Exp $
Copyright: ClearWind Consulting Ltd
License: http://www.clearwind.ca/license
-->
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US"
lang="en-US" i18n:domain="plone"
metal:use-macro="here/main_template/macros/master">
<body>
<div metal:fill-slot="main"
tal:define="errors options/state/getErrors;">
<p>Please send us any feedback you might have about the
site.</p>
<form method="post" tal:attributes="action template/id;">
<fieldset>
<legend class="legend"
i18n:translate="legend_feedback_form">Website
Feedback</legend>
<div class="field"
tal:define="error_email_address errors/email_address|nothing;"
tal:attributes="class python:test(error_email_address, 'field error', 'field')">
<label i18n:translate="label_email_address">Your email
address</label>
<span class="fieldRequired" title="Required"
i18n:attributes="title"
i18n:translate="label_required">(Required)</span>
<div class="formHelp"
i18n:translate="label_email_address_help">Enter your
email address.</div>
<div tal:condition="error_email_address">
<tal:block i18n:translate=""
content="error_email_address">Error</tal:block>
</div>
<input type="text" name="email_address"
tal:define="user context/portal_membership/getAuthenticatedMember; email user/email|nothing"
tal:attributes="tabindex tabindex/next; value request/email_address|email|nothing" />
</div>
<div class="field">
<label i18n:translate="label_feedback_comments">
Comments</label>
<div class="formHelp" id="label_feedback_comments_help"
i18n:translate="label_feedback_comments_help">Enter the
comments you have about the site.</div>
<textarea name="comments" rows="10"
tal:content="request/comments|nothing"
tal:attributes="tabindex tabindex/next;" />
</div>
<div class="formControls">
<input class="context" type="submit" tabindex=""
name="form.button.Submit" value="Submit"
i18n:attributes="value"
tal:attributes="tabindex tabindex/next;" />
</div>
</fieldset>
<input type="hidden" name="form.submitted" value="1" />
</form>
</div>
</body>
</html>
Controller Python-Skript: sendEmail
Listing B.10 zeigt das Steuerskript, das dem Benutzer die E-Mail schickt.
Listing B.10. sendEmail
#!/usr/bin/python
#$Id: appB.rst,v 1.1 2005/09/08 12:41:33 dinu_gherman Exp $
#Copyright: ClearWind Consulting Ltd
#License: http://www.clearwind.ca/license
mhost = context.MailHost
emailAddress = context.REQUEST.get('email_address')
administratorEmailAddress = context.email_from_address
comments = context.REQUEST.get('comments')
# the message format, %s will be filled in from data
message = """
From: %s
To: %s
Subject: Website Feedback
%s
URL: %s
"""
# format the message
message = message % (
emailAddress,
administratorEmailAddress,
comments,
context.absolute_url())
mhost.send(message)
screenMsg = "Comments sent, thank you."
state.setKwargs( {'portal_status_message':screenMsg} )
return state
Controller Python-Skript: validEmail
Listing B.11 zeigt das Skript, das die E-Mail validiert.
Listing B.11. validEmail
#$Id: appB.rst,v 1.1 2005/09/08 12:41:33 dinu_gherman Exp $
#Copyright: ClearWind Consulting Ltd
#License: http://www.clearwind.ca/license
email = context.REQUEST.get('email_address', None)
if not email:
state.setError('email_address', 'An email address is required', new_status='failure')
if state.getErrors():
state.set(portal_status_message='Please correct the errors shown.')
return state
Kapitel 7
Die folgenden Code-Listings stammen aus Kapitel 7.
Script (Python): setSkin
Falls es in Form einer Zugriffsregel in Ihrer Site zugewiesen wurde, ändert das Skript in Listing B.12 alle Anfragen auf intern.einesite.org so ab, dass die Skin Plone Default verwendet wird. Sonst wird die Skin Custom Chrome verwendet.
Listing B.12. setSkin
##title=Skin changing script
##parameters=
req = context.REQUEST
if req['SERVER_URL'].find('internal.somesite.org') > -1:
context.changeSkin("Plone Default")
context.changeSkin("Custom Chrome")
CSS: ploneCustom.css
Listing B.13 zeigt das Cascading Stylesheet, das auf der NASA-Mars-Site verwendet wird.
Listing B.13. ploneCustom.css
body {
background: #343434;
}
#visual-portal-wrapper {
width: 680px;
margin: 1em auto 0 auto;
}
#portal-top {
background: url("http://mars.telascience.org/header.jpg") transparent no-repeat;
padding: 162px 0 0 0;
position: relative;
}
#portal-logo {
background: transparent;
background-image: none;
margin: 0;
position: absolute;
top: 130px;
left: 5px;
z-index: 20;
}
#portal-logo a {
padding-top: 25px;
height /**/: 25px;
width: 375px;
}
#portal-globalnav {
background: url("http://mars.telascience.org/listspacer.gif") transparent;
padding: 0;
height: 21px;
border: 0;
margin: 0 0 1px 6px;
clear: both;
}
#portal-globalnav li {
display: block;
float: left;
height: 21px;
background: url("http://mars.telascience.org/liststart.gif") transparent no-repeat;
padding: 0 0 0 33px;
margin: 0 0.5em 0 0;
}
#portal-globalnav li a {
display: block;
float: left;
height: 21px;
background: url("http://mars.telascience.org/listitem.gif") transparent right top;
padding: 0 33px 0 0;
border: 0;
line-height: 2em;
color: black;
font-size: 90%;
margin: 0;
}
#portal-globalnav li a:hover,
#portal-globalnav li.selected a {
background-color: transparent;
border: 0;
color: #444;
}
#portal-personaltools {
clear: both;
margin-left: 6px;
border-top-color: #776a44;
border-top-style: solid;
border-top-width: 1px;
}
#portal-breadcrumbs {
clear: both;
}
#portal-breadcrumbs,
#portal-columns,
.documentContent {
background: white;
margin-left: 6px;
}
.documentContent {
margin: 0;
font-size: 100%;
}
.screenshotThumb {
float:right;
}
#portal-footer {
margin: -1px 0 0 6px;
padding: 0.8em 0;
border: 1px solid #ddd;
border-style: solid none none none;
background: white;
color: #666;
font-size: 90%;
}
#portal-footer a {
color: #333;
text-decoration: underline;
}
dt {
color: #ECA200;
}
.documentDescription {
font-size: 110%;
}
#portal-breadcrumbs img {
display: none;
}
li.reqlist {
margin-top: 0;
margin-bottom: 0;
}
Kapitel 8
Die folgenden Code-Listings stammen aus Kapitel 8.
Script (Python): mail.py
Listing B.14 enthält ein Skript, das immer dann eine E-Mail an Benutzer schickt, wenn sich ein Objekt geändert hat.
Listing B.14. mail.py
##parameters=state_change
# the objects we need
object = state_change.object
mship = context.portal_membership
mhost = context.MailHost
administratorEmailAddress = context.email_from_address
# the message format, %s will be filled in from data
message = """
From: %s
To: %s
Subject: New item submitted for approval - %s
%s
URL: %s
"""
for user in mship.listMembers():
if "Reviewer" in mship.getMemberById(user.id).getRoles():
if user.email:
msg = message % (
administratorEmailAddress,
user.email,
object.TitleOrId(),
object.Description(),
object.absolute_url()
)
mhost.send(msg)
Kapitel 9
Die folgenden Code-Listings stammen aus Kapitel 9.
Externe Methode: importUsers
Listing B.15 zeigt eine externe Methode zum Importieren von Benutzern aus einer .csv-Datei im Dateisystem.
Listing B.15. importUsers
#!/usr/bin/python
#$Id: appB.rst,v 1.1 2005/09/08 12:41:33 dinu_gherman Exp $
#Copyright: ClearWind Consulting Ltd
#License: http://www.clearwind.ca/license
import csv
fileName = "/var/zope.zeo/Extensions/test.csv"
def importUsers(self):
reader = csv.reader(open(fileName, "r"))
pm = self.portal_membership
pr = self.portal_registration
pg = self.portal_groups
out = []
ignoreLine = 1
for row in reader:
# ignore blank lines
if not row: continue
if ignoreLine:
continue
ignoreLine = 0
# check we have exactly 4 items
assert len(row) == 4
id, name, email, groups = row
password = pr.generatePassword()
try:
pr.addMember(id = id,
password = password,
roles = ["Member",],
properties = {
'fullname': name,
'username': id,
'email': email,
}
)
for groupId in groups.split(','):
group = pg.getGroupById(groupId)
group.addMember(id)
out.append("Added user %s" % id)
except ValueError:
out.append("Skipped %s" % id)
return "\n".join(out)
Externe Methode: fixUsers
Listing B.16 zeigt ein Skript, das für alle Benutzer Eigenschaften von Epoz setzt.
Listing B.16. fixUsers
def fixUsers(self):
pm = self.portal_membership
members = pm.listMemberIds()
out = []
for member in members:
# now get the actual member
m = pm.getMemberById(member)
# get the editor property for that member
p = m.getProperty('wysiwyg_editor', None)
out.append("%s %s" % (p, member))
if p is not None and p != 'Epoz':
m.setMemberProperties({'wysiwyg_editor': 'Epoz',})
out.append("Changed property for %s" % member)
return "\n".join(out)
Externe Methode: getGroups
Listing B.17 zeigt ein Skript, das zuerst den Erzeuger eines Objekts holt. Dann holt es alle Benutzer, die in der gleichen Gruppe sind wie dieser Erzeuger.
Listing B.17. getGroups
##parameters=object=None
# object is the object to find all the members of the same group for
users = []
# get the creator
userName = object.Creator()
user = context.portal_membership.getMemberById(userName)
pg = context.portal_groups
# loop through the groups the user is in
for group in user.getGroups():
group = pg.getGroupById(group)
# loop through the users in each of those groups
for user in group.getGroupUsers():
if user not in users and user != userName:
users.append(user)
return users
Kapitel 11
Die folgenden Code-Listings stammen aus Kapitel 11.
Script (Python): scriptObjectCreation
Listing B.18 zeigt ein Skript, das einen Ordner mit einem Dokument darin erzeugt.
Listing B.18. scriptObjectCreation
##title=Create
##parameters=
# create with a random id
newId = context.generateUniqueId('Folder')
# create a object of type Folder
context.invokeFactory(id=newId, type_name='Folder')
newFolder = getattr(context, newId)
# create a new Document type
newFolder.invokeFactory(id='index.html', type_name='Document')
# get the new page
newPage = getattr(newFolder, 'index.html')
newPage.edit('html', '<p>This is the default page.</p>')
# return something back to the calling script
return "Done"
Page Template: getCatalogResults
Listing B.19 zeigt eine Beispielkatalogabfrage, die auf den REQUEST-Parametern eine Abfrage durchführt.
Listing B.19. getCatalogResults
##title=Get Catalog Results ##parameters= return context.portal_catalog.searchResults(REQUEST=context.REQUEST)
Page Template: testResults
Listing B.20 zeigt ein Beispielergebnis einer Katalogsuche.
Listing B.20. testResults
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US"
lang="en-US"
metal:use-macro="here/main_template/macros/master"
i18n:domain="plone">
<body>
<div metal:fill-slot="main">
<ul tal:define="results here/getCatalogResults">
<li tal:repeat="result results">
<a href=""
tal:attributes="href result/getURL"
tal:content="result/Title" />
<span tal:replace="result/Description" />
</li>
</ul>
</div>
</body>
</html>
Page Template: testForm
Listing B.21 zeigt ein Beispielformular, das die Seite testResults mit einer Dropdown-Liste aufruft, die auf einer Katalogabfrage basiert.
Listing B.21. testForm
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US"
lang="en-US"
metal:use-macro="here/main_template/macros/master"
i18n:domain="plone">
<body>
<div metal:fill-slot="main">
<p>Select a content type to search for</p>
<form method="post" action="testResults">
<select name="Type">
<option
tal:repeat="value python:here.portal_catalog.uniqueValuesFor('Type")"
tal: content="value" />
</select>
<br />
<input type="submit" class="context">
</form>
</div>
</body>
</html>
Kapitel 12
Die folgenden Code-Listings stammen aus Kapitel 12.
Beispielprodukt: PloneSilverCity
Dieses Produkt finden Sie im Kollektiv unter http://sf.net/projects/collective. Sie können dieses Produkt auch von der Website zu diesem Buch unter http://plone-book.agmweb.ca herunterladen.
Beispielprodukt: PloneStats
Dieses Produkt finden Sie im Kollektiv unter http://sf.net/projects/collective. Sie können dieses Produkt auch von der Website zu diesem Buch unter http://plone-book.agmweb.ca herunterladen.
Kapitel 13
Die folgenden Code-Listings stammen aus Kapitel 13.
Beispielprodukt: ArchExample
Dieses Produkt befindet sich im Archetypes-Release, aber Sie können eine Kopie auch von der Website zu diesem Buch unter http://plone-book.agmweb.ca herunterladen.
Page Template: email_widget.py
Listing B.22 zeigt ein eigenes Beispiel-Widget für Archetypes.
Listing B.22. email_widget.py
<!--
$Id: appB.rst,v 1.1 2005/09/08 12:41:33 dinu_gherman Exp $
Copyright: ClearWind Consulting Ltd
License: http://www.clearwind.ca/license
-->
<html xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal"
i18n:domain="plone">
<body>
<div metal:define-macro="edit">
<div metal:use-macro="here/widgets/string/macros/edit" />
</div>
<div metal:define-macro="search">
<div metal:use-macro="here/widgets/string/macros/search" />
</div>
<div class="field" metal:define-macro="view">
<metal:block define-slot="widget_label" />
<metal:block use-macro="here/widgets/field/macros/view">
<metal:block fill-slot="widget_view">
<a href="#" tal:attributes="href string:mailto:${accessor}"
tal:content="accessor">email</a>
</metal:block>
</metal:block>
</div>
</body>
</html>
Beispielprodukt: WorldExample
Das Listing für das Produkt WorldExample können Sie von der Website zu diesem Buch unter http://plone-book.agmweb.ca herunterladen.
Python-Modul: PersonSQL.py
Listing B.23 zeigt einen Archetypes-Inhaltstyp für eine Person, der die Daten in einer relationalen Datenbank speichert.
Listing B.23. PersonSQL.py
#!/usr/bin/python
#$Id: appB.rst,v 1.1 2005/09/08 12:41:33 dinu_gherman Exp $
#Copyright: ClearWind Consulting Ltd
#License: http://www.clearwind.ca/license
from Products.Archetypes.public import Schema
from Products.Archetypes.public import IntegerField, StringField
from Products.Archetypes.public import IntegerWidget, StringField
from Products.Archetypes.SQLStorage import PostgreSQLStorage
from config import PROJECTNAME
schema = BaseSchema + Schema((
IntegerField('age',
validators=(("isInt",)),
storage = SQLStorage(),
widget=IntegerWidget(label="Your age"),
),
StringField('email',
validators = ('isEmail',),
index = "TextIndex",
storage = SQLStorage(),
widget = StringWidget(label='Email',)
),
))
class PersonSQL(BaseContent):
"""Our person object"""
schema = schema
registerType(PersonSQL, PROJECTNAME)
Kapitel 14
Die folgenden Code-Listings stammen aus Kapitel 14.
Python-Modul: header.py
Listing B.24 zeigt ein Skript, das die Header für eine URL ausgibt.
Listing B.24. header.py
#!/usr/bin/python
import sys
from httplib import HTTP
from urlparse import urlparse
def getHeaders(url, method):
p = list(urlparse(url))
if not p[0]:
url = 'http://' + url
p = list(urlparse(url))
h = HTTP(p[1])
h.putrequest(method, p[2])
h.putheader('Accept-Encoding', 'gzip, deflate')
h.endheaders()
reply = h.getreply()
print "Status:", reply[0]
print "Status message:", reply[1]
hdrs = reply[2].headers
hdrs.sort()
for header in hdrs:
print header[:-1]
def usage():
print """Usage: headers.py URL [method]
URL - the URL to get headers for, http:// default
method - GET default
"""
sys.exit()
if __name__=='__main__':
if len(sys.argv) < 2: usage()
method = 'GET'
if len(sys.argv) > 2:
method = sys.argv[2]
getHeaders(sys.argv[1], method)
Script (Python): myCachingRules
Listing B.25 enthält eine angepasste Caching-Regel für einen Policy-Manager, nur um Ihnen mehr Optionen zu geben.
Listing B.25. myCachingRules
##parameters=content
# cache all files, images and anything
# thats published
if content.portal_type in ['File', 'Image']:
return 1
if content.review_state in ['published',]:
return 1
Externe Methode: Purge Cache
Listing B.26 zeigt ein Beispiel für ein Skript, das den Cache-Speicher löscht.
Listing B.26. Purge Cache
import urllib
import urlparse
import httplib
URLs = [
# enter the URLs you would like
# to purge here
'http://localhost:8080',
]
def purge(objectURL):
for url in URLs:
if not url:
continue
assert url[:4] == 'http', "No protocol specified"
url = urlparse.urljoin(url, objectURL)
parsed = urlparse.urlparse(url)
host = parsed[1]
path = parsed[2]
h = httplib.HTTP(host)
h.putrequest('PURGE', path)
h.endheaders()
errcode, errmsg, headers = h.getreply()
h.getfile.read()
if __name__ == '__main__':
print purge('/')
Andy McKay: Plone. Addison-Wesley 2005
Es wurde zuletzt von ctheune am 30.04.2006 14:15 aus der local Quelle via
/tmp/plonebook/PloneBook/de/ aktualisiert.