diff --git a/doc/gen.html b/doc/gen.html index a0e4c8b..705be4c 100644 --- a/doc/gen.html +++ b/doc/gen.html @@ -7,7 +7,7 @@

What is gen ?

-

gen is a code generator that allows you write web apps without having to face and understand the plumbery of a given web framework. gen protects you. Concentrate on functionalities that need to be implemented: gen will fight for you against the low-level twisted machineries and will let you evolve in your pure, elegant and minimalistic Python world.

+

gen is a code generator that allows you write web apps without having to face and understand the plumbery of a given web framework. Concentrate on functionalities that need to be implemented: gen protects you from the complexity of low-level twisted machineries and will let you evolve in your pure, elegant and minimalistic Python world.

Download and install

@@ -23,7 +23,7 @@ ./install.sh

-

Don't be afraid, you just installed a lot of lines of code, but we will only use a very small subset of it: absolutely no line from Plone, a tiny subset of Zope and the Python interpreter that was also included.


+

You just installed a lot of (deprecated) lines of code, but we will only use a very small subset of it: absolutely no line from Plone, a tiny subset of Zope and the Python interpreter that was also included.


Install Appy. Download it here, unzip it and install it in the Python interpreter previously mentioned.

@@ -83,12 +83,10 @@     companyDescription = String(format=String.XHTML, **p)
# ------------------------------------------------------------------------------

- -

This class is declared as root: for the moment, just remember that it gives it a special, first-class status. One line below, we define the roles that are allowed to create instances of this class. Anonymous and Manager are 2 standard Appy roles. The remaining lines define the attributes of every registration. Dict p is simply a shorthand for specifying the same (group of) attribute(s) to several methods.


-

Now that we have developed a complete webapp (yes, it is already over! Be patient, in a second iteration we will improve it), we need to plug it into the Zope instance.

+

Now that we have developed a complete webapp, we need to plug it into the Zope instance.

Plug the webapp into the Zope instance

@@ -150,7 +148,7 @@


-

Congratulations! Now, let's create a new registration by clicking on the "plus" icon. You should get this form:

+

Congratulations! Now, without logging in (as anonymous user), let's create a new registration by clicking on the "plus" icon. You should get this form:

...

@@ -164,9 +162,10 @@

In this example, I have entered an email with a wrong format (remember, a validator String.EMAIL was defined) and I entered no value for a mandatory field. multiplicity=(1,1) means: at least and at most one value is required.


-

Once validation succeeds, the registration is stored in the database and the user can visualize it:

+

Once validation succeeds, the registration is stored in the database and the user can visualize it.


- - +

You can also log in as admin (password: admin) to discover the standard screens that are available, in your app, for an administrator.

+ + diff --git a/doc/genCreatingAdvancedClasses.html b/doc/genCreatingAdvancedClasses.html index 7b4f4aa..f88cb6c 100644 --- a/doc/genCreatingAdvancedClasses.html +++ b/doc/genCreatingAdvancedClasses.html @@ -4,11 +4,13 @@ +
This section contains some deprecated information. Documentation is currently being rewritten and will soon be available.
+

Class inheritance

A gen-class may inherit from another gen-class. Suppose you work as director of human resources in a company and you need to measure employee productivity. Incredible but true: you are also a Python programmer. So you start developing a small application for managing employees. Although you are a director, you have reached a high level of wisdom (maybe due to the fact that you program in Python). So you know that there are two kinds of employees: those that work and those that don't. You decide to create 2 different classes to reflect this:

-

+

class Person:
  root = True
  title = String(show=False)
@@ -36,7 +38,7 @@

After a while, you become anxious about having 95% of the data in your database (=Person instances) serving absolutely no purpose. Logically, you decide to register the hair color for every non-worker. You realize that you need to change your model to be able to do this:

-

+

class Person:
  abstract = True
  title = String(show=False)
@@ -64,7 +66,7 @@

After a while, you become so excited about encoding hair colors that you would like to encode it even before encoding a parasite's name and first name. You have noticed that with gen, order of fields within the "edit" and "consult" views follows order of field declarations in the corresponding gen-classes; furthermore, fields defined in child classes appear after the fields defined in parent classes. Fortunately, the "move" parameter allows to change this default setting. Changing the Parasite class this way produces the desired result:

-

+

class Parasite(Person):
  root = True
  hairColor= String(move=-2)
@@ -76,7 +78,7 @@

When defining a gen-class, some method names are reserved. Until now, we have already encountered the method onEdit, like in this example:

-

+

class Person:
  abstract = True
  title = String(show=False)
@@ -90,7 +92,7 @@

Another special method is named validate. While the field-specific validators are used for validating field values individually, the validate method allows to perform "inter-field" validation when all individual field validations have succeeded. Consider this extension of class Parasite:

-

+

class Parasite(Person):
  root = True
  hairColor= String(move=-2)
@@ -162,7 +164,7 @@

What you need to know when using pod with gen is the exact pod context (ie the set of Python objects, variables, modules, etc) that is given by gen to your pod template. The table below presents all entries included in it.

- +
@@ -185,6 +187,7 @@
Entry DescriptionA string containing the absolute path of the folder where your gen-application resides on disk. If your gen-application is a folder hierarchy, projectFolder is the root folder of it.
+

In the previous examples, we have always rendered documents in Odt format. When generating ODT, gen and pod do not need any other piece of software. If you configure a template for generating documents in Adobe PDF (Pdf), Rich Text Format (Rtf) or Microsoft Word (Doc), you will need to run OpenOffice in server mode. Indeed, for producing any of these formats, pod will generate, from your POD template, an ODT file and will ask OpenOffice to convert it into PDF, DOC or RTF. Suppose you modify the ODT template named "Gossips" for producing documents in PDF instead of ODT (in the config, default flavour, tab "document generation", edit the template named "gossips" and choose "Pdf" as "Pod format"). If now you go back to the consult view for a parasite, the PDF icon will show up for the template "Gossips":

@@ -214,7 +217,7 @@

Let's consider the following example. It is an improved version of the Human Resources software developed by yourself several minutes ago (see above). I have added more fields in order to illustrate how to layout fields into pages and groups.

-

+

class Person:
  abstract = True
  pod = True
@@ -393,14 +396,14 @@

Let's illustrate it first by defining a custom Tool. We will use our ZopeComponent example. Suppose that the company that creates those components is organized into bunches of geeks. Every component is developed under the responsibility of a bunch. We first need a class for defining bunches:

-

+

class BunchOfGeek:
  description = String(format=String.TEXT)

Creating a custom tool is as simple as inheriting from class appy.gen.Tool:

-

+

class ZopeComponentTool(Tool):
  someUsefulConfigurationOption = String()
  bunchesOfGeeks = Ref(BunchOfGeek, multiplicity=(0,None), add=True,
@@ -412,7 +415,7 @@

Now please modify class ZopeComponent by adding a Ref attribute that will allow to assign the component to a given bunch:

-

+

  responsibleBunch = Ref(BunchOfGeek, multiplicity=(1,1), add=False,
    link=True, back=Ref(attribute='components'))

@@ -433,7 +436,7 @@

We will now transform our example in order to define bunches of geeks at the flavour level. First, delete from your tool the 3 bunches you have created. Then, move attribute bunchesOfGeeks from your custom tool to a new custom Flavour:

-

+

class ZopeComponentTool(Tool):
  someUsefulConfigurationOption = String()

@@ -452,7 +455,7 @@

As you may have noticed, the default Tool class defines only one page (the main page). Feel free to add pages to your custom tool. The default Flavour class defines the following pages; within your custom flavour, you may either add fields to those existing pages (like in the previous example) or add your own pages.

- +
@@ -474,6 +477,7 @@
Page name DescriptionConfiguration options related to the way your gen-application looks. Columns displayed in the dashboards are configured here for example.
+

When defining fields on custom tools and flavours, some parameters have no sense. This is the case for all parameters enabling "configurability", like editDefault, optional, etc: you can't meta-configure (=configure the configuration). If you try to use those parameters on fields defined on custom tools and flavour they will be ignored by gen.

@@ -495,7 +499,7 @@

Pragmatically, what you need to know is the following. For every Appy field defined in a gen-class, the corresponding wrapper class contains a Python property (more info here) that has the same name, for which a getter function is defined. Every time you write a thing like:

-

self.bunchesOfGeeks

+

self.bunchesOfGeeks

The getter function is triggered behind the scenes and queries the real Zope object for getting the expected result. After it, the function may potentialy adapt the result before giving it to you. In this example, every "Zope" bunch of geeks is wrapped and you get a list of BunchOfGeek wrappers. This way, you always manipulate wrappers and you may forget everything about the cruel Zope world.

@@ -515,7 +519,7 @@

Beyond getters and setters, Appy wrappers give you plenty of nice attributes and methods for manipulating your objects. The following table shows you available attributes (or Python properties).

- +
@@ -557,10 +561,11 @@
field name writable?The Python class (=gen-class) related to this class. Indeed, because the instances you manipulate in the code inherit from special wrappers, writing self.__class__ will give you a wrapper class. So write self.klass instead.
+

The following table shows you available methods.

- +
@@ -570,7 +575,7 @@
method name parameterscreate fieldName, **kwargs Creates a new object and links it to this one through Ref field having name fieldName. Remaining parameters **kwargs are used for initialising fields of the newly created object. Here is an example, inspired from the one described above. Suppose that, in every flavour, a bunch of geeks called "Escadron de la mort" must absolutely be present: it includes the best geeks that you use for debugging the most critical Zope components. So every time you create or edit a flavour, you need to ensure that this bunch is there. If not, you will create it automatically. Code for class ZopeComponentFlavour must evolve this way: -

+

class ZopeComponentFlavour(Flavour):
  anIntegerOption = Integer()
  bunchesOfGeeks = Ref(BunchOfGeek, multiplicity=(0,None), add=True,
@@ -590,6 +595,7 @@

Links the existing object obj to the current one through Ref field fieldName. Already linked objects are not unlinked. This method is used by method create but may also be used independently.
+

For setting Ref fields, please use the create and link methods instead of the predefined setters which may not work as expected.

@@ -597,7 +603,7 @@

Tool and Flavour objects, be they customized or not, adopt the same "wrapper" mechanism as described above. In this section, we will simply explain how to get/set programmatically the predefined fields that gen generates automatically on tools and flavours. The following table presents the names of the predefined attributes defined on any Tool:

- +
@@ -627,10 +633,11 @@
field name descriptionThe boolean indicating if the field for entering comments when triggering a worflow transition must be shown or not.
+

For flavours, things are a little more complex. Imagine we have, throughout our gen-application, 25 fields parameterized with editDefault=True in 6 classes. The corresponding attributes that hold the default values in the flavours have an ugly name that includes the full package name of the class. Instead of forcing you to remember this obscure naming convention, a nice method defined on the Flavour class allows to retrieve this attribute name:

- +
@@ -669,10 +676,11 @@
method name parameters
+

Besides flavour attributes having oscure names, some attributes have a normal name:

- +
@@ -684,6 +692,7 @@
field name writable?The flavour number.
+

When choosing field and method names for your gen-classes, try to avoid using names corresponding to fields or methods from base Appy classes (wrappers, tool, flavours).

@@ -691,7 +700,7 @@

When (re-)installing your gen-application, you may want to initialize it with some data or configuration options. You do this by specifying a special action named install on your customized tool, like in the example below (the ZopeComponent application).

-

+

class ZopeComponentTool(Tool):
  someUsefulConfigurationOption = String()
  def onInstall(self):
@@ -715,7 +724,7 @@

gen-applications benefit from an automated support for i18n. How does it work? First of all, you need to declare what language(s) need to be supported in your gen-application. This is done through the creation, anywhere in your gen-application, of an instance of class appy.gen.Config:

-

+

from appy.gen import Config
c = Config()
c.languages = ('en', 'fr')
@@ -725,7 +734,7 @@

Every time you (re-)generate your gen-application, i18n files are created or updated in the corresponding generated Plone product. With the above settings, gen will generate the following files, in [yourZopeInstance]/Products/[yourApplication]/i18n (the product here is named ZopeComponent):

-

+

ZopeComponent.pot
ZopeComponent-en.po
ZopeComponent-fr.po
@@ -735,7 +744,7 @@

The format of these files is quite standard in the i18n world. Le'ts take a look to the beginning of ZopeComponent.pot:

-

+

msgid ""
msgstr ""
"Project-Id-Version: ZopeComponent\n"
@@ -754,7 +763,7 @@

After this header, you will find a list of labels. In the pot file, every label is defined like this one:

-

+

#. Default: "Funeral date"
msgid "ZopeComponent_ZopeComponent_funeralDate"
msgstr ""
@@ -768,7 +777,7 @@

Now, you need to know the meaning of every label gen creates and maintains in the po(t) file(s), and at what place(s) they are used within the generated edit and consult views (or in the dashboards, etc). The following table explains this. Dynamic parts used in labels (like [className]) are explained in yet another table below. For every label, the default value is specified. For the majority of labels, gen proposes a "nice" default value. For example, field responsibleBunch will have default value Responsible bunch; class BunchOfGeeks will have default value Bunch of geeks. The "nice" algorithm tries simply to recognize camel-cased words and separates them.

- +
@@ -949,10 +958,11 @@
label "pattern" usage"Please enter a valid alphanumeric value."
+

As already mentioned, in the table below, some label "parts" are explained.

- +
@@ -974,12 +984,13 @@
label part descriptionRefers to the name of an Appy field, declared as a static attribute in a gen-class. For example, attribute responsibleBunch of class ZopeComponent will have fieldName "responsibleBunch".
+

Creating and using new i18n labels

Although gen tries to manage automatically the whole i18n thing, in some cases you may need to create and use specific labels. You create new labels by adding them "by hand" in the pot file. For example, edit ZopeComponent.pot and add the following label:

-

+

#. Default: "Please be honest and do not pretend there is any kind of simplicity in your Zope 3 component."
msgid "zope_3_is_not_simple"
msgstr "" @@ -989,7 +1000,7 @@

In the following example, we use the new label for producing a translated validation message on a given field.

-

+

class ZopeComponent:   ...
  def validateDescription(self, value):
@@ -1005,7 +1016,7 @@

Now imagine you need to change the default value for this label in ZopeComponent.pot. It means that your translations in ZopeComponent-fr.po and ZopeComponent-en.po need a potential revision. Edit the default value and re-generate your product. The labels in the po files will be flagged as "fuzzy":

-

+

#. Default: "Please be honest and do not pretend there is any kind of simplicity in your Zope 3 component. I changed the default value."
#, fuzzy
msgid "zope_3_is_not_simple"
@@ -1018,7 +1029,7 @@

It is possible to make your gen pages more dynamic by defining relationships between fields belonging to the same page. For example, by selecting a given item in a list (on a master field), another (slave) field may become visible. This is achieved by using parameters master and masterValue. Let's try it on an example. Remember that, in our HR system, we have added the ability to associate a "parasite index" to a parasite. Suppose that in some cases, the parasite is so weird that you are unable to give him a parasite index. Among the values for field parasiteIndex, we add the value unquantifiable; but in this case, you must give, in another field, details about why you can't quantify its parasiteness:

-

+

class Parasite(Person):
  ...
  parasiteIndex = String(validator=['low', 'medium', 'high', 'unquantifiable'],
@@ -1060,7 +1071,7 @@

As already mentioned, a series of configuration options for your gen-application may be defined in an instance of class appy.gen.Config. There must be only one such instance by gen-application. The following table describes the available options defined as Config attributes.

- +
@@ -1082,10 +1093,11 @@
Attribute Default valueIf True, this flag will produce a minimalist Plone, where some user interface elements, like actions, portlets or other stuff less relevant for building web applications, are removed or hidden. Using this produces effects on your whole Plone site! This can be interesting on development machines: less visual gadgets make geeks more productive.
+

Here is an example of a Config instance:

-

+

from appy.gen import Config
c = Config()
c.languages = ('en', 'fr')
diff --git a/doc/genCreatingBasicClasses.html b/doc/genCreatingBasicClasses.html index 06b3e75..d0c88e8 100644 --- a/doc/genCreatingBasicClasses.html +++ b/doc/genCreatingBasicClasses.html @@ -4,6 +4,8 @@ +

This section contains some deprecated information. Documentation is currently being rewritten and will soon be available.
+

Gen-applications

A gen-application is a simple Python module (a single Python file) or package (a hierarchy of folders where every (sub-)folder contains a file named __init__.py). In the main gen presentation page, we've created a simple application in the form of a Python module, we've run it by installing Zope and Plone and by generating a Plone product. Working with a Python package instead of a Python module is quite easy: instead of creating MyModule.py in [myZopeInstance]/lib/python you simply create a folder "MyPackage" at the same place.

@@ -16,7 +18,7 @@

The code below shows an example of a gen-class.

-

+

from appy.gen import *
class A:
  at1 = String()
@@ -28,9 +30,9 @@     print self.at5 + str(self.at2)

-

String, Integer, Float and Date all inherit from appy.gen.Type. You may still define standard Python attributes like at5 and methods like sayHello. The list of basic types provided by gen is shown below.

+


String, Integer, Float and Date all inherit from appy.gen.Type. You may still define standard Python attributes like at5 and methods like sayHello. The list of basic types provided by gen is shown below.

- +
@@ -68,9 +70,10 @@
Integer  Holds an integer value.Holds nothing; the field represents a button or icon that triggers a function specified as a Python method.
+

When defining instances of those types, you will typically give some parameters to it. Below is the list of parameters that are common to all types. In the next sections, we will see into more detail every type with its specificities.

- +
@@ -198,7 +201,7 @@

Integers and floats have no additional parameters. In this section, we will simply illustrate, on Integers and Floats, some parameters defined in the previous section. Let's consider the following class:

-

from appy.gen import *
+

from appy.gen import *
class Zzz:
  root = True
  def show_f1(self): return True
@@ -236,9 +239,9 @@

Strings

-

Strings have an additional attribute named format which may take the following values:

+

Strings have an additional attribute named format which may take the following values

-
parameter default value
+
@@ -268,10 +271,11 @@
value default?
+

With Strings, adequate use of arguments validator and multiplicity may produce more widgets and/or behaviours. Consider the following class:

-

+

class SeveralStrings:
  root=True
  anEmail = String(validator=String.EMAIL)
@@ -311,7 +315,7 @@

Dates have an additional attribute named format which may take the following values:

- +
@@ -371,7 +375,7 @@

The corresponding gen model looks like this:

-

+

class Order:
  description = String(format=String.TEXT)

@@ -415,7 +419,7 @@

Besides the "add" functionality, association ends may also provide the "link" functionality. This produces another widget that allows you to link an object to existing ones instead of creating + linking them. Suppose you extend you model with the concept of Product: an order may now specify one or several products. The model would include the new Product class and the Order class would get an additional Ref field:

-

+

class Product:
  root = True
  description = String(format=String.TEXT)
@@ -452,7 +456,7 @@

For references, 2 more parameters allow to customize the way they are rendered: the boolean showHeaders and shownInfo. Let's consider again the consult view for a client (5 pictures above: gold client "Gaetan Delannay"). Beyond order's titles, I would like to display their description, too, in another column. But If I have several columns, it would be nice to get some columns headers. You may achieve the desired result by changing the definition of the field orders this way:

-

+

  orders = Ref(Order, add=True, link=False, multiplicity=(0,None),
    back=Ref(attribute='client'), showHeaders=True,
    shownInfo=('description',))
@@ -476,7 +480,7 @@

When using shownInfo, you may specify any field name, including Ref fields. If you specify shownInfo=('description', 'products') for the field orders of class Client and modify rendering of field products from class Order this way:

-

+

class Order:
  description = String(format=String.TEXT)
  products = Ref(Product, add=False, link=True, multiplicity=(1,None),
@@ -498,7 +502,7 @@

You may want to filter only some products instead of gathering all defined products. The select parameter may be used for this. Here is an example:

-

+

class Order:
  description = String(format=String.TEXT)
  def filterProducts(self, allProducts):
@@ -527,7 +531,7 @@

Let's try it on our example. Suppose we want to produce nice references for orders, based on some random number (yes, it would have been better to use some incremental number: it it really easy to do this with gen, but you need to know how to customize the configuration panel, which is explained later). We need to define a field number that will hold the order number and will be invisible. Then, we will define a Computed field named reference that will produce the reference based on some prefix and the order number. Class Order need to be updated like this:

-

+

class Order:
  ...
  number = Float(show=False)
@@ -551,7 +555,7 @@

Like any other field, Computed fields may appear on Ref fields or on dashboards. For example, if we change the definition of Ref field orders on class Client this way:

-

+

class Client:
  ...
  orders = Ref(Order, add=True, link=False, multiplicity=(0,None),
@@ -569,7 +573,7 @@

Actions are special fields that allow to trigger functions. For the moment, they are represented as buttons and are shown only on consult views (not on edit views). Let's take an example. Suppose we modify class Product this way:

-

+

class Product:
  root = True
  description = String(format=String.TEXT)
@@ -623,7 +627,7 @@

This screenshot shows the ZMI (Zope Management Interface) available at http://localhost:8080/manage. You see that within the Appy object (which is a Plone site) you have, among some predefined Plone objects like MailHost or Members, 2 folders named ZopeComponent and Zzz. Each of these 2 folders correspond to a gen-application that has the same name. Those folders are an Appy adaptation of the Plone standard content type named "Large Plone Folder", which is used for storing a large number of objects. If you click on this folder you will see its content (=all objects created through the corresponding gen-application). For several reasons, you may want to put more structure among this folder. Firstly, if you reach a large number of objects, it could lead to performance problems. Secondly, if you use the standard Plone "navigation" portlet, you will see in it all your objects in a single long and unstructured list under a single folder entry named according to your application. The solution is to tell gen that some classes are "folderish". You simply tell this by specifying folder=True on your class. Suppose you do this on class Client:

-

+

class Client:
  root = True
  folder = True
diff --git a/doc/genSecurityAndWorkflows.html b/doc/genSecurityAndWorkflows.html index 1c4dc24..43fd8db 100644 --- a/doc/genSecurityAndWorkflows.html +++ b/doc/genSecurityAndWorkflows.html @@ -4,6 +4,9 @@ + +

This section contains some deprecated information. Documentation is currently being rewritten and will soon be available.
+

The principles

The security model behinds gen-applications is similar to what Zope and Plone offer; simply, gen tries to simplify the way to declare and manage security within your applications. According to this model, users are put into groups; groups have some roles; roles are granted basic permissions on objects (create, read, write, delete, etc). Permissions-to-roles mappings may vary according to the state of objects.

@@ -18,7 +21,7 @@

Now, go to "Users and Groups Administration" (still in "Site setup") and add the following users using the button "add new user" (do not check "Send a mail with the password" and enter a dummy email):

-
value default?
+
@@ -32,12 +35,13 @@
User Name Passwordludovic
+

Now, we need groups. Guess what? We will not create groups. Why? Because gen will generate groups automatically for you!

Now that we have users and groups, it is time to create roles. Guess what? We will not do it. Why? Because it is simply not needed. gen will scan your code and find every role you mention and will create them automatically at the Zope/Plone level if they do not exist yet. We will use the following roles:

- +
@@ -55,10 +59,11 @@
role name descriptionZope/Python developer
+

gen will create one group for every role defined in your application; the group will be granted only the corresponding role. Note that we will probably not use the role ZDeveloper. Indeed, developers work. They will probably not use a management tool. Now, let's tackle permissions. Again, it is not needed to create permissions (at least now): gen provides the following default permissions:

- +
@@ -85,6 +90,7 @@
name corresponding code objectPermission to delete an object
+

All the security ingredients are now ready (users, groups, roles and permissions): we will now see how to use them to define security on a gen-application.

@@ -92,7 +98,7 @@

Permission to create objects is done at 2 levels. First, you may define a global list of roles that will, by default, be allowed to create any object of any class in your gen-application. In our company, ZLeaders are responsible for creating Zope components. You declare this global list in attribute defaultCreators of your appy.gen.Config instance introduced while presenting i18n:

-

+

c = Config()
c.languages = ('en', 'fr')
c.defaultCreators += ['ZLeader']
@@ -102,7 +108,7 @@

Defining default creator roles for every class of your application may not be subtle enough. This is why gen allows you do it per class, with static attribute creators. For example, you may use this attribute on class ZopeComponent:

-

+

class ZopeComponent:
  ...
  creators = c.defaultCreators + ['ZLeader']
@@ -118,7 +124,7 @@

So let's define a simple workflow for our class ZopeComponent. Until now our class looks like this:

-

+

class ZopeComponent:
  root = True
  def showDate(self):
@@ -141,7 +147,7 @@

Field status seems to be a kind of workflow embryo. So we will remove it and create a workflow whose states will look like values of this field:

-

+

class ZopeComponentWorkflow:
  # Roles
  zManager = 'ZManager'
@@ -230,7 +236,7 @@

We have now finished our first security tour. An important remark is that we have granted roles "globally" to groups: any user of the group has always the globally granted role, under all circumstances, on any related object in your gen-application. In our example, Ludovic and all potential other project leaders have the right to edit all created components. This may not be the desired behaviour. Maybe would you prefer any project leader to be able to edit his own components but not components created by other project leaders. This is where "local roles" come into play. A local role is a role that a user or group has, but only on a given object. The default Plone role "Owner" is an example of local role: this is not a role that you grant "globally" to a user or group (like the ones shown in tab "groups" or "users" of "Site setup -> Users and Groups Administration"); this is a role that is granted on an object to the user that created it. You may of course reference local roles within gen-workflows. For example, if you want to restrict component modifications to Owners and Managers when the component is created, you may modify the workflow state created like this:

-

created = State({r:leaderM, w:('Owner', 'Manager'), d:leaderM}, initial=True) +

created = State({r:leaderM, w:('Owner', 'Manager'), d:leaderM}, initial=True)

Re-generate your product and re-install it. The Plone procedure for re-installing a product updates the workflow definition but does not update the permissions-to-roles mappings defined on existing objects. In order to synchronize them with the new workflow definition, you need to go, through the ZMI, in object "portal_workflow" within your Plone site. At the bottom of the page, you will find a button "Update security settings". Click on it. This may take a long time if you have a large number of objects in your database. In future gen releases, you will be able to re-install your product directly from your tool. This specific procedure will ask you if you want to "Update workflow settings on existing objects" or not.

@@ -247,7 +253,7 @@

Until now, we have seen that, as transition condition, you can specify role(s) (one, as a string, or a tuple of roles). You can also specify Python method(s) the same way, and even mix roles and Python methods. Specified Python method(s) must belong to your gen-workflow (or one of its parents, yes, we will soon talk about workflow inheritance!). With such methods, more complex conditions may be defined. Let's show it by refining our previous example. Suppose that components can be validated only if a funeral date (which is not a mandatory field) has been specified. Transition validate need to evolve:

-

+

class ZopeComponentWorkflow:
  ...
  def funeralOk(self, obj): return obj.funeralDate
@@ -259,7 +265,7 @@

One may also define action(s) (as Python method(s)) that are executed after any transition has been triggered. Let's suppose we want to reinitialise the component description when we start its development. This is completely silly of course. But I like to force you doing silly things, it is a pleasant feeling. So let's update transition startDevelopment:

-

+

class ZopeComponentWorkflow:
  ...
  def updateDescription(self, obj):
@@ -277,7 +283,7 @@

Let's try it on our class ZopeComponent. Suppose we need a specific write permission on field funeralDate and a specific read permission on field responsibleBunch:

-

+

class ZopeComponent:
  ...
  funeralDate = Date(optional=True, specificWritePermission=True)
@@ -289,7 +295,7 @@

Now, in our workflow, for every state, we need to update the permissions-to-roles mapping by specifying the roles that will be granted those 2 new permissions. But first, we need a way to designate those permissions. This is done by using classes appy.gen.ReadPermission and appy.gen.WritePermission like this:

-

+

class ZopeComponentWorkflow:
  # Specific permissions
  wf = WritePermission('ZopeComponent.funeralDate')
@@ -302,7 +308,7 @@

Now let's update every state definition by integrating those 2 permissions in the permissions-to-roles mappings:

-

+

class ZopeComponentWorkflow:
  ...
  # States
@@ -331,7 +337,7 @@

With gen, workflows are Python classes. This allows us to benefit from class inheritance and apply it to workflows. Our company that creates Zope components is now used to heavy technologies. They got a business revelation: some managers discovered that COBOL and Zope 3 had a lot in common on both philosophical and technical points of view. So they decided to begin creating components in COBOL. They were so excited about it that they needed to update their management software as quickly as possible. So a new class was added for registering information about COBOL components. The associated workflow was almost similar to the existing ZopeComponentWorkflow; a new workflow inheriting from it was created:

-

+

class CobolComponentWorkflow(ZopeComponentWorkflow):
  p = ZopeComponentWorkflow # Shortcut to workflow parent
  # An additional state
@@ -359,7 +365,7 @@

As usual, for every workflow state and transition, i18n labels have been automatically generated (in the plone domain), together with a "nice" default value. The format of those labels is defined here. There is still a small problem with the CobolComponentWorkflow: the transition for finishing the work is called cancelDevelopment. I am too lazy for creating another transition, so I will simply modify here the translation of this transition in the corresponding i18n file (=ZopeComponent-plone-en.po in this case):

-

+

#. Default: "Cancel development"
msgid "zopecomponent_cobolcomponentworkflow_cancelDevelopment"
msgstr "Finish"
diff --git a/doc/img/ErrorExpression.png b/doc/img/ErrorExpression.png index 0456c72..3ac1944 100644 Binary files a/doc/img/ErrorExpression.png and b/doc/img/ErrorExpression.png differ diff --git a/doc/img/ErrorExpression.res.png b/doc/img/ErrorExpression.res.png index 7a51b10..e3bcf79 100644 Binary files a/doc/img/ErrorExpression.res.png and b/doc/img/ErrorExpression.res.png differ diff --git a/doc/img/ErrorForParsetime.png b/doc/img/ErrorForParsetime.png index 77664b2..57a454b 100644 Binary files a/doc/img/ErrorForParsetime.png and b/doc/img/ErrorForParsetime.png differ diff --git a/doc/img/ErrorForParsetime.res.png b/doc/img/ErrorForParsetime.res.png index ac25424..36dac10 100644 Binary files a/doc/img/ErrorForParsetime.res.png and b/doc/img/ErrorForParsetime.res.png differ diff --git a/doc/img/ErrorForRuntime.png b/doc/img/ErrorForRuntime.png index c6ced14..6c1a9e6 100644 Binary files a/doc/img/ErrorForRuntime.png and b/doc/img/ErrorForRuntime.png differ diff --git a/doc/img/ErrorForRuntime.res.png b/doc/img/ErrorForRuntime.res.png index c9e5519..ddf1a42 100644 Binary files a/doc/img/ErrorForRuntime.res.png and b/doc/img/ErrorForRuntime.res.png differ diff --git a/doc/img/ErrorIf.png b/doc/img/ErrorIf.png index ada79f4..a54269c 100644 Binary files a/doc/img/ErrorIf.png and b/doc/img/ErrorIf.png differ diff --git a/doc/img/ErrorIf.res.png b/doc/img/ErrorIf.res.png index 13acb82..dc834d7 100644 Binary files a/doc/img/ErrorIf.res.png and b/doc/img/ErrorIf.res.png differ diff --git a/doc/img/ForCellNotEnough.png b/doc/img/ForCellNotEnough.png index 6b4128f..f835e71 100644 Binary files a/doc/img/ForCellNotEnough.png and b/doc/img/ForCellNotEnough.png differ diff --git a/doc/img/ForCellNotEnough.res.png b/doc/img/ForCellNotEnough.res.png index 0f81a76..0bb72ad 100644 Binary files a/doc/img/ForCellNotEnough.res.png and b/doc/img/ForCellNotEnough.res.png differ diff --git a/doc/img/ForCellTooMuch2.png b/doc/img/ForCellTooMuch2.png index 9144722..1c0615d 100644 Binary files a/doc/img/ForCellTooMuch2.png and b/doc/img/ForCellTooMuch2.png differ diff --git a/doc/img/ForCellTooMuch2.res.png b/doc/img/ForCellTooMuch2.res.png index c81d7ce..d6f4862 100644 Binary files a/doc/img/ForCellTooMuch2.res.png and b/doc/img/ForCellTooMuch2.res.png differ diff --git a/doc/img/ForTableMinus.png b/doc/img/ForTableMinus.png index dde05ed..b4a879f 100644 Binary files a/doc/img/ForTableMinus.png and b/doc/img/ForTableMinus.png differ diff --git a/doc/img/ForTableMinus.res.png b/doc/img/ForTableMinus.res.png index 3ce83d6..de486b8 100644 Binary files a/doc/img/ForTableMinus.res.png and b/doc/img/ForTableMinus.res.png differ diff --git a/doc/img/ForTableMinusError.png b/doc/img/ForTableMinusError.png index 2fd1d9d..d7d7929 100644 Binary files a/doc/img/ForTableMinusError.png and b/doc/img/ForTableMinusError.png differ diff --git a/doc/img/ForTableMinusError.res.png b/doc/img/ForTableMinusError.res.png index 2fa8339..09dfac2 100644 Binary files a/doc/img/ForTableMinusError.res.png and b/doc/img/ForTableMinusError.res.png differ diff --git a/doc/img/IfAndFors1.png b/doc/img/IfAndFors1.png index 0127422..0119bdb 100644 Binary files a/doc/img/IfAndFors1.png and b/doc/img/IfAndFors1.png differ diff --git a/doc/img/IfAndFors1.res.png b/doc/img/IfAndFors1.res.png index 7d40016..8045147 100644 Binary files a/doc/img/IfAndFors1.res.png and b/doc/img/IfAndFors1.res.png differ diff --git a/doc/img/IfExpression.png b/doc/img/IfExpression.png index eb27ce4..dad1027 100644 Binary files a/doc/img/IfExpression.png and b/doc/img/IfExpression.png differ diff --git a/doc/img/OnlyExpressions.png b/doc/img/OnlyExpressions.png index 9c3a82f..c2aba31 100644 Binary files a/doc/img/OnlyExpressions.png and b/doc/img/OnlyExpressions.png differ diff --git a/doc/img/OnlyExpressions.res.png b/doc/img/OnlyExpressions.res.png index dafc4d2..1fd62e9 100644 Binary files a/doc/img/OnlyExpressions.res.png and b/doc/img/OnlyExpressions.res.png differ diff --git a/doc/img/SimpleTest.png b/doc/img/SimpleTest.png index d41c9a6..7f9d9b2 100644 Binary files a/doc/img/SimpleTest.png and b/doc/img/SimpleTest.png differ diff --git a/doc/img/SimpleTest.res.png b/doc/img/SimpleTest.res.png index d675807..ecb50a8 100644 Binary files a/doc/img/SimpleTest.res.png and b/doc/img/SimpleTest.res.png differ diff --git a/doc/img/dates1.png b/doc/img/dates1.png index cb9007f..913ca64 100644 Binary files a/doc/img/dates1.png and b/doc/img/dates1.png differ diff --git a/doc/img/dates2.png b/doc/img/dates2.png index 96d186d..a93b6fd 100644 Binary files a/doc/img/dates2.png and b/doc/img/dates2.png differ diff --git a/doc/img/dates3.png b/doc/img/dates3.png index 7a49506..dad9cb3 100644 Binary files a/doc/img/dates3.png and b/doc/img/dates3.png differ diff --git a/doc/img/dates4.png b/doc/img/dates4.png index 49802ac..c7f9de0 100644 Binary files a/doc/img/dates4.png and b/doc/img/dates4.png differ diff --git a/doc/img/documentFunction1.png b/doc/img/documentFunction1.png index 50c1e98..fb68302 100644 Binary files a/doc/img/documentFunction1.png and b/doc/img/documentFunction1.png differ diff --git a/doc/img/documentFunction2.png b/doc/img/documentFunction2.png index 570588c..094c9ad 100644 Binary files a/doc/img/documentFunction2.png and b/doc/img/documentFunction2.png differ diff --git a/doc/img/documentFunction3.png b/doc/img/documentFunction3.png index 1a63522..3a9b652 100644 Binary files a/doc/img/documentFunction3.png and b/doc/img/documentFunction3.png differ diff --git a/doc/img/strings1.png b/doc/img/strings1.png index ca43156..11e6cd6 100644 Binary files a/doc/img/strings1.png and b/doc/img/strings1.png differ diff --git a/doc/img/strings2.png b/doc/img/strings2.png index 82b2cdb..20a51c7 100644 Binary files a/doc/img/strings2.png and b/doc/img/strings2.png differ diff --git a/doc/img/strings3.png b/doc/img/strings3.png index a872532..9564e75 100644 Binary files a/doc/img/strings3.png and b/doc/img/strings3.png differ diff --git a/doc/img/strings4.png b/doc/img/strings4.png index 68291ce..f794f86 100644 Binary files a/doc/img/strings4.png and b/doc/img/strings4.png differ diff --git a/doc/img/strings5.png b/doc/img/strings5.png index 8c821d0..c04c937 100644 Binary files a/doc/img/strings5.png and b/doc/img/strings5.png differ diff --git a/doc/img/strings6.png b/doc/img/strings6.png index 738ff82..522e150 100644 Binary files a/doc/img/strings6.png and b/doc/img/strings6.png differ diff --git a/doc/img/xhtmlResult.png b/doc/img/xhtmlResult.png index 1733429..5f397f1 100644 Binary files a/doc/img/xhtmlResult.png and b/doc/img/xhtmlResult.png differ diff --git a/doc/pod.html b/doc/pod.html index 6006932..1e905c1 100644 --- a/doc/pod.html +++ b/doc/pod.html @@ -6,34 +6,40 @@

What is pod ?

-

pod (python open document) is a library that allows to easily generate documents whose content is dynamic. The principle is simple: you create an ODF (Open Document Format) text document (with OpenOffice Writer 2.0 or higher for example), you insert some Python code at some places inside it, and from any program written in Python, you can call pod with, as input, the OpenDocument file and a bunch of Python objects. pod generates another ODF text document (ODT) that contains the desired result. If you prefer to get the result in another format, pod can call OpenOffice in server mode to generate the result in PDF, DOC, RTF or TXT format.

+

pod (python open document) is a library that allows to easily generate documents whose content is dynamic. The principle is simple: you create an ODF (Open Document Format) text document (with LibreOffice Writer for example), you insert some Python code at some places inside it, and from any program written in Python, you can call pod with, as input, the OpenDocument file and a bunch of Python objects. pod generates another ODF text document (ODT) that contains the desired result. If you prefer to get the result in another format, pod can call LibreOffice in server mode to generate the result in PDF, DOC, RTF or TXT format.

Getting started with pod

-

First, create a pod template, like the one besides this text. A pod template is an ODT document where:

+

First, create a pod template, like the one below.

+ +


+ +

A pod template is an ODT document where:

-

In this template, I wrote the Python expression IWillTellYouWhatInAMoment while being in "track changes" mode (with OpenOffice, in the Edit menu, choose Modifications->Record). I've also added 2 notes (with OpenOffice, in the Insert menu, choose Note). The first (before "It just claims...") contains the statement do text for i in range(3). The second contains do text if (not beingPaidForIt). Click here if you want to learn more about creating pod templates.

+


In this template, I wrote the Python expression commercial in a conditional field. With LibreOffice, click on [Ctrl]-F2 to create a field. Choose field type 'conditional text', write true as condition, write commercial in the then expression and write nothing in the else expression. Another expression ("i") in the next line has been defined similarly. 2 notes were also added. With LibreOffice, in the Insert menu, choose Note). Click here if you want to learn more about creating pod templates.

-

Here is the code for calling pod for generating a result in ODT format.

+


Here is the code for calling pod for generating a result in ODT format.

-

- 01  from appy.pod.renderer import Renderer
- 02  
- 03  IWillTellYouWhatInAMoment = 'return'
- 04  beingPaidForIt = True
- 05  renderer = Renderer('SimpleTest.odt', globals(), 'result.odt')
- 06  renderer.run()

+

+ 01  from appy.pod.renderer import Renderer
+ 02  
+ 03  commercial = 'creative'
+ 04  beingPaidForIt = True
+ 05  renderer = Renderer('SimpleTest.odt', globals(), 'result.odt')
+ 06  renderer.run()

-

First we need to import the Renderer class. Then we define some Python variables. We must then create an instance of the Renderer (line 5), with, as parameters, the name of the pod template (we assume here that the pod template shown above is called SimpleTest.odt and lies in the current folder), a dictionary of named Python objects (here we simply take the global environment) and the name of the result file. The script will generate it, with, as content, what is shown in the image below.

+

First we need to import class Renderer. Then we define some Python variables. We must then create an instance of the Renderer (line 5), with, as parameters, the name of the pod template (we assume here that the pod template shown above is called SimpleTest.odt and lies in the current folder), a dictionary of named Python objects (here we simply take the global environment) and the name of the result file. The script will generate it, with, as content, what is shown in the image below.

-

The second line of the template is repeated 3 times. It is the effect of the for loop in the first note. All text insertions in "track changes" mode were replaced by the results of evaluating them as Python expressions, thanks to the context given to the Renderer as second parameter of its constructor. Note that within a loop, a new name (the iterator variable, i in this case) is added in the context and can be used within the document part that is impacted by the for loop. The last line of the template was not rendered because the condition of the second note evaluated to False.

+


-

Click here if you want to learn more about rendering pod templates.

+


The second line of the template is repeated 3 times. It is the effect of the for loop in the first note. Content of every field was replaced by the result of evaluating it as a Python expression, thanks to the context given to the Renderer as second parameter of its constructor. Note that within a loop, a new name (the iterator variable, i in this case) is added in the context and can be used within the document part that is impacted by the for loop. The last line of the template was not rendered because the condition of the second note evaluated to False.

+ +


Click here if you want to learn more about rendering pod templates.

diff --git a/doc/podRenderingTemplates.html b/doc/podRenderingTemplates.html index 70358ae..6390e54 100644 --- a/doc/podRenderingTemplates.html +++ b/doc/podRenderingTemplates.html @@ -9,38 +9,88 @@

In order to render a pod template, the first thing to do is to create a renderer (create a appy.pod.Renderer instance). The constructor for this class looks like this:

- def __init__(self, template, context, result, pythonWithUnoPath=None, ooPort=2002, stylesMapping={}, forceOoCall=False):
-   '''This Python Open Document Renderer (PodRenderer) loads a document
-      template (p_template) which is a OpenDocument file with some elements
-      written in Python. Based on this template and some Python objects
-      defined in p_context, the renderer generates an OpenDocument file
-      (p_result) that instantiates the p_template and fills it with objects
-      from the p_context. If p_result does not end with .odt, the Renderer
-      will call OpenOffice to perform a conversion. If p_forceOoCall is True,
-      even if p_result ends with .odt, OpenOffice will be called, not for
-      performing a conversion, but for updating some elements like indexes
-      (table of contents, etc) and sections containing links to external
-      files (which is the case, for example, if you use the default function
-      "document"). If the Python interpreter
-      which runs the current script is not UNO-enabled, this script will
-      run, in another process, a UNO-enabled Python interpreter (whose path
-      is p_pythonWithUnoPath) which will call OpenOffice. In both cases, we
-      will try to connect to OpenOffice in server mode on port p_ooPort.
-      If you plan to make "XHTML to OpenDocument" conversions, you may specify
-      a styles mapping in p_stylesMapping.''' + class Renderer:
+     def __init__(self, template, context, result, pythonWithUnoPath=None,
+                  + ooPort=2002, stylesMapping={}, forceOoCall=False,
+                  + finalizeFunction=None, overwriteExisting=False,
+                  + imageResolver=None):
+         '''This Python Open Document Renderer (PodRenderer) loads a document
+         template (p_template) which is an ODT file with some elements
+         written in Python. Based on this template and some Python objects
+         defined in p_context, the renderer generates an ODT file
+         (p_result) that instantiates the p_template and fills it with objects
+         from the p_context.
+
+         - If p_result does not end with .odt, the Renderer
+            + will call LibreOffice to perform a conversion. If p_forceOoCall is
+            + True, even if p_result ends with .odt, LibreOffice will be called, not
+            + for performing a conversion, but for updating some elements like
+            + indexes (table of contents, etc) and sections containing links to
+            + external files (which is the case, for example, if you use the
+            + default function "document").
+
+         - If the Python interpreter which runs the current script is not
+            + UNO-enabled, this script will run, in another process, a UNO-enabled
+            + Python interpreter (whose path is p_pythonWithUnoPath) which will
+            + call LibreOffice. In both cases, we will try to connect to LibreOffice
+            + in server mode on port p_ooPort.
+
+         - If you plan to make "XHTML to OpenDocument" conversions, you may
+            + specify a styles mapping in p_stylesMapping.
+
+         - If you specify a function in p_finalizeFunction, this function will
+            + be called by the renderer before re-zipping the ODT result. This way,
+            + you can still perform some actions on the content of the ODT file
+            + before it is zipped and potentially converted. This function must
+            + accept one arg: the absolute path to the temporary folder containing
+            + the un-zipped content of the ODT result.
+
+         - If you set p_overwriteExisting to True, the renderer will overwrite
+            + the result file. Else, an exception will be thrown if the result file
+            + already exists.
+
+         - p_imageResolver allows POD to retrieve images, from "img" tags within
+            + XHTML content. Indeed, POD may not be able (ie, may not have the
+            + permission to) perform a HTTP GET on those images. Currently, the
+            + resolver can only be a Zope application object.
+         '''

For the template and the result, you can specify absolute or relative paths. I guess it is better to always specify absolute paths.

-

The context may be either a dict, UserDict, or an instance. If it is an instance, its __dict__ attribute is used. For example, context may be the result of calling globals() or locals(). Every (key, value) pair defined in the context corresponds to a name (the key) that you can use within your template within pod statements or expressions. Those names may refer to any Python object: a function, a variable, an object, a module, etc.

+


The context may be either a dict, UserDict, or an instance. If it is an instance, its __dict__ attribute is used. For example, context may be the result of calling globals() or locals(). Every (key, value) pair defined in the context corresponds to a name (the key) that you can use within your template within pod statements or expressions. Those names may refer to any Python object: a function, a variable, an object, a module, etc.

Once you have the Renderer instance, simply call its run method. This method may raise a appy.pod.PodError exception.

-

Since pod 0.0.2, you may put a XHTML document somewhere in the context and ask pod to convert it as a chunk of OpenDocument into the resulting OpenDocument. You may want to customize the mapping between XHTML and OpenDocument styles. This can be done through the stylesMapping parameter. A detailed explanation about the "XHTML to OpenDocument" abilities of pod may be found here.

+


Since pod 0.0.2, you may put a XHTML document somewhere in the context and ask pod to convert it as a chunk of OpenDocument into the resulting OpenDocument. You may want to customize the mapping between XHTML and OpenDocument styles. This can be done through the stylesMapping parameter. A detailed explanation about the "XHTML to OpenDocument" abilities of pod may be found here.

Result formats

-

If result ends with .odt, OpenOffice will NOT be called (unless forceOoCall is True). pod does not need OpenOffice to generate a result in ODT format, excepted in the following cases:

+

If result ends with .odt, LibreOffice will NOT be called (unless forceOoCall is True). pod does not need LibreOffice to generate a result in ODT format, excepted in the following cases:

-

OpenOffice will be called in order to convert a temporary ODT file rendered by pod into the desired format. This will work only if your Python interpreter knows about the Python UNO bindings. UNO is the OpenOffice API. If typing import uno at the interpreter prompt does not produce an error, your interpreter is UNO-enabled. If not, there is probably a UNO-enabled Python interpreter within your OpenOffice copy (in <OpenOfficePath>/program). In this case you can specify this path in the pythonWithUnoPath parameter of the Renderer constructor. Note that when using a UNO-disabled interpreter, there will be one additional process fork for launching a Python-enabled interpreter.

+

LibreOffice will be called in order to convert a temporary ODT file rendered by pod into the desired format. This will work only if your Python interpreter knows about the Python UNO bindings. UNO is the OpenOffice API. If typing import uno at the interpreter prompt does not produce an error, your interpreter is UNO-enabled. If not, there is probably a UNO-enabled Python interpreter within your LibreOffice copy (in <LibreOfficePath>/program). In this case you can specify this path in the pythonWithUnoPath parameter of the Renderer constructor. Note that when using a UNO-disabled interpreter, there will be one additional process fork for launching a Python-enabled interpreter.

During rendering, pod uses a temp folder located at <result>.temp.

-

Launching OpenOffice in server mode

+

Launching LibreOffice in server mode

-

You launch OpenOffice in server mode by running the command:

+

You launch LibreOffice in server mode by running the command (under Linux):

-

(under Windows: ) call "[path_to_oo]\program\soffice" "-accept=socket,host=localhost,port=2002;urp;"& (put this command in .bat file, for example)

+

soffice -invisible -headless "-accept=socket,host=localhost,port=2002;urp;"

-

Under Windows you may also need to define this environment variable (with OpenOffice 3.x) (here it is done in Python):

+

Under Windows it may look like:

-

os.environ['URE_BOOTSTRAP']='file:///C:/Program%20Files/OpenOffice.org%203/program/fundamental.ini'

- -

(under Linux: ) soffice "-accept=socket,host=localhost,port=2002;urp;"

+

"[path_to_lo]\program\soffice" -invisible -headless "-accept=socket,host=localhost,port=2002;urp;"

Of course, use any port number you prefer.

- -

Unfortunately, OpenOffice, even when launched in server mode, needs a Windowing system. So if your server runs Linux you will need to run a X server on it. If you want to avoid windows being shown on your server, you may launch a VNC server and define the variable DISPLAY=:1. If you run Ubuntu server and if you install package "openoffice.org-headless" you will not need a X server. On other Linux flavours you may also investigate the use of xvfb.

- -

Rendering a pod template with Django

- -

pod can be called from any Python program. Here is an example of integration of pod with Django for producing a PDF through the web. In the first code excerpt below, in a Django view I launch a pod Renderer and I give him a mix of Python objects and functions coming from various places (the Django session, other Python modules, etc).

- -

- 01 gotTheLock = Lock.acquire(10)
- 02 if gotTheLock:
- 03    template = '%s/pages/resultEpmDetails.odt' % os.path.dirname(faitesletest.__file__)
- 04    params = self.getParameters()
- 05    # Add 'time' package to renderer context
- 06    import time
- 07    params['time'] = time
- 08    params['i18n'] = i21c(self.session['language'])
- 09    params['floatToString'] = Converter.floatToString
- 10    tmpFolder = os.path.join(os.path.dirname(faitesletest.__file__), 'temp')
- 11    resultFile = os.path.join(tmpFolder, '%s_%f.%s' % (self.session.session_key, time.time(), self.docFormat))
- 12    try:
- 13        renderer = appy.pod.renderer.Renderer(template, params, resultFile, coOpenOfficePath)
- 14        renderer.run()
- 15        Lock.release()
- 16    except PodError, pe:
- 17        Lock.release()
- 18        raise pe
- 19    docFile = open(resultFile, 'rb')
- 20    self.session['doc'] = docFile.read()
- 21    self.session['docFormat'] = self.docFormat
- 22    docFile.close()
- 23    os.remove(resultFile)
- 24 else:
- 25    raise ViaActionError('docError')
-

- -

When I wrote this, I was told of some unstabilities of OpenOffice in server mode (! this info is probably outdated, it was written in 2007). So I decided to send to OpenOffice one request at a time through a Locking system (lines 1 and 2). My site did not need to produce a lot of PDFs, but it may not be what you need for your site.

- -

Line 11, I use the Django session id and current time to produce in a temp folder a unique name for dumping the PDF on disk. Let's assume that self.docFormat is pdf. Then, line 20, I read the content of this file into the Django session. I can then remove the temporary file (line 23). Finally, the chunk of code below is executed and a HttpResponse instance is returned.

- -

- 01 from django.http import HttpResponse
- 02 ...
- 03    # I am within a Django view
- 04    res = HttpResponse(mimetype=self.getDocMimeType()) # Returns "application/pdf" for a PDF and "application/vnd.oasis.opendocument.text" for an ODT
- 05    res['Content-Disposition'] = 'attachment; filename=%s.%s' % (
- 06        self.getDocName(), self.extensions[self.getDocMimeType()])
- 07    res.write(self.getDocContent())
- 08    # Clean session
- 09    self.session['doc'] = None
- 10    self.session['docFormat'] = None
- 11    return res
-

- -

Line 7, self.getDocContent() returns the content of the PDF file, that we stored in self.session['doc'] (line 20 of previous code chunk). Line 5, by specifying attachment we force the browser to show a popup that asks the user if he wants to store the file on disk or open it with a given program. You can also specify inline. In this case the browser will try to open the file within its own window (potentially with the help of a plugin).

- -

Rendering a pod template with Plone

- -

The following code excerpt shows how to render the "ODT view" of a Plone object. Here, we start from an Archetypes class that was generated with ArchGenXML. It is a class named Avis that is part of a Zope/Plone product named Products.Avis. While "Avis" content type instances can be created, deleted, edited, etc, as any Plone object, it can also be rendered as ODT (and in any other format supported by pod/OpenOffice). As shown below we have added a method generateOdt to the Avis class, that calls a private method that does the job.

- -

- 01 import appy.pod.renderer
- 02 import Products.Avis
- 03 ...
- 04    # We are in the Avis class
- 05    security.declarePublic('generateOdt')
- 06    def generateOdt(self, RESPONSE):
- 07        '''Generates the ODT version of this advice.'''
- 08        return self._generate(RESPONSE, 'odt')
- 09
- 10    # Manually created methods
- 11
- 12    def _generate(self, response, fileType):
- 13        '''Generates a document that represents this advice.
- 14           The document format is specified by p_fileType.'''
- 15        # First, generate the ODT in a temp file
- 16        tempFileName = '/tmp/%s.%f.%s' % (self._at_uid, time.time(), fileType)
- 17        renderer = appy.pod.renderer.Renderer('%s/Avis.odt' % os.path.dirname(Products.Avis.__file__), {'avis': self}, tempFileName)
- 18        renderer.run()
- 19        # Tell the browser that the resulting page contains ODT
- 20        response.setHeader('Content-type', 'application/%s' % fileType)
- 21        response.setHeader('Content-disposition', 'inline;filename="%s.%s"' % (self.id, fileType))
- 22        # Returns the doc and removes the temp file
- 23        f = open(tempFileName, 'rb')
- 24        doc = f.read()
- 25        f.close()
- 26        os.remove(tempFileName)
- 27        return doc
-

- -

First, I plan to create the ODT file on disk, in a temp folder. Line 16, I use a combination of the attribute self._at_uid of the Zope/Plone object and the current time to produce a unique name for the temp file. Then (lines 17 and 18) I create and call a pod renderer. I give him a pod template which is located in the Products folder (Avis subfolder) of my Zope instance. Lines 20 and 21, I manipulate the Zope object that represents the HTTP response to tell Zope that the resulting page will be an ODT document. Finally, the method returns the content of the temp file (line 27) that is deleted (line 26). In order to trigger the ODT generation from the "view" page of the Avis content type, I have overridden the header macro in Products/Avis/skins/Avis/avis_view.pt:

- -

- <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US" i18n:domain="plone">
- <body>
-     <div metal:define-macro="header">
-     <div class="documentActions"><a href="" tal:attributes="href python:here['id'] + '/generateOdt'">Generate ODT version</a></div>
-     <h1 tal:content="here/title">Advice title</h1>
-     <div class="discreet">
-         If you want to edit this advice, click on the "edit" tab above.
-     </div>
-     <p></p>
-     </div>
- </body>
- </html>
-

- -

Hum... I know there are cleanest ways to achieve this result :-D

diff --git a/doc/podWritingAdvancedTemplates.html b/doc/podWritingAdvancedTemplates.html index 280dd5c..2f31086 100644 --- a/doc/podWritingAdvancedTemplates.html +++ b/doc/podWritingAdvancedTemplates.html @@ -4,6 +4,9 @@ + +
Historically, defining Python expressions with pod was done via text inserted in 'track changes' mode (with LibreOffice, in the Edit menu, enable/disable this mode by clicking on Modifications->Record). This page still shows examples with track-changed text, although conditional fields (as explained here) have become the preferred way to insert Python expressions.
+

Inserting arbitrary content: the from clause

In the section "Writing templates", you've learned how to write pod statements (if, for). Every pod statement is linked to a given part of the pod template (a text paragraph, a title, a table, a table row, etc) and conditions how this part is rendered in the result (the if statement, for example, renders the part only if the related condition is True). This way to work has its limits. Indeed, you can't insert what you want into the result: you are forced to use the part of the document that is the target of the statement. Of course, in this part, you can still write funny things like Python expressions and statements, but it may not be sufficient.

@@ -12,19 +15,19 @@

In the example below, the statement has a from clause that produces a simple paragraph containing 'Hello'.

-

+

In the result, the targeted paragraph has been replaced by the chunk of odt content specified in the from expression. Note that the from clause MUST start on a new line in the note. Else, it will be considered as part of statement and will probably produce an error.

-

+

Surprise! This statement is neither a 'if' not a 'for' statement... It is a "null" statement whose sole objective is to replace the target by the content of the from expression. But you can also add from clauses to 'if' and 'for' statements. Here is an example with a 'for' statement.

-

+

Here's the result. Note that within the from clause you may use the iterator variable (i, in this case) defined by the for statement.

-

+

Actually, when you don't specify a from clause in a statement, pod generates an implicit from clause whose result comes from the odt chunk that is the target of the statement.

@@ -37,7 +40,7 @@

One of these functions is the xhtml function, that allows to convert chunks of XHTML documents (given as strings in the context) into chunks of OpenDocument within the resulting OpenDocument. This functionality is useful, for example, when using pod with systems like Plone, that maintain a part of their data in XHTML format (Kupu fields, for example).

Suppose you want to render this chunk of XHTML code at some place in your pod result:

- +
@@ -67,10 +70,11 @@
XHTML code XHTML rendering (Plone)
+

pod comes with a function named xhtml that you may use within your pod templates, like this:

-

+

In this example, the name dummy is available in the context, and dummy.getAt1() produces a Python string that contains the XHTML chunk shown above. This string is given as paremeter of the built-in pod xhtml function.

@@ -78,7 +82,7 @@

The rendering produces this document:

-

+

The OpenDocument rendering is a bit different than the XHTML rendering shown above. This is because pod uses the styles found in the pod template and tries to make a correspondence between style information in the XHTML chunk and styles present in the pod template. By default, when pod encounters a XHTML element: