--- a/appobject.py Thu Jan 20 10:10:22 2011 +0100
+++ b/appobject.py Mon Jan 24 17:02:38 2011 +0100
@@ -214,6 +214,9 @@
return NotImplementedError("selector %s must implement its logic "
"in its __call__ method" % self.__class__)
+ def __repr__(self):
+ return u'<Selector %s at %x>' % (self.__class__.__name__, id(self))
+
class MultiSelector(Selector):
"""base class for compound selector classes"""
--- a/cwconfig.py Thu Jan 20 10:10:22 2011 +0100
+++ b/cwconfig.py Mon Jan 24 17:02:38 2011 +0100
@@ -22,7 +22,7 @@
Resource mode
-------------
-A resource *mode* is a predifined set of settings for various resources
+A resource *mode* is a predefined set of settings for various resources
directories, such as cubes, instances, etc. to ease development with the
framework. There are two running modes with *CubicWeb*:
@@ -159,14 +159,6 @@
SMTP_LOCK = Lock()
-class metaconfiguration(type):
- """metaclass to automaticaly register configuration"""
- def __new__(mcs, name, bases, classdict):
- cls = super(metaconfiguration, mcs).__new__(mcs, name, bases, classdict)
- if classdict.get('name'):
- CONFIGURATIONS.append(cls)
- return cls
-
def configuration_cls(name):
"""return the configuration class registered with the given name"""
try:
@@ -290,7 +282,6 @@
class CubicWebNoAppConfiguration(ConfigurationMixIn):
"""base class for cubicweb configuration without a specific instance directory
"""
- __metaclass__ = metaconfiguration
# to set in concrete configuration
name = None
# log messages format (see logging module documentation for available keys)
--- a/cwctl.py Thu Jan 20 10:10:22 2011 +0100
+++ b/cwctl.py Mon Jan 24 17:02:38 2011 +0100
@@ -235,7 +235,7 @@
tinfo = cwcfg.cube_pkginfo(cube)
tversion = tinfo.version
cfgpb.add_cube(cube, tversion)
- except ConfigurationError, ex:
+ except (ConfigurationError, AttributeError), ex:
tinfo = None
tversion = '[missing cube information: %s]' % ex
print '* %s %s' % (cube.ljust(namesize), tversion)
--- a/devtools/__init__.py Thu Jan 20 10:10:22 2011 +0100
+++ b/devtools/__init__.py Mon Jan 24 17:02:38 2011 +0100
@@ -175,6 +175,7 @@
class BaseApptestConfiguration(TestServerConfiguration, TwistedConfiguration):
repo_method = 'inmemory'
+ name = 'all-in-one' # so it search for all-in-one.conf, not repository.conf
options = cwconfig.merge_options(TestServerConfiguration.options
+ TwistedConfiguration.options)
cubicweb_appobject_path = TestServerConfiguration.cubicweb_appobject_path | TwistedConfiguration.cubicweb_appobject_path
--- a/doc/book/en/admin/setup.rst Thu Jan 20 10:10:22 2011 +0100
+++ b/doc/book/en/admin/setup.rst Mon Jan 24 17:02:38 2011 +0100
@@ -2,7 +2,7 @@
.. _SetUpEnv:
-Installation and set-up of a *CubicWeb* environment
+Installation and set-up of a |cubicweb| environment
===================================================
Installation of `Cubicweb` and its dependencies
@@ -68,8 +68,8 @@
`cubicweb with postgresql datatabase`_ and `cubicweb-mysql-support` contains
necessary dependency for using `cubicweb with mysql database`_ .
-There is also a wide variety of :ref:`cubes <Cubes>` listed on the `CubicWeb.org Forge`_
-available as debian packages and tarball.
+There is also a wide variety of :ref:`cubes <AvailableCubes>` listed on the
+`CubicWeb.org Forge`_ available as debian packages and tarball.
The repositories are signed with `Logilab's gnupg key`_. To avoid warning on
"apt-get update":
--- a/doc/book/en/annexes/index.rst Thu Jan 20 10:10:22 2011 +0100
+++ b/doc/book/en/annexes/index.rst Mon Jan 24 17:02:38 2011 +0100
@@ -16,5 +16,4 @@
rql/index
mercurial
depends
- javascript-api
docstrings-conventions
--- a/doc/book/en/annexes/rql/index.rst Thu Jan 20 10:10:22 2011 +0100
+++ b/doc/book/en/annexes/rql/index.rst Mon Jan 24 17:02:38 2011 +0100
@@ -1,4 +1,4 @@
-.. _RQLChapter
+.. _RQLChapter:
Relation Query Language (RQL)
=============================
--- a/doc/book/en/devrepo/cubes/available-cubes.rst Thu Jan 20 10:10:22 2011 +0100
+++ b/doc/book/en/devrepo/cubes/available-cubes.rst Mon Jan 24 17:02:38 2011 +0100
@@ -1,3 +1,4 @@
+.. _AvailableCubes:
Available cubes
---------------
--- a/doc/book/en/devrepo/datamodel/define-workflows.rst Thu Jan 20 10:10:22 2011 +0100
+++ b/doc/book/en/devrepo/datamodel/define-workflows.rst Mon Jan 24 17:02:38 2011 +0100
@@ -8,14 +8,13 @@
General
-------
-A workflow describes how certain entities have to evolve between
-different states. Hence we have a set of states, and a "transition
-graph", i.e. a set of possible transitions from one state to another
-state.
+A workflow describes how certain entities have to evolve between different
+states. Hence we have a set of states, and a "transition graph", i.e. a set of
+possible transitions from one state to another state.
-We will define a simple workflow for a blog, with only the following
-two states: `submitted` and `published`. So first, we create a simple
-|cubicweb| instance in five minutes (see :ref:`BlogFiveMinutes`).
+We will define a simple workflow for a blog, with only the following two states:
+`submitted` and `published`. You may want to take a look at :ref:`_TutosBase` if
+you want to quickly setup an instance running a blog.
Setting up a workflow
---------------------
--- a/doc/book/en/devweb/edition/examples.rst Thu Jan 20 10:10:22 2011 +0100
+++ b/doc/book/en/devweb/edition/examples.rst Mon Jan 24 17:02:38 2011 +0100
@@ -117,7 +117,7 @@
set to 'sendmail', which is our form DOM id as specified by its `domid`
attribute), another to cancel the form which will go back to the previous page
using another javascript call. Also we specify an image to use as button icon as a
-resource identifier (see :ref:`external_resources`) given as last argument to
+resource identifier (see :ref:`uiprops`) given as last argument to
:class:`cubicweb.web.formwidgets.ImgButton`.
To see this form, we still have to wrap it in a view. This is pretty simple:
@@ -131,12 +131,13 @@
def call(self):
form = self._cw.vreg['forms'].select('massmailing', self._cw,
rset=self.cw_rset)
- self.w(form.render())
+ form.render(w=self.w)
As you see, we simply define a view with proper selector so it only apply to a
result set containing :class:`IEmailable` entities, and so that only users in the
managers or users group can use it. Then in the `call()` method for this view we
-simply select the above form and write what its `.render()` method returns.
+simply select the above form and call its `.render()` method with our output
+stream as argument.
When this form is submitted, a controller with id 'sendmail' will be called (as
specified using `action`). This controller will be responsible to actually send
--- a/doc/book/en/devweb/views/basetemplates.rst Thu Jan 20 10:10:22 2011 +0100
+++ b/doc/book/en/devweb/views/basetemplates.rst Mon Jan 24 17:02:38 2011 +0100
@@ -1,7 +1,5 @@
.. -*- coding: utf-8 -*-
-.. |cubicweb| replace:: *CubicWeb*
-
.. _templates:
Templates
--- a/doc/book/en/devweb/views/primary.rst Thu Jan 20 10:10:22 2011 +0100
+++ b/doc/book/en/devweb/views/primary.rst Mon Jan 24 17:02:38 2011 +0100
@@ -226,8 +226,6 @@
We'll show you now an example of a ``primary`` view and how to customize it.
-We continue along the basic tutorial :ref:`tuto_blog`.
-
If you want to change the way a ``BlogEntry`` is displayed, just
override the method ``cell_call()`` of the view ``primary`` in
``BlogDemo/views.py``.
@@ -247,7 +245,7 @@
The above source code defines a new primary view for
-``BlogEntry``. The `id` class attribute is not repeated there since it
+``BlogEntry``. The `__reid__` class attribute is not repeated there since it
is inherited through the `primary.PrimaryView` class.
The selector for this view chains the selector of the inherited class
Binary file doc/book/en/images/tutos-photowebsite_background-image.png has changed
Binary file doc/book/en/images/tutos-photowebsite_boxes.png has changed
Binary file doc/book/en/images/tutos-photowebsite_breadcrumbs.png has changed
Binary file doc/book/en/images/tutos-photowebsite_facets.png has changed
Binary file doc/book/en/images/tutos-photowebsite_grey-box.png has changed
Binary file doc/book/en/images/tutos-photowebsite_index-after.png has changed
Binary file doc/book/en/images/tutos-photowebsite_index-before.png has changed
Binary file doc/book/en/images/tutos-photowebsite_login-box.png has changed
Binary file doc/book/en/images/tutos-photowebsite_prevnext.png has changed
Binary file doc/book/en/images/tutos-photowebsite_ui1.png has changed
Binary file doc/book/en/images/tutos-photowebsite_ui2.png has changed
Binary file doc/book/en/images/tutos-photowebsite_ui3.png has changed
--- a/doc/book/en/index.rst Thu Jan 20 10:10:22 2011 +0100
+++ b/doc/book/en/index.rst Mon Jan 24 17:02:38 2011 +0100
@@ -13,19 +13,28 @@
Its main features are:
-* an engine driven by the explicit :ref:`data model <DefineDataModel>` of the application,
+* an engine driven by the explicit :ref:`data model
+ <TutosBaseCustomizingTheApplicationDataModel>` of the application,
+
* a query language named :ref:`RQL <RQL>` similar to W3C's SPARQL,
-* a :ref:`selection+view <DefineViews>` mechanism for semi-automatic XHTML/XML/JSON/text generation,
-* a library of reusable :ref:`components <cubes>` (data model and views) that fulfill common needs,
+
+* a :ref:`selection+view <TutosBaseCustomizingTheApplicationCustomViews>`
+ mechanism for semi-automatic XHTML/XML/JSON/text generation,
+
+* a library of reusable :ref:`components <Cube>` (data model and views) that
+ fulfill common needs,
+
* the power and flexibility of the Python_ programming language,
-* the reliability of SQL databases, LDAP directories, Subversion and Mercurial for storage backends.
+
+* the reliability of SQL databases, LDAP directories, Subversion and Mercurial
+ for storage backends.
Built since 2000 from an R&D effort still continued, supporting 100,000s of
daily visits at some production sites, |cubicweb| is a proven end to end solution
for semantic web application development that promotes quality, reusability and
efficiency.
-The unbeliever will read the :ref:`Tutorial`.
+The unbeliever will read the :ref:`Tutorials`.
The hacker will join development at the forge_.
--- a/doc/book/en/makefile Thu Jan 20 10:10:22 2011 +0100
+++ b/doc/book/en/makefile Mon Jan 24 17:02:38 2011 +0100
@@ -33,7 +33,7 @@
clean:
rm -f *.html
- -rm -rf ${BUILDDIR}/*
+ -rm -rf ${BUILDDIR}/html ${BUILDDIR}/doctrees
-rm -rf ${BUILDJS}
all: html
--- a/doc/book/en/tutorials/advanced/index.rst Thu Jan 20 10:10:22 2011 +0100
+++ b/doc/book/en/tutorials/advanced/index.rst Mon Jan 24 17:02:38 2011 +0100
@@ -1,7 +1,8 @@
-.. _advanced_tutorial:
+
+.. _TutosPhotoWebSite:
-Building a photo gallery with CubicWeb
-======================================
+Building a photo gallery with |cubicweb|
+========================================
Desired features
----------------
@@ -16,595 +17,13 @@
* advanced security (not everyone can see everything). More on this later.
-Cube creation and schema definition
------------------------------------
-
-.. _adv_tuto_create_new_cube:
-
-Step 1: creating a new cube for my web site
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-One note about my development environment: I wanted to use the packaged
-version of CubicWeb and cubes while keeping my cube in my user
-directory, let's say `~src/cubes`. I achieve this by setting the
-following environment variables::
-
- CW_CUBES_PATH=~/src/cubes
- CW_MODE=user
-
-I can now create the cube which will hold custom code for this web
-site using::
-
- cubicweb-ctl newcube --directory=~/src/cubes sytweb
-
-
-.. _adv_tuto_assemble_cubes:
-
-Step 2: pick building blocks into existing cubes
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Almost everything I want to handle in my web-site is somehow already modelized in
-existing cubes that I'll extend for my need. So I'll pick the following cubes:
-
-* `folder`, containing the `Folder` entity type, which will be used as
- both 'album' and a way to map file system folders. Entities are
- added to a given folder using the `filed_under` relation.
-
-* `file`, containing `File` and `Image` entity types, gallery view,
- and a file system import utility.
-
-* `zone`, containing the `Zone` entity type for hierarchical geographical
- zones. Entities (including sub-zones) are added to a given zone using the
- `situated_in` relation.
-
-* `person`, containing the `Person` entity type plus some basic views.
-
-* `comment`, providing a full commenting system allowing one to comment entity types
- supporting the `comments` relation by adding a `Comment` entity.
-
-* `tag`, providing a full tagging system as an easy and powerful way to classify
- entities supporting the `tags` relation by linking the to `Tag` entities. This
- will allows navigation into a large number of picture.
-
-Ok, now I'll tell my cube requires all this by editing :file:`cubes/sytweb/__pkginfo__.py`:
-
- .. sourcecode:: python
-
- __depends__ = {'cubicweb': '>= 3.8.0',
- 'cubicweb-file': '>= 1.2.0',
- 'cubicweb-folder': '>= 1.1.0',
- 'cubicweb-person': '>= 1.2.0',
- 'cubicweb-comment': '>= 1.2.0',
- 'cubicweb-tag': '>= 1.2.0',
- 'cubicweb-zone': None}
-
-Notice that you can express minimal version of the cube that should be used,
-`None` meaning whatever version available. All packages starting with 'cubicweb-'
-will be recognized as being cube, not bare python packages. You can still specify
-this explicitly using instead the `__depends_cubes__` dictionary which should
-contains cube's name without the prefix. So the example below would be written
-as:
-
- .. sourcecode:: python
+.. toctree::
+ :maxdepth: 2
- __depends__ = {'cubicweb': '>= 3.8.0'}
- __depends_cubes__ = {'file': '>= 1.2.0',
- 'folder': '>= 1.1.0',
- 'person': '>= 1.2.0',
- 'comment': '>= 1.2.0',
- 'tag': '>= 1.2.0',
- 'zone': None}
-
-
-Step 3: glue everything together in my cube's schema
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. sourcecode:: python
-
- from yams.buildobjs import RelationDefinition
-
- class comments(RelationDefinition):
- subject = 'Comment'
- object = ('File', 'Image')
- cardinality = '1*'
- composite = 'object'
-
- class tags(RelationDefinition):
- subject = 'Tag'
- object = ('File', 'Image')
-
- class filed_under(RelationDefinition):
- subject = ('File', 'Image')
- object = 'Folder'
-
- class situated_in(RelationDefinition):
- subject = 'Image'
- object = 'Zone'
-
- class displayed_on(RelationDefinition):
- subject = 'Person'
- object = 'Image'
-
-
-This schema:
-
-* allows to comment and tag on `File` and `Image` entity types by adding the
- `comments` and `tags` relations. This should be all we've to do for this
- feature since the related cubes provide 'pluggable section' which are
- automatically displayed on the primary view of entity types supporting the
- relation.
-
-* adds a `situated_in` relation definition so that image entities can be
- geolocalized.
-
-* add a new relation `displayed_on` relation telling who can be seen on a
- picture.
-
-This schema will probably have to evolve as time goes (for security handling at
-least), but since the possibility to let a schema evolve is one of CubicWeb's
-features (and goals), we won't worry about it for now and see that later when needed.
-
-
-Step 4: creating the instance
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Now that I have a schema, I want to create an instance. To
-do so using this new 'sytweb' cube, I run::
-
- cubicweb-ctl create sytweb sytweb_instance
-
-Hint: if you get an error while the database is initialized, you can
-avoid having to answer the questions again by running::
-
- cubicweb-ctl db-create sytweb_instance
-
-This will use your already configured instance and start directly from the create
-database step, thus skipping questions asked by the 'create' command.
-
-Once the instance and database are fully initialized, run ::
-
- cubicweb-ctl start sytweb_instance
-
-to start the instance, check you can connect on it, etc...
+ part01_create-cube
+ part02_security
+ part03_bfss
+ part04_ui-base
+ part05_ui-advanced
-Security, testing and migration
--------------------------------
-
-This part will cover various topics:
-
-* configuring security
-* migrating existing instance
-* writing some unit tests
-
-Here is the ``read`` security model I want:
-
-* folders, files, images and comments should have one of the following visibility:
-
- - ``public``, everyone can see it
- - ``authenticated``, only authenticated users can see it
- - ``restricted``, only a subset of authenticated users can see it
-
-* managers (e.g. me) can see everything
-* only authenticated users can see people
-* everyone can see classifier entities, such as tag and zone
-
-Also, unless explicitly specified, the visibility of an image should be the same as
-its parent folder, as well as visibility of a comment should be the same as the
-commented entity. If there is no parent entity, the default visibility is
-``authenticated``.
-
-Regarding write security, that's much easier:
-* anonymous can't write anything
-* authenticated users can only add comment
-* managers will add the remaining stuff
-
-Now, let's implement that!
-
-Proper security in CubicWeb is done at the schema level, so you don't have to
-bother with it in views: users will only see what they can see automatically.
-
-.. _adv_tuto_security:
-
-Step 1: configuring security into the schema
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-In schema, you can grant access according to groups, or to some RQL expressions:
-users get access if the expression returns some results. To implement the read
-security defined earlier, groups are not enough, we'll need some RQL expression. Here
-is the idea:
-
-* add a `visibility` attribute on Folder, Image and Comment, which may be one of
- the value explained above
-
-* add a `may_be_read_by` relation from Folder, Image and Comment to users,
- which will define who can see the entity
-
-* security propagation will be done in hook.
-
-So the first thing to do is to modify my cube's schema.py to define those
-relations:
-
-.. sourcecode:: python
-
- from yams.constraints import StaticVocabularyConstraint
-
- class visibility(RelationDefinition):
- subject = ('Folder', 'File', 'Image', 'Comment')
- object = 'String'
- constraints = [StaticVocabularyConstraint(('public', 'authenticated',
- 'restricted', 'parent'))]
- default = 'parent'
- cardinality = '11' # required
-
- class may_be_read_by(RelationDefinition):
- __permissions__ = {
- 'read': ('managers', 'users'),
- 'add': ('managers',),
- 'delete': ('managers',),
- }
-
- subject = ('Folder', 'File', 'Image', 'Comment',)
- object = 'CWUser'
-
-We can note the following points:
-
-* we've added a new `visibility` attribute to folder, file, image and comment
- using a `RelationDefinition`
-
-* `cardinality = '11'` means this attribute is required. This is usually hidden
- under the `required` argument given to the `String` constructor, but we can
- rely on this here (same thing for StaticVocabularyConstraint, which is usually
- hidden by the `vocabulary` argument)
-
-* the `parent` possible value will be used for visibility propagation
-
-* think to secure the `may_be_read_by` permissions, else any user can add/delte it
- by default, which somewhat breaks our security model...
-
-Now, we should be able to define security rules in the schema, based on these new
-attribute and relation. Here is the code to add to *schema.py*:
-
-.. sourcecode:: python
-
- from cubicweb.schema import ERQLExpression
-
- VISIBILITY_PERMISSIONS = {
- 'read': ('managers',
- ERQLExpression('X visibility "public"'),
- ERQLExpression('X may_be_read_by U')),
- 'add': ('managers',),
- 'update': ('managers', 'owners',),
- 'delete': ('managers', 'owners'),
- }
- AUTH_ONLY_PERMISSIONS = {
- 'read': ('managers', 'users'),
- 'add': ('managers',),
- 'update': ('managers', 'owners',),
- 'delete': ('managers', 'owners'),
- }
- CLASSIFIERS_PERMISSIONS = {
- 'read': ('managers', 'users', 'guests'),
- 'add': ('managers',),
- 'update': ('managers', 'owners',),
- 'delete': ('managers', 'owners'),
- }
-
- from cubes.folder.schema import Folder
- from cubes.file.schema import File, Image
- from cubes.comment.schema import Comment
- from cubes.person.schema import Person
- from cubes.zone.schema import Zone
- from cubes.tag.schema import Tag
-
- Folder.__permissions__ = VISIBILITY_PERMISSIONS
- File.__permissions__ = VISIBILITY_PERMISSIONS
- Image.__permissions__ = VISIBILITY_PERMISSIONS
- Comment.__permissions__ = VISIBILITY_PERMISSIONS.copy()
- Comment.__permissions__['add'] = ('managers', 'users',)
- Person.__permissions__ = AUTH_ONLY_PERMISSIONS
- Zone.__permissions__ = CLASSIFIERS_PERMISSIONS
- Tag.__permissions__ = CLASSIFIERS_PERMISSIONS
-
-What's important in there:
-
-* `VISIBILITY_PERMISSIONS` provides read access to managers group, if
- `visibility` attribute's value is 'public', or if user (designed by the 'U'
- variable in the expression) is linked to the entity (the 'X' variable) through
- the `may_read` permission
-
-* we modify permissions of the entity types we use by importing them and
- modifying their `__permissions__` attribute
-
-* notice the `.copy()`: we only want to modify 'add' permission for `Comment`,
- not for all entity types using `VISIBILITY_PERMISSIONS`!
-
-* the remaining part of the security model is done using regular groups:
-
- - `users` is the group to which all authenticated users will belong
- - `guests` is the group of anonymous users
-
-
-.. _adv_tuto_security_propagation:
-
-Step 2: security propagation in hooks
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-To fullfill the requirements, we have to implement::
-
- Also, unless explicity specified, visibility of an image should be the same as
- its parent folder, as well as visibility of a comment should be the same as the
- commented entity.
-
-This kind of `active` rule will be done using CubicWeb's hook
-system. Hooks are triggered on database event such as addition of new
-entity or relation.
-
-The tricky part of the requirement is in *unless explicitly specified*, notably
-because when the entity is added, we don't know yet its 'parent'
-entity (e.g. Folder of an Image, Image commented by a Comment). To handle such things,
-CubicWeb provides `Operation`, which allow to schedule things to do at commit time.
-
-In our case we will:
-
-* on entity creation, schedule an operation that will set default visibility
-
-* when a "parent" relation is added, propagate parent's visibility unless the
- child already has a visibility set
-
-Here is the code in cube's *hooks.py*:
-
-.. sourcecode:: python
-
- from cubicweb.selectors import is_instance
- from cubicweb.server import hook
-
- class SetVisibilityOp(hook.Operation):
- def precommit_event(self):
- for eid in self.session.transaction_data.pop('pending_visibility'):
- entity = self.session.entity_from_eid(eid)
- if entity.visibility == 'parent':
- entity.set_attributes(visibility=u'authenticated')
-
- class SetVisibilityHook(hook.Hook):
- __regid__ = 'sytweb.setvisibility'
- __select__ = hook.Hook.__select__ & is_instance('Folder', 'File', 'Image', 'Comment')
- events = ('after_add_entity',)
- def __call__(self):
- hook.set_operation(self._cw, 'pending_visibility', self.entity.eid,
- SetVisibilityOp)
-
- class SetParentVisibilityHook(hook.Hook):
- __regid__ = 'sytweb.setparentvisibility'
- __select__ = hook.Hook.__select__ & hook.match_rtype('filed_under', 'comments')
- events = ('after_add_relation',)
-
- def __call__(self):
- parent = self._cw.entity_from_eid(self.eidto)
- child = self._cw.entity_from_eid(self.eidfrom)
- if child.visibility == 'parent':
- child.set_attributes(visibility=parent.visibility)
-
-Notice:
-
-* hooks are application objects, hence have selectors that should match entity or
- relation types to which the hook applies. To match a relation type, we use the
- hook specific `match_rtype` selector.
-
-* usage of `set_operation`: instead of adding an operation for each added entity,
- set_operation allows to create a single one and to store entity's eids to be
- processed in session's transaction data. This is a good pratice to avoid heavy
- operations manipulation cost when creating a lot of entities in the same
- transaction.
-
-* the `precommit_event` method of the operation will be called at transaction's
- commit time.
-
-* in a hook, `self._cw` is the repository session, not a web request as usually
- in views
-
-* according to hook's event, you have access to different attributes on the hook
- instance. Here:
-
- - `self.entity` is the newly added entity on 'after_add_entity' events
-
- - `self.eidfrom` / `self.eidto` are the eid of the subject / object entity on
- 'after_add_relatiohn' events (you may also get the relation type using
- `self.rtype`)
-
-The `parent` visibility value is used to tell "propagate using parent security"
-because we want that attribute to be required, so we can't use None value else
-we'll get an error before we get any chance to propagate...
-
-Now, we also want to propagate the `may_be_read_by` relation. Fortunately,
-CubicWeb provides some base hook classes for such things, so we only have to add
-the following code to *hooks.py*:
-
-.. sourcecode:: python
-
- # relations where the "parent" entity is the subject
- S_RELS = set()
- # relations where the "parent" entity is the object
- O_RELS = set(('filed_under', 'comments',))
-
- class AddEntitySecurityPropagationHook(hook.PropagateSubjectRelationHook):
- """propagate permissions when new entity are added"""
- __regid__ = 'sytweb.addentity_security_propagation'
- __select__ = (hook.PropagateSubjectRelationHook.__select__
- & hook.match_rtype_sets(S_RELS, O_RELS))
- main_rtype = 'may_be_read_by'
- subject_relations = S_RELS
- object_relations = O_RELS
-
- class AddPermissionSecurityPropagationHook(hook.PropagateSubjectRelationAddHook):
- """propagate permissions when new entity are added"""
- __regid__ = 'sytweb.addperm_security_propagation'
- __select__ = (hook.PropagateSubjectRelationAddHook.__select__
- & hook.match_rtype('may_be_read_by',))
- subject_relations = S_RELS
- object_relations = O_RELS
-
- class DelPermissionSecurityPropagationHook(hook.PropagateSubjectRelationDelHook):
- __regid__ = 'sytweb.delperm_security_propagation'
- __select__ = (hook.PropagateSubjectRelationDelHook.__select__
- & hook.match_rtype('may_be_read_by',))
- subject_relations = S_RELS
- object_relations = O_RELS
-
-* the `AddEntitySecurityPropagationHook` will propagate the relation
- when `filed_under` or `comments` relations are added
-
- - the `S_RELS` and `O_RELS` set as well as the `match_rtype_sets` selector are
- used here so that if my cube is used by another one, it'll be able to
- configure security propagation by simply adding relation to one of the two
- sets.
-
-* the two others will propagate permissions changes on parent entities to
- children entities
-
-
-.. _adv_tuto_tesing_security:
-
-Step 3: testing our security
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Security is tricky. Writing some tests for it is a very good idea. You should
-even write them first, as Test Driven Development recommends!
-
-Here is a small test case that will check the basis of our security
-model, in *test/unittest_sytweb.py*:
-
-.. sourcecode:: python
-
- from cubicweb.devtools.testlib import CubicWebTC
- from cubicweb import Binary
-
- class SecurityTC(CubicWebTC):
-
- def test_visibility_propagation(self):
- # create a user for later security checks
- toto = self.create_user('toto')
- # init some data using the default manager connection
- req = self.request()
- folder = req.create_entity('Folder',
- name=u'restricted',
- visibility=u'restricted')
- photo1 = req.create_entity('Image',
- data_name=u'photo1.jpg',
- data=Binary('xxx'),
- filed_under=folder)
- self.commit()
- photo1.clear_all_caches() # good practice, avoid request cache effects
- # visibility propagation
- self.assertEquals(photo1.visibility, 'restricted')
- # unless explicitly specified
- photo2 = req.create_entity('Image',
- data_name=u'photo2.jpg',
- data=Binary('xxx'),
- visibility=u'public',
- filed_under=folder)
- self.commit()
- self.assertEquals(photo2.visibility, 'public')
- # test security
- self.login('toto')
- req = self.request()
- self.assertEquals(len(req.execute('Image X')), 1) # only the public one
- self.assertEquals(len(req.execute('Folder X')), 0) # restricted...
- # may_be_read_by propagation
- self.restore_connection()
- folder.set_relations(may_be_read_by=toto)
- self.commit()
- photo1.clear_all_caches()
- self.failUnless(photo1.may_be_read_by)
- # test security with permissions
- self.login('toto')
- req = self.request()
- self.assertEquals(len(req.execute('Image X')), 2) # now toto has access to photo2
- self.assertEquals(len(req.execute('Folder X')), 1) # and to restricted folder
-
- if __name__ == '__main__':
- from logilab.common.testlib import unittest_main
- unittest_main()
-
-It's not complete, but show most things you'll want to do in tests: adding some
-content, creating users and connecting as them in the test, etc...
-
-To run it type:
-
-.. sourcecode:: bash
-
- $ pytest unittest_sytweb.py
- ======================== unittest_sytweb.py ========================
- -> creating tables [....................]
- -> inserting default user and default groups.
- -> storing the schema in the database [....................]
- -> database for instance data initialized.
- .
- ----------------------------------------------------------------------
- Ran 1 test in 22.547s
-
- OK
-
-
-The first execution is taking time, since it creates a sqlite database for the
-test instance. The second one will be much quicker:
-
-.. sourcecode:: bash
-
- $ pytest unittest_sytweb.py
- ======================== unittest_sytweb.py ========================
- .
- ----------------------------------------------------------------------
- Ran 1 test in 2.662s
-
- OK
-
-If you do some changes in your schema, you'll have to force regeneration of that
-database. You do that by removing the tmpdb files before running the test: ::
-
- $ rm data/tmpdb*
-
-
-.. Note::
- pytest is a very convenient utility used to control test execution. It is available from the `logilab-common`_ package.
-
-.. _`logilab-common`: http://www.logilab.org/project/logilab-common
-
-.. _adv_tuto_migration_script:
-
-Step 4: writing the migration script and migrating the instance
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Prior to those changes, I created an instance, feeded it with some data, so I
-don't want to create a new one, but to migrate the existing one. Let's see how to
-do that.
-
-Migration commands should be put in the cube's *migration* directory, in a
-file named file:`<X.Y.Z>_Any.py` ('Any' being there mostly for historical reason).
-
-Here I'll create a *migration/0.2.0_Any.py* file containing the following
-instructions:
-
-.. sourcecode:: python
-
- add_relation_type('may_be_read_by')
- add_relation_type('visibility')
- sync_schema_props_perms()
-
-Then I update the version number in cube's *__pkginfo__.py* to 0.2.0. And
-that's it! Those instructions will:
-
-* update the instance's schema by adding our two new relations and update the
- underlying database tables accordingly (the two first instructions)
-
-* update schema's permissions definition (the last instruction)
-
-
-To migrate my instance I simply type::
-
- cubicweb-ctl upgrade sytweb
-
-I'll then be asked some questions to do the migration step by step. You should say
-YES when it asks if a backup of your database should be done, so you can get back
-to initial state if anything goes wrong...
-
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/tutorials/advanced/part01_create-cube.rst Mon Jan 24 17:02:38 2011 +0100
@@ -0,0 +1,153 @@
+.. _TutosPhotoWebSiteCubeCreation:
+
+Cube creation and schema definition
+-----------------------------------
+
+.. _adv_tuto_create_new_cube:
+
+Step 1: creating a new cube for my web site
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+One note about my development environment: I wanted to use the packaged
+version of CubicWeb and cubes while keeping my cube in my user
+directory, let's say `~src/cubes`. I achieve this by setting the
+following environment variables::
+
+ CW_CUBES_PATH=~/src/cubes
+ CW_MODE=user
+
+I can now create the cube which will hold custom code for this web
+site using::
+
+ cubicweb-ctl newcube --directory=~/src/cubes sytweb
+
+
+.. _adv_tuto_assemble_cubes:
+
+Step 2: pick building blocks into existing cubes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Almost everything I want to handle in my web-site is somehow already modelized in
+existing cubes that I'll extend for my need. So I'll pick the following cubes:
+
+* `folder`, containing the `Folder` entity type, which will be used as
+ both 'album' and a way to map file system folders. Entities are
+ added to a given folder using the `filed_under` relation.
+
+* `file`, containing `File` and `Image` entity types, gallery view,
+ and a file system import utility.
+
+* `zone`, containing the `Zone` entity type for hierarchical geographical
+ zones. Entities (including sub-zones) are added to a given zone using the
+ `situated_in` relation.
+
+* `person`, containing the `Person` entity type plus some basic views.
+
+* `comment`, providing a full commenting system allowing one to comment entity types
+ supporting the `comments` relation by adding a `Comment` entity.
+
+* `tag`, providing a full tagging system as an easy and powerful way to classify
+ entities supporting the `tags` relation by linking the to `Tag` entities. This
+ will allows navigation into a large number of picture.
+
+Ok, now I'll tell my cube requires all this by editing :file:`cubes/sytweb/__pkginfo__.py`:
+
+ .. sourcecode:: python
+
+ __depends__ = {'cubicweb': '>= 3.8.0',
+ 'cubicweb-file': '>= 1.2.0',
+ 'cubicweb-folder': '>= 1.1.0',
+ 'cubicweb-person': '>= 1.2.0',
+ 'cubicweb-comment': '>= 1.2.0',
+ 'cubicweb-tag': '>= 1.2.0',
+ 'cubicweb-zone': None}
+
+Notice that you can express minimal version of the cube that should be used,
+`None` meaning whatever version available. All packages starting with 'cubicweb-'
+will be recognized as being cube, not bare python packages. You can still specify
+this explicitly using instead the `__depends_cubes__` dictionary which should
+contains cube's name without the prefix. So the example below would be written
+as:
+
+ .. sourcecode:: python
+
+ __depends__ = {'cubicweb': '>= 3.8.0'}
+ __depends_cubes__ = {'file': '>= 1.2.0',
+ 'folder': '>= 1.1.0',
+ 'person': '>= 1.2.0',
+ 'comment': '>= 1.2.0',
+ 'tag': '>= 1.2.0',
+ 'zone': None}
+
+
+Step 3: glue everything together in my cube's schema
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. sourcecode:: python
+
+ from yams.buildobjs import RelationDefinition
+
+ class comments(RelationDefinition):
+ subject = 'Comment'
+ object = ('File', 'Image')
+ cardinality = '1*'
+ composite = 'object'
+
+ class tags(RelationDefinition):
+ subject = 'Tag'
+ object = ('File', 'Image')
+
+ class filed_under(RelationDefinition):
+ subject = ('File', 'Image')
+ object = 'Folder'
+
+ class situated_in(RelationDefinition):
+ subject = 'Image'
+ object = 'Zone'
+
+ class displayed_on(RelationDefinition):
+ subject = 'Person'
+ object = 'Image'
+
+
+This schema:
+
+* allows to comment and tag on `File` and `Image` entity types by adding the
+ `comments` and `tags` relations. This should be all we've to do for this
+ feature since the related cubes provide 'pluggable section' which are
+ automatically displayed on the primary view of entity types supporting the
+ relation.
+
+* adds a `situated_in` relation definition so that image entities can be
+ geolocalized.
+
+* add a new relation `displayed_on` relation telling who can be seen on a
+ picture.
+
+This schema will probably have to evolve as time goes (for security handling at
+least), but since the possibility to let a schema evolve is one of CubicWeb's
+features (and goals), we won't worry about it for now and see that later when needed.
+
+
+Step 4: creating the instance
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Now that I have a schema, I want to create an instance. To
+do so using this new 'sytweb' cube, I run::
+
+ cubicweb-ctl create sytweb sytweb_instance
+
+Hint: if you get an error while the database is initialized, you can
+avoid having to answer the questions again by running::
+
+ cubicweb-ctl db-create sytweb_instance
+
+This will use your already configured instance and start directly from the create
+database step, thus skipping questions asked by the 'create' command.
+
+Once the instance and database are fully initialized, run ::
+
+ cubicweb-ctl start sytweb_instance
+
+to start the instance, check you can connect on it, etc...
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/tutorials/advanced/part02_security.rst Mon Jan 24 17:02:38 2011 +0100
@@ -0,0 +1,441 @@
+.. _TutosPhotoWebSiteSecurity:
+
+Security, testing and migration
+-------------------------------
+
+This part will cover various topics:
+
+* configuring security
+* migrating existing instance
+* writing some unit tests
+
+Here is the ``read`` security model I want:
+
+* folders, files, images and comments should have one of the following visibility:
+
+ - ``public``, everyone can see it
+ - ``authenticated``, only authenticated users can see it
+ - ``restricted``, only a subset of authenticated users can see it
+
+* managers (e.g. me) can see everything
+* only authenticated users can see people
+* everyone can see classifier entities, such as tag and zone
+
+Also, unless explicitly specified, the visibility of an image should be the same as
+its parent folder, as well as visibility of a comment should be the same as the
+commented entity. If there is no parent entity, the default visibility is
+``authenticated``.
+
+Regarding write security, that's much easier:
+* anonymous can't write anything
+* authenticated users can only add comment
+* managers will add the remaining stuff
+
+Now, let's implement that!
+
+Proper security in CubicWeb is done at the schema level, so you don't have to
+bother with it in views: users will only see what they can see automatically.
+
+.. _adv_tuto_security:
+
+Step 1: configuring security into the schema
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In schema, you can grant access according to groups, or to some RQL expressions:
+users get access if the expression returns some results. To implement the read
+security defined earlier, groups are not enough, we'll need some RQL expression. Here
+is the idea:
+
+* add a `visibility` attribute on Folder, Image and Comment, which may be one of
+ the value explained above
+
+* add a `may_be_read_by` relation from Folder, Image and Comment to users,
+ which will define who can see the entity
+
+* security propagation will be done in hook.
+
+So the first thing to do is to modify my cube's schema.py to define those
+relations:
+
+.. sourcecode:: python
+
+ from yams.constraints import StaticVocabularyConstraint
+
+ class visibility(RelationDefinition):
+ subject = ('Folder', 'File', 'Image', 'Comment')
+ object = 'String'
+ constraints = [StaticVocabularyConstraint(('public', 'authenticated',
+ 'restricted', 'parent'))]
+ default = 'parent'
+ cardinality = '11' # required
+
+ class may_be_read_by(RelationDefinition):
+ __permissions__ = {
+ 'read': ('managers', 'users'),
+ 'add': ('managers',),
+ 'delete': ('managers',),
+ }
+
+ subject = ('Folder', 'File', 'Image', 'Comment',)
+ object = 'CWUser'
+
+We can note the following points:
+
+* we've added a new `visibility` attribute to folder, file, image and comment
+ using a `RelationDefinition`
+
+* `cardinality = '11'` means this attribute is required. This is usually hidden
+ under the `required` argument given to the `String` constructor, but we can
+ rely on this here (same thing for StaticVocabularyConstraint, which is usually
+ hidden by the `vocabulary` argument)
+
+* the `parent` possible value will be used for visibility propagation
+
+* think to secure the `may_be_read_by` permissions, else any user can add/delete it
+ by default, which somewhat breaks our security model...
+
+Now, we should be able to define security rules in the schema, based on these new
+attribute and relation. Here is the code to add to *schema.py*:
+
+.. sourcecode:: python
+
+ from cubicweb.schema import ERQLExpression
+
+ VISIBILITY_PERMISSIONS = {
+ 'read': ('managers',
+ ERQLExpression('X visibility "public"'),
+ ERQLExpression('X may_be_read_by U')),
+ 'add': ('managers',),
+ 'update': ('managers', 'owners',),
+ 'delete': ('managers', 'owners'),
+ }
+ AUTH_ONLY_PERMISSIONS = {
+ 'read': ('managers', 'users'),
+ 'add': ('managers',),
+ 'update': ('managers', 'owners',),
+ 'delete': ('managers', 'owners'),
+ }
+ CLASSIFIERS_PERMISSIONS = {
+ 'read': ('managers', 'users', 'guests'),
+ 'add': ('managers',),
+ 'update': ('managers', 'owners',),
+ 'delete': ('managers', 'owners'),
+ }
+
+ from cubes.folder.schema import Folder
+ from cubes.file.schema import File, Image
+ from cubes.comment.schema import Comment
+ from cubes.person.schema import Person
+ from cubes.zone.schema import Zone
+ from cubes.tag.schema import Tag
+
+ Folder.__permissions__ = VISIBILITY_PERMISSIONS
+ File.__permissions__ = VISIBILITY_PERMISSIONS
+ Image.__permissions__ = VISIBILITY_PERMISSIONS
+ Comment.__permissions__ = VISIBILITY_PERMISSIONS.copy()
+ Comment.__permissions__['add'] = ('managers', 'users',)
+ Person.__permissions__ = AUTH_ONLY_PERMISSIONS
+ Zone.__permissions__ = CLASSIFIERS_PERMISSIONS
+ Tag.__permissions__ = CLASSIFIERS_PERMISSIONS
+
+What's important in there:
+
+* `VISIBILITY_PERMISSIONS` provides read access to managers group, if
+ `visibility` attribute's value is 'public', or if user (designed by the 'U'
+ variable in the expression) is linked to the entity (the 'X' variable) through
+ the `may_read` permission
+
+* we modify permissions of the entity types we use by importing them and
+ modifying their `__permissions__` attribute
+
+* notice the `.copy()`: we only want to modify 'add' permission for `Comment`,
+ not for all entity types using `VISIBILITY_PERMISSIONS`!
+
+* the remaining part of the security model is done using regular groups:
+
+ - `users` is the group to which all authenticated users will belong
+ - `guests` is the group of anonymous users
+
+
+.. _adv_tuto_security_propagation:
+
+Step 2: security propagation in hooks
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To fullfill the requirements, we have to implement::
+
+ Also, unless explicity specified, visibility of an image should be the same as
+ its parent folder, as well as visibility of a comment should be the same as the
+ commented entity.
+
+This kind of `active` rule will be done using CubicWeb's hook
+system. Hooks are triggered on database event such as addition of new
+entity or relation.
+
+The tricky part of the requirement is in *unless explicitly specified*, notably
+because when the entity is added, we don't know yet its 'parent'
+entity (e.g. Folder of an Image, Image commented by a Comment). To handle such things,
+CubicWeb provides `Operation`, which allow to schedule things to do at commit time.
+
+In our case we will:
+
+* on entity creation, schedule an operation that will set default visibility
+
+* when a "parent" relation is added, propagate parent's visibility unless the
+ child already has a visibility set
+
+Here is the code in cube's *hooks.py*:
+
+.. sourcecode:: python
+
+ from cubicweb.selectors import is_instance
+ from cubicweb.server import hook
+
+ class SetVisibilityOp(hook.Operation):
+ def precommit_event(self):
+ for eid in self.session.transaction_data.pop('pending_visibility'):
+ entity = self.session.entity_from_eid(eid)
+ if entity.visibility == 'parent':
+ entity.set_attributes(visibility=u'authenticated')
+
+ class SetVisibilityHook(hook.Hook):
+ __regid__ = 'sytweb.setvisibility'
+ __select__ = hook.Hook.__select__ & is_instance('Folder', 'File', 'Image', 'Comment')
+ events = ('after_add_entity',)
+ def __call__(self):
+ hook.set_operation(self._cw, 'pending_visibility', self.entity.eid,
+ SetVisibilityOp)
+
+ class SetParentVisibilityHook(hook.Hook):
+ __regid__ = 'sytweb.setparentvisibility'
+ __select__ = hook.Hook.__select__ & hook.match_rtype('filed_under', 'comments')
+ events = ('after_add_relation',)
+
+ def __call__(self):
+ parent = self._cw.entity_from_eid(self.eidto)
+ child = self._cw.entity_from_eid(self.eidfrom)
+ if child.visibility == 'parent':
+ child.set_attributes(visibility=parent.visibility)
+
+Notice:
+
+* hooks are application objects, hence have selectors that should match entity or
+ relation types to which the hook applies. To match a relation type, we use the
+ hook specific `match_rtype` selector.
+
+* usage of `set_operation`: instead of adding an operation for each added entity,
+ set_operation allows to create a single one and to store entity's eids to be
+ processed in session's transaction data. This is a good pratice to avoid heavy
+ operations manipulation cost when creating a lot of entities in the same
+ transaction.
+
+* the `precommit_event` method of the operation will be called at transaction's
+ commit time.
+
+* in a hook, `self._cw` is the repository session, not a web request as usually
+ in views
+
+* according to hook's event, you have access to different attributes on the hook
+ instance. Here:
+
+ - `self.entity` is the newly added entity on 'after_add_entity' events
+
+ - `self.eidfrom` / `self.eidto` are the eid of the subject / object entity on
+ 'after_add_relatiohn' events (you may also get the relation type using
+ `self.rtype`)
+
+The `parent` visibility value is used to tell "propagate using parent security"
+because we want that attribute to be required, so we can't use None value else
+we'll get an error before we get any chance to propagate...
+
+Now, we also want to propagate the `may_be_read_by` relation. Fortunately,
+CubicWeb provides some base hook classes for such things, so we only have to add
+the following code to *hooks.py*:
+
+.. sourcecode:: python
+
+ # relations where the "parent" entity is the subject
+ S_RELS = set()
+ # relations where the "parent" entity is the object
+ O_RELS = set(('filed_under', 'comments',))
+
+ class AddEntitySecurityPropagationHook(hook.PropagateSubjectRelationHook):
+ """propagate permissions when new entity are added"""
+ __regid__ = 'sytweb.addentity_security_propagation'
+ __select__ = (hook.PropagateSubjectRelationHook.__select__
+ & hook.match_rtype_sets(S_RELS, O_RELS))
+ main_rtype = 'may_be_read_by'
+ subject_relations = S_RELS
+ object_relations = O_RELS
+
+ class AddPermissionSecurityPropagationHook(hook.PropagateSubjectRelationAddHook):
+ """propagate permissions when new entity are added"""
+ __regid__ = 'sytweb.addperm_security_propagation'
+ __select__ = (hook.PropagateSubjectRelationAddHook.__select__
+ & hook.match_rtype('may_be_read_by',))
+ subject_relations = S_RELS
+ object_relations = O_RELS
+
+ class DelPermissionSecurityPropagationHook(hook.PropagateSubjectRelationDelHook):
+ __regid__ = 'sytweb.delperm_security_propagation'
+ __select__ = (hook.PropagateSubjectRelationDelHook.__select__
+ & hook.match_rtype('may_be_read_by',))
+ subject_relations = S_RELS
+ object_relations = O_RELS
+
+* the `AddEntitySecurityPropagationHook` will propagate the relation
+ when `filed_under` or `comments` relations are added
+
+ - the `S_RELS` and `O_RELS` set as well as the `match_rtype_sets` selector are
+ used here so that if my cube is used by another one, it'll be able to
+ configure security propagation by simply adding relation to one of the two
+ sets.
+
+* the two others will propagate permissions changes on parent entities to
+ children entities
+
+
+.. _adv_tuto_tesing_security:
+
+Step 3: testing our security
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Security is tricky. Writing some tests for it is a very good idea. You should
+even write them first, as Test Driven Development recommends!
+
+Here is a small test case that will check the basis of our security
+model, in *test/unittest_sytweb.py*:
+
+.. sourcecode:: python
+
+ from cubicweb.devtools.testlib import CubicWebTC
+ from cubicweb import Binary
+
+ class SecurityTC(CubicWebTC):
+
+ def test_visibility_propagation(self):
+ # create a user for later security checks
+ toto = self.create_user('toto')
+ # init some data using the default manager connection
+ req = self.request()
+ folder = req.create_entity('Folder',
+ name=u'restricted',
+ visibility=u'restricted')
+ photo1 = req.create_entity('Image',
+ data_name=u'photo1.jpg',
+ data=Binary('xxx'),
+ filed_under=folder)
+ self.commit()
+ photo1.clear_all_caches() # good practice, avoid request cache effects
+ # visibility propagation
+ self.assertEquals(photo1.visibility, 'restricted')
+ # unless explicitly specified
+ photo2 = req.create_entity('Image',
+ data_name=u'photo2.jpg',
+ data=Binary('xxx'),
+ visibility=u'public',
+ filed_under=folder)
+ self.commit()
+ self.assertEquals(photo2.visibility, 'public')
+ # test security
+ self.login('toto')
+ req = self.request()
+ self.assertEquals(len(req.execute('Image X')), 1) # only the public one
+ self.assertEquals(len(req.execute('Folder X')), 0) # restricted...
+ # may_be_read_by propagation
+ self.restore_connection()
+ folder.set_relations(may_be_read_by=toto)
+ self.commit()
+ photo1.clear_all_caches()
+ self.failUnless(photo1.may_be_read_by)
+ # test security with permissions
+ self.login('toto')
+ req = self.request()
+ self.assertEquals(len(req.execute('Image X')), 2) # now toto has access to photo2
+ self.assertEquals(len(req.execute('Folder X')), 1) # and to restricted folder
+
+ if __name__ == '__main__':
+ from logilab.common.testlib import unittest_main
+ unittest_main()
+
+It's not complete, but show most things you'll want to do in tests: adding some
+content, creating users and connecting as them in the test, etc...
+
+To run it type:
+
+.. sourcecode:: bash
+
+ $ pytest unittest_sytweb.py
+ ======================== unittest_sytweb.py ========================
+ -> creating tables [....................]
+ -> inserting default user and default groups.
+ -> storing the schema in the database [....................]
+ -> database for instance data initialized.
+ .
+ ----------------------------------------------------------------------
+ Ran 1 test in 22.547s
+
+ OK
+
+
+The first execution is taking time, since it creates a sqlite database for the
+test instance. The second one will be much quicker:
+
+.. sourcecode:: bash
+
+ $ pytest unittest_sytweb.py
+ ======================== unittest_sytweb.py ========================
+ .
+ ----------------------------------------------------------------------
+ Ran 1 test in 2.662s
+
+ OK
+
+If you do some changes in your schema, you'll have to force regeneration of that
+database. You do that by removing the tmpdb files before running the test: ::
+
+ $ rm data/tmpdb*
+
+
+.. Note::
+ pytest is a very convenient utility used to control test execution. It is available from the `logilab-common`_ package.
+
+.. _`logilab-common`: http://www.logilab.org/project/logilab-common
+
+.. _adv_tuto_migration_script:
+
+Step 4: writing the migration script and migrating the instance
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Prior to those changes, I created an instance, feeded it with some data, so I
+don't want to create a new one, but to migrate the existing one. Let's see how to
+do that.
+
+Migration commands should be put in the cube's *migration* directory, in a
+file named file:`<X.Y.Z>_Any.py` ('Any' being there mostly for historical reason).
+
+Here I'll create a *migration/0.2.0_Any.py* file containing the following
+instructions:
+
+.. sourcecode:: python
+
+ add_relation_type('may_be_read_by')
+ add_relation_type('visibility')
+ sync_schema_props_perms()
+
+Then I update the version number in cube's *__pkginfo__.py* to 0.2.0. And
+that's it! Those instructions will:
+
+* update the instance's schema by adding our two new relations and update the
+ underlying database tables accordingly (the two first instructions)
+
+* update schema's permissions definition (the last instruction)
+
+
+To migrate my instance I simply type::
+
+ cubicweb-ctl upgrade sytweb
+
+You'll then be asked some questions to do the migration step by step. You should say
+YES when it asks if a backup of your database should be done, so you can get back
+to initial state if anything goes wrong...
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/tutorials/advanced/part03_bfss.rst Mon Jan 24 17:02:38 2011 +0100
@@ -0,0 +1,134 @@
+Storing images on the file-system
+---------------------------------
+
+Step 1: configuring the BytesFileSystem storage
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To avoid cluttering my database, and to ease file manipulation, I don't want
+them to be stored in the database. I want to be able create File/Image entities
+for some files on the server file system, where those file will be accessed to
+get entities data. To do so I've to set a custom :class:`BytesFileSystemStorage` storage
+for the File/Image 'data' attribute, which hold the actual file's content.
+
+Since the function to register a custom storage needs to have a repository
+instance as first argument, we've to call it in a server startup hook. So I added
+in `cubes/sytweb/hooks.py` :
+
+.. sourcecode:: python
+
+ from os import makedirs
+ from os.path import join, exists
+
+ from cubicweb.server import hook
+ from cubicweb.server.sources import storage
+
+ class ServerStartupHook(hook.Hook):
+ __regid__ = 'sytweb.serverstartup'
+ events = ('server_startup', 'server_maintenance')
+
+ def __call__(self):
+ bfssdir = join(self.repo.config.appdatahome, 'bfss')
+ if not exists(bfssdir):
+ makedirs(bfssdir)
+ print 'created', bfssdir
+ storage = storages.BytesFileSystemStorage(bfssdir)
+ set_attribute_storage(self.repo, 'File', 'data', storage)
+ set_attribute_storage(self.repo, 'Image', 'data', storage)
+
+.. Note::
+
+ * how we built the hook's registry identifier (_`_regid__`): you can introduce
+ 'namespaces' by using there python module like naming identifiers. This is
+ especially import for hooks where you usually want a new custom hook, not
+ overriding / specializing an existant one, but the concept may be applied to
+ any application objects
+
+ * we catch two events here: "server_startup" and "server_maintenance". The first
+ is called on regular repository startup (eg, as a server), the other for
+ maintenance task such as shell or upgrade. In both cases, we need to have
+ the storage set, else we'll be in trouble...
+
+ * the path given to the storage is the place where file added through the ui
+ (or in the database before migration) will be located
+
+ * be ware that by doing this, you can't anymore write queries that will try to
+ restrict on File and Image `data` attribute. Hopefuly we don't do that usually
+ on file's content or more generally on attributes for the Bytes type
+
+Now, if you've already added some photos through the web ui, you'll have to
+migrate existing data so file's content will be stored on the file-system instead
+of the database. There is a migration command to do so, let's run it in the
+cubicweb shell (in actual life, you'd have to put it in a migration script as we
+seen last time):
+
+::
+
+ $ cubicweb-ctl shell sytweb
+ entering the migration python shell
+ just type migration commands or arbitrary python code and type ENTER to execute it
+ type "exit" or Ctrl-D to quit the shell and resume operation
+ >>> storage_changed('File', 'data')
+ [........................]
+ >>> storage_changed('Image', 'data')
+ [........................]
+
+
+That's it. Now, file added through the web ui will have their content stored on
+the file-system, and you'll also be able to import files from the file-system as
+explained in the next part.
+
+Step 2: importing some data into the instance
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Hey, we start to have some nice features, let give us a try on this new web
+site. For instance if I have a 'photos/201005WePyrenees' containing pictures for
+a particular event, I can import it to my web site by typing ::
+
+ $ cubicweb-ctl fsimport -F sytweb photos/201005WePyrenees/
+ ** importing directory /home/syt/photos/201005WePyrenees
+ importing IMG_8314.JPG
+ importing IMG_8274.JPG
+ importing IMG_8286.JPG
+ importing IMG_8308.JPG
+ importing IMG_8304.JPG
+
+.. Note::
+ The -F option tell that folders should be mapped, hence my photos will be
+ all under a Folder entity corresponding to the file-system folder.
+
+Let's take a look at the web ui:
+
+.. image:: ../../images/tutos-photowebsite_ui1.png
+
+Nothing different, I can't see the new folder... But remember our security model!
+By default, files are only accessible to authenticated users, and I'm looking at
+the site as anonymous, e.g. not authenticated. If I login, I can now see:
+
+.. image:: ../../images/tutos-photowebsite_ui2.png
+
+Yeah, it's there! You can also notice that I can see some entities as well as
+folders and images the anonymous user can't. It just works **everywhere in the
+ui** since it's handled at the repository level, thanks to our security model.
+
+Now if I click on the newly inserted folder, I can see
+
+.. image:: ../../images/tutos-photowebsite_ui3.png
+
+Great! There is even my pictures in the folder. I can know give to this folder a
+nicer name (provided I don't intend to import from it anymore, else already
+imported photos will be reimported), change permissions, title for some pictures,
+etc... Having a good content is much more difficult than having a good web site
+;)
+
+
+Conclusion
+~~~~~~~~~~
+
+We started to see here an advanced feature of our repository: the ability
+to store some parts of our data-model into a custom storage, outside the
+database. There is currently only the :class:`BytesFileSystemStorage` available,
+but you can expect to see more coming in a near future (our write your own!).
+
+Also, we can know start to feed our web-site with some nice pictures!
+The site isn't perfect (far from it actually) but it's usable, and we can
+now start using it and improve it on the way. The Incremental Cubic Way :)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/tutorials/advanced/part04_ui-base.rst Mon Jan 24 17:02:38 2011 +0100
@@ -0,0 +1,453 @@
+Let's make it more user friendly
+================================
+
+
+Step 0: updating code to CubicWeb 3.9 / cubicweb-file 1.9
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+CubicWeb 3.9 brings `several improvments`_ that we'll want to use, and the 1.9
+version of the file cube has a major change: the `Image` type has been dropped in
+favor of an `IImage` adapter that makes code globally much cleaner (though we wont
+see that much this here). So the first thing to do is to upgrade our cube to the
+3.9 API. As CubicWeb releases are mostly backward compatible, this is not
+mandatory but it's easier to follow change as they come than having a huge
+upgrade to do at some point. Also, this remove deprecation warnings which are a
+bit tedious...
+
+Also, since we've only a few lines of code yet, this is quite easy to upgrade.
+Actually the main thing we've to do is to upgrade our schema, to remove occurences
+of the `Image` type or replace them by the `File` type. Here is the (striped) diff:
+
+.. sourcecode:: diff
+
+ class comments(RelationDefinition):
+ subject = 'Comment'
+ - object = ('File', 'Image')
+ + object = 'File'
+ cardinality = '1*'
+ composite = 'object'
+
+ class tags(RelationDefinition):
+ subject = 'Tag'
+ - object = ('File', 'Image')
+ + object = 'File'
+
+ class displayed_on(RelationDefinition):
+ subject = 'Person'
+ - object = 'Image'
+ + object = 'File'
+
+ class situated_in(RelationDefinition):
+ - subject = 'Image'
+ + subject = 'File'
+ object = 'Zone'
+
+ class filed_under(RelationDefinition):
+ - subject = ('File', 'Image')
+ + subject = 'File'
+ object = 'Folder'
+
+ class visibility(RelationDefinition):
+ - subject = ('Folder', 'File', 'Image', 'Comment')
+ + subject = ('Folder', 'File', 'Comment')
+ object = 'String'
+ constraints = [StaticVocabularyConstraint(('public', 'authenticated',
+ 'restricted', 'parent'))]
+
+ class may_be_readen_by(RelationDefinition):
+ - subject = ('Folder', 'File', 'Image', 'Comment',)
+ + subject = ('Folder', 'File', 'Comment',)
+ object = 'CWUser'
+
+
+ -from cubes.file.schema import File, Image
+ +from cubes.file.schema import File
+
+ File.__permissions__ = VISIBILITY_PERMISSIONS
+ -Image.__permissions__ = VISIBILITY_PERMISSIONS
+
+Now, let's record that we depends on the versions in the __pkginfo__ file. As
+`3.8`_ simplify this file, we can merge `__depends_cubes__` (as introduced if the
+`first blog of this series`_) with `__depends__` to get the following result:
+
+.. sourcecode:: python
+
+ __depends__ = {'cubicweb': '>= 3.9.0',
+ 'cubicweb-file': '>= 1.9.0',
+ 'cubicweb-folder': None,
+ 'cubicweb-person': None,
+ 'cubicweb-zone': None,
+ 'cubicweb-comment': None,
+ 'cubicweb-tag': None,
+ }
+
+If your cube is packaged for debian, it's a good idea to update the
+`debian/control` file at the same time, so you won't forget it.
+
+That's it for the API update, CubicWeb, cubicweb-file will handle other stuff for
+us. Easy, no?
+
+We can now start some more funny stuff...
+
+
+Step 1: let's improve site's usability for our visitors
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The first thing I've noticed is that people to whom I send links to photos with
+some login/password authentication get lost, because they don't grasp they have
+to login by clicking on the 'authenticate' link. That's much probably because
+they only get a 404 when trying to access an unauthorized folder, and the site
+doesn't make clear that 1. you're not authenticated, 2. you could get more
+content by authenticating yourself.
+
+So, to improve this situation, I decided that I should:
+
+* make a login box appears for anonymous, so they see at a first glance a place
+ to put the login / password information I provided
+
+* customize the 404 page, proposing to login to anonymous.
+
+Here is the code, samples from my cube's `views.py` file:
+
+.. sourcecode:: python
+
+ from cubicweb.selectors import is_instance
+ from cubicweb.web import component
+ from cubicweb.web.views import error
+
+ class FourOhFour(error.FourOhFour):
+ __select__ = error.FourOhFour.__select__ & anonymous_user()
+
+ def call(self):
+ self.w(u"<h1>%s</h1>" % self._cw._('this resource does not exist'))
+ self.w(u"<p>%s</p>" % self._cw._('have you tried to login?'))
+
+
+ class LoginBox(component.CtxComponent):
+ """display a box containing links to all startup views"""
+ __regid__ = 'sytweb.loginbox'
+ __select__ = component.CtxComponent.__select__ & anonymous_user()
+
+ title = _('Authenticate yourself')
+ order = 70
+
+ def render_body(self, w):
+ cw = self._cw
+ form = cw.vreg['forms'].select('logform', cw)
+ form.render(w=w, table_class='', display_progress_div=False)
+
+The first class provides a new specific implementation of the default page you
+get on 404 error, to display an adapted message to anonymous user.
+
+.. Note::
+
+ Thanks to the selection mecanism, it will be selected for anoymous user,
+ since the additional `anonymous_user()` selector gives it a higher score than
+ the default, and not for authenticated since this selector will return 0 in
+ such case (hence the object won't be selectable)
+
+The second class defines a simple box, that will be displayed by default with
+boxes in the left column, thanks to default :class:`component.CtxComponent`
+selector. The HTML is written to match default CubicWeb boxes style. The code
+fetch the actual login form and render it.
+
+
+.. figure:: ../../images/tutos-photowebsite_login-box.png
+ :alt: login box / 404 screenshot
+
+ The login box and the custom 404 page for an anonymous visitor (translated in french)
+
+
+Step 2: providing a custom index page
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Another thing we can easily do to improve the site is... A nicer index page
+(e.g. the first page you get when accessing the web site)! The default one is
+quite intimidating (that should change in a near future). I will provide a much
+simpler index page that simply list available folders (e.g. photo albums in that
+site).
+
+.. sourcecode:: python
+
+ from cubicweb.web.views import startup
+
+ class IndexView(startup.IndexView):
+ def call(self, **kwargs):
+ self.w(u'<div>\n')
+ if self._cw.cnx.anonymous_connection:
+ self.w(u'<h4>%s</h4>\n' % self._cw._('Public Albums'))
+ else:
+ self.w(u'<h4>%s</h4>\n' % self._cw._('Albums for %s') % self._cw.user.login)
+ self._cw.vreg['views'].select('tree', self._cw).render(w=self.w)
+ self.w(u'</div>\n')
+
+ def registration_callback(vreg):
+ vreg.register_all(globals().values(), __name__, (IndexView,))
+ vreg.register_and_replace(IndexView, startup.IndexView)
+
+As you can see, we override the default index view found in
+`cubicweb.web.views.startup`, geting back nothing but its identifier and selector
+since we override the top level view's `call` method.
+
+.. Note::
+
+ in that case, we want our index view to **replace** the existing one. To do so
+ we've to implements the `registration_callback` function, in which we tell to
+ register everything in the module *but* our IndexView, then we register it
+ instead of the former index view.
+
+Also, we added a title that tries to make it more evident that the visitor is
+authenticated, or not. Hopefuly people will get it now!
+
+
+.. figure:: ../../images/tutos-photowebsite_index-before.png
+ :alt: default index page screenshot
+
+ The default index page
+
+.. figure:: ../../images/tutos-photowebsite_index-after.png
+ :alt: new index page screenshot
+
+ Our simpler, less intimidating, index page (still translated in french)
+
+
+Step 3: more navigation improvments
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+There are still a few problems I want to solve...
+
+* Images in a folder are displayed in a somewhat random order. I would like to
+ have them ordered by file's name (which will usually, inside a given folder,
+ also result ordering photo by their date and time)
+
+* When clicking a photo from an album view, you've to get back to the gallery
+ view to go to the next photo. This is pretty annoying...
+
+* Also, when viewing an image, there is no clue about the folder to which this
+ image belongs to.
+
+I will first try to explain the ordering problem. By default, when accessing related
+entities by using the ORM's API, you should get them ordered according to the target's
+class `fetch_order`. If we take a look at the file cube'schema, we can see:
+
+.. sourcecode:: python
+
+
+ class File(AnyEntity):
+ """customized class for File entities"""
+ __regid__ = 'File'
+ fetch_attrs, fetch_order = fetch_config(['data_name', 'title'])
+
+By default, `fetch_config` will return a `fetch_order` method that will order on
+the first attribute in the list. So, we could expect to get files ordered by
+their name. But we don't. What's up doc ?
+
+The problem is that files are related to folder using the `filed_under` relation.
+And that relation is ambiguous, eg it can lead to `File` entities, but also to
+`Folder` entities. In such case, since both entity types doesn't share the
+attribute on which we want to sort, we'll get linked entities sorted on a common
+attribute (usually `modification_date`).
+
+To fix this, we've to help the ORM. We'll do this in the method from the `ITree`
+folder's adapter, used in the folder's primary view to display the folder's
+content. Here's the code, that I've put in our cube's `entities.py` file, since
+it's more logical stuff than view stuff:
+
+.. sourcecode:: python
+
+ from cubes.folder import entities as folder
+
+ class FolderITreeAdapter(folder.FolderITreeAdapter):
+
+ def different_type_children(self, entities=True):
+ rql = self.entity.cw_related_rql(self.tree_relation,
+ self.parent_role, ('File',))
+ rset = self._cw.execute(rql, {'x': self.entity.eid})
+ if entities:
+ return list(rset.entities())
+ return rset
+
+ def registration_callback(vreg):
+ vreg.register_and_replace(FolderITreeAdapter, folder.FolderITreeAdapter)
+
+As you can see, we simple inherit from the adapter defined in the `folder` cube,
+then we override the `different_type_children` method to give a clue to the ORM's
+`cw_related_rql` method, that is responsible to generate the rql to get entities
+related to the folder by the `filed_under` relation (the value of the
+`tree_relation` attribute). The clue is that we only want to consider the `File`
+target entity type. By doing this, we remove the ambiguity and get back a RQL
+query that correctly order files by their `data_name` attribute.
+
+
+.. Note::
+
+ * Adapters have been introduced in CubicWeb 3.9 / cubicweb-folder 1.8.
+
+ * As seen earlier, we want to **replace** the folder's `ITree` adapter by our
+ implementation, hence the custom `registration_callback` method.
+
+
+Ouf. That one was tricky...
+
+Now the easier parts. Let's start by adding some links on the file's primary view
+to see the previous / next image in the same folder. CubicWeb's provide a
+component that do exactly that. To make it appears, one have to be adaptable to
+the `IPrevNext` interface. Here is the related code sample, extracted from our
+cube's `views.py` file:
+
+.. sourcecode:: python
+
+ from cubicweb.selectors import is_instance
+ from cubicweb.web.views import navigation
+
+
+ class FileIPrevNextAdapter(navigation.IPrevNextAdapter):
+ __select__ = is_instance('File')
+
+ def previous_entity(self):
+ rset = self._cw.execute('File F ORDERBY FDN DESC LIMIT 1 WHERE '
+ 'X filed_under FOLDER, F filed_under FOLDER, '
+ 'F data_name FDN, X data_name > FDN, X eid %(x)s',
+ {'x': self.entity.eid})
+ if rset:
+ return rset.get_entity(0, 0)
+
+ def next_entity(self):
+ rset = self._cw.execute('File F ORDERBY FDN ASC LIMIT 1 WHERE '
+ 'X filed_under FOLDER, F filed_under FOLDER, '
+ 'F data_name FDN, X data_name < FDN, X eid %(x)s',
+ {'x': self.entity.eid})
+ if rset:
+ return rset.get_entity(0, 0)
+
+
+The `IPrevNext` interface implemented by the adapter simply consist in the
+`previous_entity` / `next_entity` methods, that should respectivly return the
+previous / next entity or `None`. We make an RQL query to get files in the same
+folder, ordered similarly (eg by their `data_name` attribute). We set
+ascendant/descendant ordering and a strict comparison with current file's name
+(the "X" variable representing the current file).
+
+.. Note::
+
+ * Former `implements` selector should be replaced by one of `is_instance` /
+ `adaptable` selector with CubicWeb >= 3.9. In our case, `is_instance` to
+ tell our adapter is able to adapt `File` entities.
+
+Notice that this query supposes we wont have two files of the same name in the
+same folder, else things may go wrong. Fixing this is out of the scope of this
+blog. And as I would like to have at some point a smarter, context sensitive
+previous/next entity, I'll probably never fix this query (though if I had to, I
+would probably choosing to add a constraint in the schema so that we can't add
+two files of the same name in a folder).
+
+One more thing: by default, the component will be displayed below the content
+zone (the one with the white background). You can change this in the site's
+properties through the ui, but you can also change the default value in the code
+by modifying the `context` attribute of the component:
+
+.. sourcecode:: python
+
+ navigation.NextPrevNavigationComponent.context = 'navcontentbottom'
+
+.. Note::
+
+ `context` may be one of 'navtop', 'navbottom', 'navcontenttop' or
+ 'navcontentbottom'; the first two being outside the main content zone, the two
+ others inside it.
+
+.. figure:: ../../images/tutos-photowebsite_prevnext.png
+ :alt: screenshot of the previous/next entity component
+
+ The previous/next entity component, at the bottom of the main content zone.
+
+Now, the only remaining stuff in my todo list is to see the file's folder. I'll use
+the standard breadcrumb component to do so. Similarly as what we've seen before, this
+component is controled by the :class:`IBreadCrumbs` interface, so we'll have to provide a custom
+adapter for `File` entity, telling the a file's parent entity is its folder:
+
+.. sourcecode:: python
+
+ from cubicweb.web.views import ibreadcrumbs
+
+ class FileIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+ __select__ = is_instance('File')
+
+ def parent_entity(self):
+ if self.entity.filed_under:
+ return self.entity.filed_under[0]
+
+In that case, we simply use attribute notation provided by the ORM to get the
+folder in which the current file (e.g. `self.entity`) is located.
+
+.. Note::
+ The :class:`IBreadCrumbs` interface is a `breadcrumbs` method, but the default
+ :class:`IBreadCrumbsAdapter` provides a default implementation for it that will look
+ at the value returned by its `parent_entity` method. It also provides a
+ default implementation for this method for entities adapting to the `ITree`
+ interface, but as our `File` doesn't, we've to provide a custom adapter.
+
+.. figure:: ../../images/tutos-photowebsite_breadcrumbs.png
+ :alt: screenshot of the breadcrumb component
+
+ The breadcrumb component when on a file entity, now displaying parent folder.
+
+
+Step 4: preparing the release and migrating the instance
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Now that greatly enhanced our cube, it's time to release it to upgrade production site.
+I'll probably detail that process later, but I currently simply transfer the new code
+to the server running the web site.
+
+However, I've still today some step to respect to get things done properly...
+
+First, as I've added some translatable string, I've to run: ::
+
+ $ cubicweb-ctl i18ncube sytweb
+
+To update the cube's gettext catalogs (the '.po' files under the cube's `i18n`
+directory). Once the above command is executed, I'll then update translations.
+
+To see if everything is ok on my test instance, I do: ::
+
+ $ cubicweb-ctl i18ninstance sytweb
+ $ cubicweb-ctl start -D sytweb
+
+The first command compile i18n catalogs (e.g. generates '.mo' files) for my test
+instance. The second command start it in debug mode, so I can open my browser and
+navigate through the web site to see if everything is ok...
+
+.. Note::
+ In the 'cubicweb-ctl i18ncube' command, `sytweb` refers to the **cube**, while
+ in the two other, it refers to the **instance** (if you can't see the
+ difference, reread CubicWeb's concept chapter !).
+
+
+Once I've checked it's ok, I simply have to bump the version number in the
+`__pkginfo__` module to trigger a migration once I'll have updated the code on
+the production site. I can check then check the migration is also going fine, by
+first restoring a dump from the production site, then upgrading my test instance.
+
+To generate a dump from the production site: ::
+
+ $ cubicweb-ctl db-dump sytweb
+ pg_dump -Fc --username=syt --no-owner --file /home/syt/etc/cubicweb.d/sytweb/backup/tmpYIN0YI/system sytweb
+ -> backup file /home/syt/etc/cubicweb.d/sytweb/backup/sytweb-2010-07-13_10-22-40.tar.gz
+
+I can now get back the dump file ('sytweb-2010-07-13_10-22-40.tar.gz') to my test
+machine (using `scp` for instance) to restore it and start migration: ::
+
+ $ cubicweb-ctl db-restore sytweb sytweb-2010-07-13_10-22-40.tar.gz
+ $ cubicweb-ctl upgrade sytweb
+
+You'll have to answer some questions, as we've seen in `an earlier post`_.
+
+Now that everything is tested, I can transfer the new code to the production
+server, `apt-get upgrade` cubicweb 3.9 and its dependencies, and eventually
+upgrade the production instance.
+
+
+.. _`several improvments`: http://www.cubicweb.org/blogentry/1179899
+.. _`3.8`: http://www.cubicweb.org/blogentry/917107
+.. _`first blog of this series`: http://www.cubicweb.org/blogentry/824642
+.. _`an earlier post`: http://www.cubicweb.org/867464
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/tutorials/advanced/part05_ui-advanced.rst Mon Jan 24 17:02:38 2011 +0100
@@ -0,0 +1,375 @@
+Building my photos web site with |cubicweb| part V: let's make it even more user friendly
+=========================================================================================
+
+We'll now see how to benefit from features introduced in 3.9 and 3.10 releases of CubicWeb
+
+Step 1: tired of the default look?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+OK... Now our site has its most desired features. But... I would like to make it look
+somewhat like *my* website. It is not www.cubicweb.org after all. Let's tackle this
+first!
+
+The first thing we can to is to change the logo. There are various way to achieve
+this. The easiest way is to put a :file:`logo.png` file into the cube's :file:`data`
+directory. As data files are looked at according to cubes order (CubicWeb
+resources coming last), that file will be selected instead of CubicWeb's one.
+
+.. Note::
+ As the location for static resources are cached, you'll have to restart
+ your instance for this to be taken into account.
+
+Though there are some cases where you don't want to use a :file:`logo.png` file.
+For instance if it's a JPEG file. You can still change the logo by defining in
+the cube's :file:`uiprops.py` file:
+
+.. sourcecode:: python
+
+ LOGO = data('logo.jpg')
+
+The uiprops machinery has been introduced in `CubicWeb 3.9`_. It is used to define
+some static file resources, such as the logo, default Javascript / CSS files, as
+well as CSS properties (we'll see that later).
+
+.. Note::
+ This file is imported specifically by |cubicweb|, with a predefined name space,
+ containing for instance the `data` function, telling the file is somewhere
+ in a cube or CubicWeb's data directory.
+
+ One side effect of this is that it can't be imported as a regular python
+ module.
+
+The nice thing is that in debug mode, change to a :file:`uiprops.py` file are detected
+and then automatically reloaded.
+
+Now, as it's a photos web-site, I would like to have a photo of mine as background...
+After some trials I won't detail here, I've found a working recipe explained `here`_.
+All I've to do is to override some stuff of the default CubicWeb user interface to
+apply it as explained.
+
+The first thing to to get the ``<img/>`` tag as first element after the
+``<body>`` tag. If you know a way to avoid this by simply specifying the image
+in the CSS, tell me! The easiest way to do so is to override the
+:class:`HTMLPageHeader` view, since that's the one that is directly called once
+the ``<body>`` has been written. How did I find this? By looking in the
+:mod:`cubiweb.web.views.basetemplates` module, since I know that global page
+layouts sits there. I could also have grep the "body" tag in
+:mod:`cubicweb.web.views`... Finding this was the hardest part. Now all I need is
+to customize it to write that ``img`` tag, as below:
+
+.. sourcecode:: python
+
+ class HTMLPageHeader(basetemplates.HTMLPageHeader):
+ # override this since it's the easier way to have our bg image
+ # as the first element following <body>
+ def call(self, **kwargs):
+ self.w(u'<img id="bg-image" src="%sbackground.jpg" alt="background image"/>'
+ % self._cw.datadir_url)
+ super(HTMLPageHeader, self).call(**kwargs)
+
+
+ def registration_callback(vreg):
+ vreg.register_all(globals().values(), __name__, (HTMLPageHeader))
+ vreg.register_and_replace(HTMLPageHeader, basetemplates.HTMLPageHeader)
+
+
+As you may have guessed, my background image is in a :file:`background.jpg` file
+in the cube's :file:`data` directory, but there are still some things to explain
+to newcomers here:
+
+* The :meth:`call` method is there the main access point of the view. It's called by
+ the view's :meth:`render` method. It is not the only access point for a view, but
+ this will be detailed later.
+
+* Calling `self.w` writes something to the output stream. Except for binary views
+ (which do not generate text), it *must* be passed an Unicode string.
+
+* The proper way to get a file in :file:`data` directory is to use the `datadir_url`
+ attribute of the incoming request (e.g. `self._cw`).
+
+I won't explain again the :func:`registration_callback` stuff, you should understand it
+now! If not, go back to previous posts in the series :)
+
+Fine. Now all I've to do is to add a bit of CSS to get it to behave nicely (which
+is not the case at all for now). I'll put all this in a :file:`cubes.sytweb.css`
+file, stored as usual in our :file:`data` directory:
+
+.. sourcecode:: css
+
+
+ /* fixed full screen background image
+ * as explained on http://webdesign.about.com/od/css3/f/blfaqbgsize.htm
+ *
+ * syt update: set z-index=0 on the img instead of z-index=1 on div#page & co to
+ * avoid pb with the user actions menu
+ */
+ img#bg-image {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 0;
+ }
+
+ div#page, table#header, div#footer {
+ background: transparent;
+ position: relative;
+ }
+
+ /* add some space around the logo
+ */
+ img#logo {
+ padding: 5px 15px 0px 15px;
+ }
+
+ /* more dark font for metadata to have a chance to see them with the background
+ * image
+ */
+ div.metadata {
+ color: black;
+ }
+
+You can see here stuff explained in the cited page, with only a slight modification
+explained in the comments, plus some additional rules to make things somewhat cleaner:
+
+* a bit of padding around the logo
+
+* darker metadata which appears by default below the content (the white frame in the page)
+
+To get this CSS file used everywhere in the site, I have to modify the :file:`uiprops.py` file
+introduced above:
+
+.. sourcecode:: python
+
+ STYLESHEETS = sheet['STYLESHEETS'] + [data('cubes.sytweb.css')]
+
+.. Note::
+ `sheet` is another predefined variable containing values defined by
+ already process `:file:`uiprops.py`` file, notably the CubicWeb's one.
+
+Here we simply want our CSS in addition to CubicWeb's base CSS files, so we
+redefine the `STYLESHEETS` variable to existing CSS (accessed through the `sheet`
+variable) with our one added. I could also have done:
+
+.. sourcecode:: python
+
+ sheet['STYLESHEETS'].append(data('cubes.sytweb.css'))
+
+But this is less interesting since we don't see the overriding mechanism...
+
+At this point, the site should start looking good, the background image being
+resized to fit the screen.
+
+.. image:: ../../images/tutos-photowebsite_background-image.png
+
+The final touch: let's customize CubicWeb's CSS to get less orange... By simply adding
+
+.. sourcecode:: python
+
+ contextualBoxTitleBg = incontextBoxTitleBg = '#AAAAAA'
+
+and reloading the page we've just seen, we know have a nice greyed box instead of
+the orange one:
+
+.. image:: ../../images/tutos-photowebsite_grey-box.png
+
+This is because CubicWeb's CSS include some variables which are
+expanded by values defined in uiprops file. In our case we controlled the
+properties of the CSS `background` property of boxes with CSS class
+`contextualBoxTitleBg` and `incontextBoxTitleBg`.
+
+
+Step 2: configuring boxes
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Boxes present to the user some ways to use the application. Let's first do a few
+user interface tweaks in our :file:`views.py` file:
+
+.. sourcecode:: python
+
+ from cubicweb.selectors import none_rset
+ from cubicweb.web.views import bookmark
+ from cubes.zone import views as zone
+ from cubes.tag import views as tag
+
+ # change bookmarks box selector so it's only displayed on startup views
+ bookmark.BookmarksBox.__select__ = bookmark.BookmarksBox.__select__ & none_rset()
+ # move zone box to the left instead of in the context frame and tweak its order
+ zone.ZoneBox.context = 'left'
+ zone.ZoneBox.order = 100
+ # move tags box to the left instead of in the context frame and tweak its order
+ tag.TagsBox.context = 'left'
+ tag.TagsBox.order = 102
+ # hide similarity box, not interested
+ tag.SimilarityBox.visible = False
+
+The idea is to move all boxes in the left column, so we get more space for the
+photos. Now, serious things: I want a box similar to the tags box but to handle
+the `Person displayed_on File` relation. We can do this simply by adding a
+:class:`AjaxEditRelationCtxComponent` subclass to our views, as below:
+
+.. sourcecode:: python
+
+ from logilab.common.decorators import monkeypatch
+ from cubicweb import ValidationError
+ from cubicweb.web import uicfg, component
+ from cubicweb.web.views import basecontrollers
+
+ # hide displayed_on relation using uicfg since it will be displayed by the box below
+ uicfg.primaryview_section.tag_object_of(('*', 'displayed_on', '*'), 'hidden')
+
+ class PersonBox(component.AjaxEditRelationCtxComponent):
+ __regid__ = 'sytweb.displayed-on-box'
+ # box position
+ order = 101
+ context = 'left'
+ # define relation to be handled
+ rtype = 'displayed_on'
+ role = 'object'
+ target_etype = 'Person'
+ # messages
+ added_msg = _('person has been added')
+ removed_msg = _('person has been removed')
+ # bind to js_* methods of the json controller
+ fname_vocabulary = 'unrelated_persons'
+ fname_validate = 'link_to_person'
+ fname_remove = 'unlink_person'
+
+
+ @monkeypatch(basecontrollers.JSonController)
+ @basecontrollers.jsonize
+ def js_unrelated_persons(self, eid):
+ """return tag unrelated to an entity"""
+ rql = "Any F + ' ' + S WHERE P surname S, P firstname F, X eid %(x)s, NOT P displayed_on X"
+ return [name for (name,) in self._cw.execute(rql, {'x' : eid})]
+
+
+ @monkeypatch(basecontrollers.JSonController)
+ def js_link_to_person(self, eid, people):
+ req = self._cw
+ for name in people:
+ name = name.strip().title()
+ if not name:
+ continue
+ try:
+ firstname, surname = name.split(None, 1)
+ except:
+ raise ValidationError(eid, {('displayed_on', 'object'): 'provide <first name> <surname>'})
+ rset = req.execute('Person P WHERE '
+ 'P firstname %(firstname)s, P surname %(surname)s',
+ locals())
+ if rset:
+ person = rset.get_entity(0, 0)
+ else:
+ person = req.create_entity('Person', firstname=firstname,
+ surname=surname)
+ req.execute('SET P displayed_on X WHERE '
+ 'P eid %(p)s, X eid %(x)s, NOT P displayed_on X',
+ {'p': person.eid, 'x' : eid})
+
+ @monkeypatch(basecontrollers.JSonController)
+ def js_unlink_person(self, eid, personeid):
+ self._cw.execute('DELETE P displayed_on X WHERE P eid %(p)s, X eid %(x)s',
+ {'p': personeid, 'x': eid})
+
+
+You basically subclass to configure with some class attributes. The `fname_*`
+attributes give the name of methods that should be defined on the json control to
+make the AJAX part of the widget work: one to get the vocabulary, one to add a
+relation and another to delete a relation. These methods must start by a `js_`
+prefix and are added to the controller using the `@monkeypatch` decorator. In my
+case, the most complicated method is the one which adds a relation, since it
+tries to see if the person already exists, and else automatically create it,
+assuming the user entered "firstname surname".
+
+Let's see how it looks like on a file primary view:
+
+.. image:: ../../images/tutos-photowebsite_boxes.png
+
+Great, it's now as easy for me to link my pictures to people than to tag them.
+Also, visitors get a consistent display of these two pieces of information.
+
+.. Note::
+ The ui component system has been refactored in `CubicWeb 3.10`_, which also
+ introduced the :class:`AjaxEditRelationCtxComponent` class.
+
+
+Step 3: configuring facets
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The last feature we'll add today is facet configuration. If you access to the
+'/file' url, you'll see a set of 'facets' appearing in the left column. Facets
+provide an intuitive way to build a query incrementally, by proposing to the user
+various way to restrict the result set. For instance CubicWeb proposes a facet to
+restrict based on who created an entity; the tag cube proposes a facet to
+restrict based on tags; the zoe cube a facet to restrict based on geographical
+location, and so on. In that gist, I want to propose a facet to restrict based on
+the people displayed on the picture. To do so, there are various classes in the
+:mod:`cubicweb.web.facet` module which simply have to be configured using class
+attributes as we've done for the box. In our case, we'll define a subclass of
+:class:`RelationFacet`.
+
+.. Note::
+
+ Since that's ui stuff, we'll continue to add code below to our
+ :file:`views.py` file. Though we begin to have a lot of various code their, so
+ it's may be a good time to split our views module into submodules of a `view`
+ package. In our case of a simple application (glue) cube, we could start using
+ for instance the layout below: ::
+
+ views/__init__.py # uicfg configuration, facets
+ views/layout.py # header/footer/background stuff
+ views/components.py # boxes, adapters
+ views/pages.py # index view, 404 view
+
+.. sourcecode:: python
+
+ from cubicweb.web import facet
+
+ class DisplayedOnFacet(facet.RelationFacet):
+ __regid__ = 'displayed_on-facet'
+ # relation to be displayed
+ rtype = 'displayed_on'
+ role = 'object'
+ # view to use to display persons
+ label_vid = 'combobox'
+
+Let's say we also want to filter according to the `visibility` attribute. This is
+even simpler as we just have to derive from the :class:`AttributeFacet` class:
+
+.. sourcecode:: python
+
+ class VisibilityFacet(facet.AttributeFacet):
+ __regid__ = 'visibility-facet'
+ rtype = 'visibility'
+
+Now if I search for some pictures on my site, I get the following facets available:
+
+.. image:: ../../images/tutos-photowebsite_facets.png
+
+.. Note::
+
+ By default a facet must be applyable to every entity in the result set and
+ provide at leat two elements of vocabulary to be displayed (for instance you
+ won't see the `created_by` facet if the same user has created all
+ entities). This may explain why you don't see yours...
+
+
+Conclusion
+~~~~~~~~~~
+
+We started to see the power behind the infrastructure provided by the
+framework, both on the pure ui (CSS, Javascript) side and on the Python side
+(high level generic classes for components, including boxes and facets). We now
+have, with a few lines of code, a full-featured web site with a personalized look.
+
+Of course we'll probably want more as time goes, but we can now
+concentrate on making good pictures, publishing albums and sharing them with
+friends...
+
+
+
+.. _`CubicWeb 3.10`: http://www.cubicweb.org/blogentry/1330518
+.. _`CubicWeb 3.9`: http://www.cubicweb.org/blogentry/1179899
+.. _`here`: http://webdesign.about.com/od/css3/f/blfaqbgsize.htm
--- a/doc/book/en/tutorials/base/customizing-the-application.rst Thu Jan 20 10:10:22 2011 +0100
+++ b/doc/book/en/tutorials/base/customizing-the-application.rst Mon Jan 24 17:02:38 2011 +0100
@@ -41,7 +41,7 @@
cubicweb-ctl newcube --directory=~/src/cubes myblog
-.. Note:
+.. Note::
We previously used `myblog` as the name of our *instance*. We're now creating
a *cube* with the same name. Both are different things. We'll now try to
@@ -63,6 +63,7 @@
where the ``None`` means we do not depends on a particular version of the cube.
+.. _TutosBaseCustomizingTheApplicationDataModel:
Extending the data model
~~~~~~~~~~~~~~~~~~~~~~~~
@@ -179,6 +180,8 @@
You'll then be able to redefine each of them according to your needs
and preferences. We'll now see how to do such thing.
+.. _TutosBaseCustomizingTheApplicationCustomViews:
+
Defining your views
~~~~~~~~~~~~~~~~~~~
--- a/doc/book/en/tutorials/base/index.rst Thu Jan 20 10:10:22 2011 +0100
+++ b/doc/book/en/tutorials/base/index.rst Mon Jan 24 17:02:38 2011 +0100
@@ -16,7 +16,7 @@
* discovering the default user interface
* basic extending and customizing the look and feel of that application
-More advanced concepts are covered in :ref:`advanced_tutorial`.
+More advanced concepts are covered in :ref:`TutosPhotoWebSite`.
.. _TutosBaseVocab:
--- a/doc/book/en/tutorials/tools/windmill.rst Thu Jan 20 10:10:22 2011 +0100
+++ b/doc/book/en/tutorials/tools/windmill.rst Mon Jan 24 17:02:38 2011 +0100
@@ -150,7 +150,7 @@
If you want to change cubicweb test server parameters, you can check class
variables from :class:`CubicWebServerConfig` or inherit it with overriding the
-:var:`configcls` attribute in :class:`CubicWebServerTC` ::
+:attr:`configcls` attribute in :class:`CubicWebServerTC` ::
.. sourcecode:: python
--- a/doc/tools/pyjsrest.py Thu Jan 20 10:10:22 2011 +0100
+++ b/doc/tools/pyjsrest.py Mon Jan 24 17:02:38 2011 +0100
@@ -4,6 +4,7 @@
"""
from __future__ import with_statement
+import os.path as osp
import sys, os, getopt, re
def clean_comment(match):
@@ -71,34 +72,48 @@
if rst_dir is None and len(args) != 1:
rst_dir = 'apidocs'
js_dir = opts.get('--jspath') or opts.get('-p')
- if not os.path.exists(os.path.join(rst_dir)):
- os.makedirs(os.path.join(rst_dir))
+ if not osp.exists(osp.join(rst_dir)):
+ os.makedirs(osp.join(rst_dir))
- f_index = open(os.path.join(rst_dir, 'index.rst'), 'wb')
- f_index.write('''
+ index = set()
+ for js_path, js_dirs, js_files in os.walk(js_dir):
+ rst_path = re.sub('%s%s*' % (js_dir, osp.sep), '', js_path)
+ for js_file in js_files:
+ if not js_file.endswith('.js'):
+ continue
+ if js_file in FILES_TO_IGNORE:
+ continue
+ if not osp.exists(osp.join(rst_dir, rst_path)):
+ os.makedirs(osp.join(rst_dir, rst_path))
+ rst_content = extract_rest(js_path, js_file)
+ filename = osp.join(rst_path, js_file[:-3])
+ # add to index
+ index.add(filename)
+ # save rst file
+ with open(osp.join(rst_dir, filename) + '.rst', 'wb') as f_rst:
+ f_rst.write(rst_content)
+ stream = open(osp.join(rst_dir, 'index.rst'), 'w')
+ stream.write('''
.. toctree::
:maxdepth: 1
-'''
-)
- for js_path, js_dirs, js_files in os.walk(js_dir):
- rst_path = re.sub('%s%s*' % (js_dir, os.path.sep), '', js_path)
- for js_file in js_files:
- if not js_file.endswith('.js'):
- continue
- if not os.path.exists(os.path.join(rst_dir, rst_path)):
- os.makedirs(os.path.join(rst_dir, rst_path))
- rst_content = extract_rest(js_path, js_file)
- filename = os.path.join(rst_path, js_file[:-3])
- # add to index
- f_index.write(' %s\n' % filename)
- # save rst file
- with open(os.path.join(rst_dir, filename) + '.rst', 'wb') as f_rst:
- f_rst.write(rst_content)
- f_index.close()
+''')
+ # first write expected files in order
+ for fileid in INDEX_IN_ORDER:
+ try:
+ index.remove(fileid)
+ except:
+ raise Exception(
+ 'Bad file id %s referenced in INDEX_IN_ORDER in %s, '
+ 'fix this please' % (fileid, __file__))
+ stream.write(' %s\n' % fileid)
+ # append remaining, by alphabetical order
+ for fileid in sorted(index):
+ stream.write(' %s\n' % fileid)
+ stream.close()
def extract_rest(js_dir, js_file):
- js_filepath = os.path.join(js_dir, js_file)
+ js_filepath = osp.join(js_dir, js_file)
filecontent = open(js_filepath, 'U').read()
comments = get_doc_comments(filecontent)
rst = rest_title(js_file, 0)
@@ -106,5 +121,50 @@
rst += '\n\n'.join(comments)
return rst
+INDEX_IN_ORDER = [
+ 'cubicweb',
+ 'cubicweb.python',
+ 'cubicweb.htmlhelpers',
+ 'cubicweb.ajax',
+
+ 'cubicweb.lazy',
+ 'cubicweb.tabs',
+ 'cubicweb.ajax.box',
+ 'cubicweb.facets',
+ 'cubicweb.widgets',
+ 'cubicweb.image',
+ 'cubicweb.flot',
+ 'cubicweb.calendar',
+ 'cubicweb.preferences',
+ 'cubicweb.edition',
+ 'cubicweb.reledit',
+ 'cubicweb.iprogress',
+ 'cubicweb.rhythm',
+ 'cubicweb.gmap',
+ 'cubicweb.timeline-ext',
+]
+
+FILES_TO_IGNORE = set([
+ 'jquery.js',
+ 'jquery.treeview.js',
+ 'jquery.json.js',
+ 'jquery.tablesorter.js',
+ 'jquery.timePicker.js',
+ 'jquery.flot.js',
+ 'jquery.corner.js',
+ 'jquery.ui.js',
+ 'ui.core.js',
+ 'ui.tabs.js',
+ 'ui.slider.js',
+ 'excanvas.js',
+ 'gmap.utility.labeledmarker.js',
+
+ 'cubicweb.fckcwconfig.js',
+ 'cubicweb.fckcwconfig-full.js',
+ 'cubicweb.goa.js',
+ 'cubicweb.compat.js',
+ 'cubicweb.timeline-bundle.js',
+ ])
+
if __name__ == '__main__':
parse_js_files()
--- a/entities/adapters.py Thu Jan 20 10:10:22 2011 +0100
+++ b/entities/adapters.py Mon Jan 24 17:02:38 2011 +0100
@@ -68,6 +68,7 @@
class INotifiableAdapter(EntityAdapter):
+ __needs_bw_compat__ = True
__regid__ = 'INotifiable'
__select__ = is_instance('Any')
@@ -157,6 +158,7 @@
class IDownloadableAdapter(EntityAdapter):
"""interface for downloadable entities"""
+ __needs_bw_compat__ = True
__regid__ = 'IDownloadable'
__select__ = implements(IDownloadable, warn=False) # XXX for bw compat, else should be abstract
@@ -208,6 +210,7 @@
.. automethod: children_rql
.. automethod: path
"""
+ __needs_bw_compat__ = True
__regid__ = 'ITree'
__select__ = implements(ITree, warn=False) # XXX for bw compat, else should be abstract
@@ -335,8 +338,8 @@
for entity in child.cw_adapt_to('ITree').prefixiter(_done):
yield entity
+ @implements_adapter_compat('ITree')
@cached
- @implements_adapter_compat('ITree')
def path(self):
"""Returns the list of eids from the root object to this object."""
path = []
@@ -366,6 +369,7 @@
You should at least override progress_info an in_progress methods on concret
implementations.
"""
+ __needs_bw_compat__ = True
__regid__ = 'IProgress'
__select__ = implements(IProgress, warn=False) # XXX for bw compat, should be abstract
@@ -434,6 +438,7 @@
class IMileStoneAdapter(IProgressAdapter):
+ __needs_bw_compat__ = True
__regid__ = 'IMileStone'
__select__ = implements(IMileStone, warn=False) # XXX for bw compat, should be abstract
--- a/entities/test/unittest_wfobjs.py Thu Jan 20 10:10:22 2011 +0100
+++ b/entities/test/unittest_wfobjs.py Mon Jan 24 17:02:38 2011 +0100
@@ -18,20 +18,16 @@
from __future__ import with_statement
+from cubicweb import ValidationError
from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb import ValidationError
from cubicweb.server.session import security_enabled
+
def add_wf(self, etype, name=None, default=False):
if name is None:
name = etype
- wf = self.execute('INSERT Workflow X: X name %(n)s', {'n': unicode(name)}).get_entity(0, 0)
- self.execute('SET WF workflow_of ET WHERE WF eid %(wf)s, ET name %(et)s',
- {'wf': wf.eid, 'et': etype})
- if default:
- self.execute('SET ET default_workflow WF WHERE WF eid %(wf)s, ET name %(et)s',
- {'wf': wf.eid, 'et': etype})
- return wf
+ return self.shell().add_workflow(name, etype, default=default,
+ ensure_workflowable=False)
def parse_hist(wfhist):
return [(ti.previous_state.name, ti.new_state.name,
--- a/etwist/twconfig.py Thu Jan 20 10:10:22 2011 +0100
+++ b/etwist/twconfig.py Mon Jan 24 17:02:38 2011 +0100
@@ -23,7 +23,6 @@
* the "all-in-one" configuration to get a web instance running in a twisted
web server integrating a repository server in the same process (only available
if the repository part of the software is installed
-
"""
__docformat__ = "restructuredtext en"
@@ -31,8 +30,10 @@
from logilab.common.configuration import Method
+from cubicweb.cwconfig import CONFIGURATIONS
from cubicweb.web.webconfig import WebConfiguration, merge_options
+
class TwistedConfiguration(WebConfiguration):
"""web instance (in a twisted web server) client of a RQL server"""
name = 'twisted'
@@ -98,6 +99,9 @@
from socket import gethostname
return 'http://%s:%s/' % (self['host'] or gethostname(), self['port'] or 8080)
+
+CONFIGURATIONS.append(TwistedConfiguration)
+
try:
from cubicweb.server.serverconfig import ServerConfiguration
@@ -114,5 +118,8 @@
"""tell if pyro is activated for the in memory repository"""
return self['pyro-server']
+
+ CONFIGURATIONS.append(AllInOneConfiguration)
+
except ImportError:
pass
--- a/i18n/de.po Thu Jan 20 10:10:22 2011 +0100
+++ b/i18n/de.po Mon Jan 24 17:02:38 2011 +0100
@@ -1360,6 +1360,9 @@
msgid "click here to see created entity"
msgstr "Hier klicken, um die angelegte Entität anzusehen"
+msgid "click here to see edited entity"
+msgstr ""
+
msgid "click on the box to cancel the deletion"
msgstr "Klicken Sie die Box an, um das Löschen rückgängig zu machen."
@@ -3405,13 +3408,9 @@
msgid "relations deleted"
msgstr "Relationen entfernt"
-msgctxt "CWAttribute"
+msgctxt "CWRType"
msgid "relations_object"
-msgstr "eingeschränkt durch"
-
-msgctxt "CWRelation"
-msgid "relations_object"
-msgstr "eingeschränkt durch"
+msgstr "Relationen von"
msgid "relations_object"
msgstr "Relationen von"
@@ -4347,12 +4346,3 @@
msgid "you should probably delete that property"
msgstr "Sie sollten diese Eigenschaft wahrscheinlich löschen."
-
-#~ msgid "ctxcomponents_loggeduserlink"
-#~ msgstr "Nutzer-Link"
-
-#~ msgid "ctxcomponents_loggeduserlink_description"
-#~ msgstr ""
-#~ "for anonymous users, this is a link pointing to authentication form, for "
-#~ "logged in users, this is a link that makes a box appear and listing some "
-#~ "possible user actions"
--- a/i18n/en.po Thu Jan 20 10:10:22 2011 +0100
+++ b/i18n/en.po Mon Jan 24 17:02:38 2011 +0100
@@ -1312,6 +1312,9 @@
msgid "click here to see created entity"
msgstr ""
+msgid "click here to see edited entity"
+msgstr ""
+
msgid "click on the box to cancel the deletion"
msgstr ""
--- a/i18n/es.po Thu Jan 20 10:10:22 2011 +0100
+++ b/i18n/es.po Mon Jan 24 17:02:38 2011 +0100
@@ -1360,6 +1360,9 @@
msgid "click here to see created entity"
msgstr "Ver la entidad creada"
+msgid "click here to see edited entity"
+msgstr ""
+
msgid "click on the box to cancel the deletion"
msgstr "Seleccione la zona de edición para cancelar la eliminación"
@@ -3432,10 +3435,7 @@
msgid "relations_object"
msgstr ""
-msgctxt "CWRelation"
-msgid "relations_object"
-msgstr ""
-
+msgctxt "CWRType"
msgid "relations_object"
msgstr ""
--- a/i18n/fr.po Thu Jan 20 10:10:22 2011 +0100
+++ b/i18n/fr.po Mon Jan 24 17:02:38 2011 +0100
@@ -1362,6 +1362,9 @@
msgid "click here to see created entity"
msgstr "cliquez ici pour voir l'entité créée"
+msgid "click here to see edited entity"
+msgstr "cliquez ici pour voir l'entité modifiée"
+
msgid "click on the box to cancel the deletion"
msgstr "cliquez dans la zone d'édition pour annuler la suppression"
@@ -3433,13 +3436,9 @@
msgid "relations deleted"
msgstr "relations supprimées"
-msgctxt "CWAttribute"
+msgctxt "CWRType"
msgid "relations_object"
-msgstr "contraint par"
-
-msgctxt "CWRelation"
-msgid "relations_object"
-msgstr "contraint par"
+msgstr "relations de"
msgid "relations_object"
msgstr "relations de"
--- a/misc/migration/3.10.7_Any.py Thu Jan 20 10:10:22 2011 +0100
+++ b/misc/migration/3.10.7_Any.py Mon Jan 24 17:02:38 2011 +0100
@@ -1,8 +1,9 @@
-add_relation_definition('CWUniqueTogetherConstraint', 'relations', 'CWRType')
-rql('SET C relations RT WHERE C relations RDEF, RDEF relation_type RT')
-commit()
-drop_relation_definition('CWUniqueTogetherConstraint', 'relations', 'CWAttribute')
-drop_relation_definition('CWUniqueTogetherConstraint', 'relations', 'CWRelation')
+if not ('CWUniqueTogetherConstraint', 'CWRType') in schema['relations'].rdefs:
+ add_relation_definition('CWUniqueTogetherConstraint', 'relations', 'CWRType')
+ rql('SET C relations RT WHERE C relations RDEF, RDEF relation_type RT')
+ commit()
+ drop_relation_definition('CWUniqueTogetherConstraint', 'relations', 'CWAttribute')
+ drop_relation_definition('CWUniqueTogetherConstraint', 'relations', 'CWRelation')
add_attribute('TrInfo', 'tr_count')
sync_schema_props_perms('TrInfo')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.10.8_Any.py Mon Jan 24 17:02:38 2011 +0100
@@ -0,0 +1,1 @@
+sync_schema_props_perms('CWSource', syncprops=False)
--- a/rset.py Thu Jan 20 10:10:22 2011 +0100
+++ b/rset.py Mon Jan 24 17:02:38 2011 +0100
@@ -386,6 +386,19 @@
if self.rows[i][col] is not None:
yield self.get_entity(i, col)
+ def iter_rows_with_entities(self):
+ """ iterates over rows, and for each row
+ eids are converted to plain entities
+ """
+ for i, row in enumerate(self):
+ _row = []
+ for j, col in enumerate(row):
+ try:
+ _row.append(self.get_entity(i, j) if col is not None else col)
+ except NotAnEntity:
+ _row.append(col)
+ yield _row
+
def complete_entity(self, row, col=0, skip_bytes=True):
"""short cut to get an completed entity instance for a particular
row (all instance's attributes have been fetched)
@@ -401,9 +414,9 @@
.. warning::
- Due to the cache wrapping this function, you should NEVER
- give row as a named parameter (i.e. rset.get_entity(req, 0)
- is OK but rset.get_entity(row=0, req=req) isn't)
+ Due to the cache wrapping this function, you should NEVER give row as
+ a named parameter (i.e. `rset.get_entity(0, 1)` is OK but
+ `rset.get_entity(row=0, col=1)` isn't)
:type row,col: int, int
:param row,col:
@@ -421,11 +434,11 @@
return self._build_entity(row, col)
def _build_entity(self, row, col):
- """internal method to get a single entity, returns a
- partially initialized Entity instance.
+ """internal method to get a single entity, returns a partially
+ initialized Entity instance.
- partially means that only attributes selected in the RQL
- query will be directly assigned to the entity.
+ partially means that only attributes selected in the RQL query will be
+ directly assigned to the entity.
:type row,col: int, int
:param row,col:
--- a/schema.py Thu Jan 20 10:10:22 2011 +0100
+++ b/schema.py Mon Jan 24 17:02:38 2011 +0100
@@ -550,7 +550,11 @@
def add_entity_type(self, edef):
edef.name = edef.name.encode()
edef.name = bw_normalize_etype(edef.name)
- assert re.match(r'[A-Z][A-Za-z0-9]*[a-z]+[0-9]*$', edef.name), repr(edef.name)
+ if not re.match(r'[A-Z][A-Za-z0-9]*[a-z]+[0-9]*$', edef.name):
+ raise BadSchemaDefinition(
+ '%r is not a valid name for an entity type. It should start '
+ 'with an upper cased letter and be followed by at least a '
+ 'lower cased letter' % edef.name)
eschema = super(CubicWebSchema, self).add_entity_type(edef)
if not eschema.final:
# automatically add the eid relation to non final entity types
@@ -565,7 +569,11 @@
return eschema
def add_relation_type(self, rdef):
- rdef.name = rdef.name.lower().encode()
+ if not rdef.name.islower():
+ raise BadSchemaDefinition(
+ '%r is not a valid name for a relation type. It should be '
+ 'lower cased' % rdef.name)
+ rdef.name = rdef.name.encode()
rschema = super(CubicWebSchema, self).add_relation_type(rdef)
self._eid_index[rschema.eid] = rschema
return rschema
--- a/schemas/base.py Thu Jan 20 10:10:22 2011 +0100
+++ b/schemas/base.py Mon Jan 24 17:02:38 2011 +0100
@@ -242,6 +242,12 @@
class CWSource(EntityType):
+ __permissions__ = {
+ 'read': ('managers', 'users', 'guests'),
+ 'add': ('managers',),
+ 'update': ('managers',),
+ 'delete': ('managers',),
+ }
name = String(required=True, unique=True, maxsize=128,
description=_('name of the source'))
type = String(required=True, maxsize=20, description=_('type of the source'))
--- a/selectors.py Thu Jan 20 10:10:22 2011 +0100
+++ b/selectors.py Mon Jan 24 17:02:38 2011 +0100
@@ -817,6 +817,9 @@
This is a very useful selector that will usually interest you since it
allows a lot of things without having to write a specific selector.
+ The function can return arbitrary value which will be casted to an integer
+ value at the end.
+
See :class:`~cubicweb.selectors.EntitySelector` documentation for entity
lookup / score rules according to the input context.
"""
@@ -1142,6 +1145,11 @@
must use 'X' variable to represent the context entity and may use 'U' to
represent the request's user.
+ .. warning::
+ If simply testing value of some attribute/relation of context entity (X),
+ you should rather use the :class:`score_entity` selector which will
+ benefit from the ORM's request entities cache.
+
See :class:`~cubicweb.selectors.EntitySelector` documentation for entity
lookup / score rules according to the input context.
"""
@@ -1153,8 +1161,8 @@
rql = 'Any COUNT(X) WHERE X eid %%(x)s, %s' % expression
self.rql = rql
- def __repr__(self):
- return u'<rql_condition "%s" at %x>' % (self.rql, id(self))
+ def __str__(self):
+ return '%s(%r)' % (self.__class__.__name__, self.rql)
def score(self, req, rset, row, col):
try:
@@ -1430,6 +1438,10 @@
@lltrace
def __call__(self, cls, req, transition=None, **kwargs):
# XXX check this is a transition that apply to the object?
+ if transition is None:
+ treid = req.form.get('treid', None)
+ if treid:
+ transition = req.entity_from_eid(treid)
if transition is not None and getattr(transition, 'name', None) in self.expected:
return 1
return 0
--- a/server/hook.py Thu Jan 20 10:10:22 2011 +0100
+++ b/server/hook.py Mon Jan 24 17:02:38 2011 +0100
@@ -212,7 +212,7 @@
* ``integrity``, data integrity checking hooks
-* ``activeintegrity``, data integrity consistency hooks, that you should *never*
+* ``activeintegrity``, data integrity consistency hooks, that you should **never**
want to disable
* ``syncsession``, hooks synchronizing existing sessions
--- a/server/serverconfig.py Thu Jan 20 10:10:22 2011 +0100
+++ b/server/serverconfig.py Mon Jan 24 17:02:38 2011 +0100
@@ -27,7 +27,7 @@
from logilab.common.decorators import wproperty, cached
from cubicweb.toolsutils import read_config, restrict_perms_to_user
-from cubicweb.cwconfig import CubicWebConfiguration, merge_options
+from cubicweb.cwconfig import CONFIGURATIONS, CubicWebConfiguration, merge_options
from cubicweb.server import SOURCE_TYPES
@@ -346,3 +346,6 @@
return ServerMigrationHelper(self, schema, interactive=interactive,
cnx=cnx, repo=repo, connect=connect,
verbosity=verbosity)
+
+
+CONFIGURATIONS.append(ServerConfiguration)
--- a/server/serverctl.py Thu Jan 20 10:10:22 2011 +0100
+++ b/server/serverctl.py Mon Jan 24 17:02:38 2011 +0100
@@ -25,6 +25,7 @@
import sys
import os
+from logilab.common import nullobject
from logilab.common.configuration import Configuration
from logilab.common.shellutils import ASK
@@ -56,16 +57,14 @@
else:
print dbname,
if dbhelper.users_support:
- if not verbose or (not special_privs and source.get('db-user')):
+ if not special_privs and source.get('db-user'):
user = source['db-user']
if verbose:
print 'as', user
- if source.get('db-password'):
- password = source['db-password']
- else:
- password = getpass('password: ')
+ password = source.get('db-password')
else:
- print
+ if verbose:
+ print
if special_privs:
print 'WARNING'
print ('the user will need the following special access rights '
@@ -74,8 +73,8 @@
print
default_user = source.get('db-user', os.environ.get('USER', ''))
user = raw_input('Connect as user ? [%r]: ' % default_user)
- user = user or default_user
- if user == source.get('db-user') and source.get('db-password'):
+ user = user.strip() or default_user
+ if user == source.get('db-user'):
password = source['db-password']
else:
password = getpass('password: ')
@@ -108,22 +107,18 @@
return source_cnx(source, system_db, special_privs=special_privs, verbose=verbose)
return source_cnx(source, special_privs=special_privs, verbose=verbose)
-def _db_sys_cnx(source, what, db=None, user=None, verbose=True):
- """return a connection on the RDMS system table (to create/drop a user
- or a database
+def _db_sys_cnx(source, special_privs, verbose=True):
+ """return a connection on the RDMS system table (to create/drop a user or a
+ database)
"""
import logilab.common as lgp
from logilab.database import get_db_helper
lgp.USE_MX_DATETIME = False
- special_privs = ''
driver = source['db-driver']
helper = get_db_helper(driver)
- if user is not None and helper.users_support:
- special_privs += '%s USER' % what
- if db is not None:
- special_privs += ' %s DATABASE' % what
# connect on the dbms system base to create our base
- cnx = system_source_cnx(source, True, special_privs=special_privs, verbose=verbose)
+ cnx = system_source_cnx(source, True, special_privs=special_privs,
+ verbose=verbose)
# disable autocommit (isolation_level(1)) because DROP and
# CREATE DATABASE can't be executed in a transaction
try:
@@ -194,6 +189,16 @@
print ('-> nevermind, you can do it later with '
'"cubicweb-ctl db-create %s".' % self.config.appid)
+ERROR = nullobject()
+
+def confirm_on_error_or_die(msg, func, *args, **kwargs):
+ try:
+ return func(*args, **kwargs)
+ except Exception, ex:
+ print 'ERROR', ex
+ if not ASK.confirm('An error occurred while %s. Continue anyway?' % msg):
+ raise ExecutionError(str(ex))
+ return ERROR
class RepositoryDeleteHandler(CommandHandler):
cmdname = 'delete'
@@ -207,19 +212,29 @@
helper = get_db_helper(source['db-driver'])
if ASK.confirm('Delete database %s ?' % dbname):
if source['db-driver'] == 'sqlite':
- os.unlink(source['db-name'])
+ if confirm_on_error_or_die(
+ 'deleting database file %s' % dbname,
+ os.unlink, source['db-name']) is not ERROR:
+ print '-> database %s dropped.' % dbname
return
user = source['db-user'] or None
- cnx = _db_sys_cnx(source, 'DROP DATABASE', user=user)
+ cnx = confirm_on_error_or_die('connecting to database %s' % dbname,
+ _db_sys_cnx, source, 'DROP DATABASE', user=user)
+ if cnx is ERROR:
+ return
cursor = cnx.cursor()
try:
- cursor.execute('DROP DATABASE %s' % dbname)
- print '-> database %s dropped.' % dbname
+ if confirm_on_error_or_die(
+ 'dropping database %s' % dbname,
+ cursor.execute, 'DROP DATABASE "%s"' % dbname) is not ERROR:
+ print '-> database %s dropped.' % dbname
# XXX should check we are not connected as user
if user and helper.users_support and \
ASK.confirm('Delete user %s ?' % user, default_is_yes=False):
- cursor.execute('DROP USER %s' % user)
- print '-> user %s dropped.' % user
+ if confirm_on_error_or_die(
+ 'dropping user %s' % user,
+ cursor.execute, 'DROP USER %s' % user) is not ERROR:
+ print '-> user %s dropped.' % user
cnx.commit()
except:
cnx.rollback()
@@ -313,7 +328,7 @@
elif self.config.create_db:
print '\n'+underline_title('Creating the system database')
# connect on the dbms system base to create our base
- dbcnx = _db_sys_cnx(source, 'CREATE DATABASE and / or USER', verbose=verbose)
+ dbcnx = _db_sys_cnx(source, 'CREATE/DROP DATABASE and / or USER', verbose=verbose)
cursor = dbcnx.cursor()
try:
if helper.users_support:
@@ -333,7 +348,8 @@
except:
dbcnx.rollback()
raise
- cnx = system_source_cnx(source, special_privs='LANGUAGE C', verbose=verbose)
+ cnx = system_source_cnx(source, special_privs='CREATE LANGUAGE',
+ verbose=verbose)
cursor = cnx.cursor()
helper.init_fti_extensions(cursor)
# postgres specific stuff
--- a/server/sources/native.py Thu Jan 20 10:10:22 2011 +0100
+++ b/server/sources/native.py Mon Jan 24 17:02:38 2011 +0100
@@ -849,8 +849,8 @@
if self._eid_creation_cnx is None:
self._eid_creation_cnx = self.get_connection()
cnx = self._eid_creation_cnx
- cursor = cnx.cursor()
try:
+ cursor = cnx.cursor()
for sql in self.dbhelper.sqls_increment_sequence('entities_id_seq'):
cursor.execute(sql)
eid = cursor.fetchone()[0]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/lowered_etype.py Mon Jan 24 17:02:38 2011 +0100
@@ -0,0 +1,5 @@
+
+from yams.buildobjs import EntityType
+
+class my_etype(EntityType):
+ pass
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/uppered_rtype.py Mon Jan 24 17:02:38 2011 +0100
@@ -0,0 +1,6 @@
+
+from yams.buildobjs import RelationDefinition
+
+class ARelation(RelationDefinition):
+ subject = 'CWUser'
+ object = 'CWGroup'
--- a/test/unittest_rset.py Thu Jan 20 10:10:22 2011 +0100
+++ b/test/unittest_rset.py Mon Jan 24 17:02:38 2011 +0100
@@ -382,6 +382,14 @@
self.assertEqual(set(e.e_schema.type for e in rset.entities(1)),
set(['CWGroup',]))
+ def test_iter_rows_with_entities(self):
+ rset = self.execute('Any U,UN,G,GN WHERE U in_group G, U login UN, G name GN')
+ # make sure we have at least one element
+ self.failUnless(rset)
+ out = list(rset.iter_rows_with_entities())[0]
+ self.assertEqual( out[0].login, out[1] )
+ self.assertEqual( out[2].name, out[3] )
+
def test_printable_rql(self):
rset = self.execute(u'CWEType X WHERE X final FALSE')
self.assertEqual(rset.printable_rql(),
--- a/test/unittest_schema.py Thu Jan 20 10:10:22 2011 +0100
+++ b/test/unittest_schema.py Mon Jan 24 17:02:38 2011 +0100
@@ -74,7 +74,6 @@
('Personne tel Int'),
('Personne fax Int'),
('Personne datenaiss Date'),
- ('Personne TEST Boolean'),
('Personne promo String'),
# real relations
('Personne travaille Societe'),
@@ -82,7 +81,7 @@
('Societe evaluee Note'),
('Personne concerne Affaire'),
('Personne concerne Societe'),
- ('Affaire Concerne Societe'),
+ ('Affaire concerne Societe'),
)
done = {}
for rel in RELS:
@@ -110,17 +109,6 @@
self.failIf(issubclass(RQLUniqueConstraint, RQLConstraint))
self.failUnless(issubclass(RQLConstraint, RQLVocabularyConstraint))
- def test_normalize(self):
- """test that entities, relations and attributes name are normalized
- """
- self.assertEqual(esociete.type, 'Societe')
- self.assertEqual(schema.has_relation('TEST'), 0)
- self.assertEqual(schema.has_relation('test'), 1)
- self.assertEqual(eperson.subjrels['test'].type, 'test')
- self.assertEqual(schema.has_relation('Concerne'), 0)
- self.assertEqual(schema.has_relation('concerne'), 1)
- self.assertEqual(schema.rschema('concerne').type, 'concerne')
-
def test_entity_perms(self):
self.assertEqual(eperson.get_groups('read'), set(('managers', 'users', 'guests')))
self.assertEqual(eperson.get_groups('update'), set(('managers', 'owners',)))
@@ -271,7 +259,7 @@
self.assertEqual([x.expression for x in aschema.get_rqlexprs('update')],
['U has_update_permission X'])
-class BadSchemaRQLExprTC(TestCase):
+class BadSchemaTC(TestCase):
def setUp(self):
self.loader = CubicWebSchemaLoader()
self.loader.defined = {}
@@ -285,6 +273,16 @@
self.loader._build_schema('toto', False)
self.assertEqual(str(cm.exception), msg)
+ def test_lowered_etype(self):
+ self._test('lowered_etype.py',
+ "'my_etype' is not a valid name for an entity type. It should "
+ "start with an upper cased letter and be followed by at least "
+ "a lower cased letter")
+
+ def test_uppered_rtype(self):
+ self._test('uppered_rtype.py',
+ "'ARelation' is not a valid name for a relation type. It should be lower cased")
+
def test_rrqlexpr_on_etype(self):
self._test('rrqlexpr_on_eetype.py',
"can't use RRQLExpression on ToTo, use an ERQLExpression")
--- a/test/unittest_selectors.py Thu Jan 20 10:10:22 2011 +0100
+++ b/test/unittest_selectors.py Mon Jan 24 17:02:38 2011 +0100
@@ -24,7 +24,7 @@
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.appobject import Selector, AndSelector, OrSelector
from cubicweb.selectors import (is_instance, adaptable, match_user_groups,
- multi_lines_rset)
+ multi_lines_rset, score_entity)
from cubicweb.interfaces import IDownloadable
from cubicweb.web import action
@@ -245,6 +245,24 @@
yield self.assertEqual, selector(None, self.req, self.rset), assertion
+class ScoreEntitySelectorTC(CubicWebTC):
+
+ def test_intscore_entity_selector(self):
+ req = self.request()
+ selector = score_entity(lambda x: None)
+ rset = req.execute('Any E WHERE E eid 0')
+ self.assertEqual(selector(None, req, rset), 0)
+ selector = score_entity(lambda x: "something")
+ self.assertEqual(selector(None, req, rset), 1)
+ selector = score_entity(lambda x: object)
+ self.assertEqual(selector(None, req, rset), 1)
+ rset = req.execute('Any G LIMIT 2 WHERE G is CWGroup')
+ selector = score_entity(lambda x: 10)
+ self.assertEqual(selector(None, req, rset), 20)
+ selector = score_entity(lambda x: 10, once_is_enough=True)
+ self.assertEqual(selector(None, req, rset), 10)
+
+
if __name__ == '__main__':
unittest_main()
--- a/view.py Thu Jan 20 10:10:22 2011 +0100
+++ b/view.py Mon Jan 24 17:02:38 2011 +0100
@@ -20,6 +20,7 @@
__docformat__ = "restructuredtext en"
_ = unicode
+import types, new
from cStringIO import StringIO
from warnings import warn
@@ -543,17 +544,6 @@
__registry__ = 'adapters'
-class EntityAdapter(Adapter):
- """base class for entity adapters (eg adapt an entity to an interface)"""
- def __init__(self, _cw, **kwargs):
- try:
- self.entity = kwargs.pop('entity')
- except KeyError:
- self.entity = kwargs['rset'].get_entity(kwargs.get('row') or 0,
- kwargs.get('col') or 0)
- Adapter.__init__(self, _cw, **kwargs)
-
-
def implements_adapter_compat(iface):
def _pre39_compat(func):
def decorated(self, *args, **kwargs):
@@ -568,5 +558,35 @@
return member(*args, **kwargs)
return member
return func(self, *args, **kwargs)
+ decorated.decorated = func
return decorated
return _pre39_compat
+
+
+def unwrap_adapter_compat(cls):
+ parent = cls.__bases__[0]
+ for member_name in dir(parent):
+ member = getattr(parent, member_name)
+ if isinstance(member, types.MethodType) and hasattr(member.im_func, 'decorated') and not member_name in cls.__dict__:
+ method = new.instancemethod(member.im_func.decorated, None, cls)
+ setattr(cls, member_name, method)
+
+
+class auto_unwrap_bw_compat(type):
+ def __new__(mcs, name, bases, classdict):
+ cls = type.__new__(mcs, name, bases, classdict)
+ if not classdict.get('__needs_bw_compat__'):
+ unwrap_adapter_compat(cls)
+ return cls
+
+
+class EntityAdapter(Adapter):
+ """base class for entity adapters (eg adapt an entity to an interface)"""
+ __metaclass__ = auto_unwrap_bw_compat
+ def __init__(self, _cw, **kwargs):
+ try:
+ self.entity = kwargs.pop('entity')
+ except KeyError:
+ self.entity = kwargs['rset'].get_entity(kwargs.get('row') or 0,
+ kwargs.get('col') or 0)
+ Adapter.__init__(self, _cw, **kwargs)
--- a/vregistry.py Thu Jan 20 10:10:22 2011 +0100
+++ b/vregistry.py Mon Jan 24 17:02:38 2011 +0100
@@ -129,6 +129,8 @@
# or simplify by calling unregister then register here
if not isinstance(replaced, basestring):
replaced = classid(replaced)
+ # prevent from misspelling
+ assert obj is not replaced, 'replacing an object by itself: %s' % obj
registered_objs = self.get(class_regid(obj), ())
for index, registered in enumerate(registered_objs):
if classid(registered) == replaced:
--- a/web/controller.py Thu Jan 20 10:10:22 2011 +0100
+++ b/web/controller.py Mon Jan 24 17:02:38 2011 +0100
@@ -148,10 +148,13 @@
path = self._cw.form['__redirectpath']
if (self._edited_entity and path != self._edited_entity.rest_path()
and '_cwmsgid' in newparams):
- # XXX may be here on modification?
- msg = u'(<a href="%s">%s</a>)' % (
- xml_escape(self._edited_entity.absolute_url()),
- self._cw._('click here to see created entity'))
+ # are we here on creation or modification?
+ if any(eid == self._edited_entity.eid
+ for eid in self._cw.data.get('eidmap', {}).values()):
+ msg = self._cw._('click here to see created entity')
+ else:
+ msg = self._cw._('click here to see edited entity')
+ msg = u'(<a href="%s">%s</a>)' % (xml_escape(self._edited_entity.absolute_url()), msg)
self._cw.append_to_redirect_message(msg)
elif self._after_deletion_path:
# else it should have been set during form processing
--- a/web/data/cubicweb.ajax.js Thu Jan 20 10:10:22 2011 +0100
+++ b/web/data/cubicweb.ajax.js Mon Jan 24 17:02:38 2011 +0100
@@ -283,7 +283,7 @@
* dictionary, `reqtype` the HTTP request type (get 'GET' or 'POST').
*/
function loadRemote(url, form, reqtype, sync) {
- if (!url.startswith(baseuri())) {
+ if (!url.toLowerCase().startswith(baseuri())) {
url = baseuri() + url;
}
if (!sync) {
--- a/web/data/cubicweb.calendar.js Thu Jan 20 10:10:22 2011 +0100
+++ b/web/data/cubicweb.calendar.js Mon Jan 24 17:02:38 2011 +0100
@@ -16,12 +16,14 @@
* .. class:: Calendar
*
* Calendar (graphical) widget
+ *
* public methods are :
+ *
* __init__ :
- * @param containerId: the DOM node's ID where the calendar will be displayed
- * @param inputId: which input needs to be updated when a date is selected
- * @param year, @param month: year and month to be displayed
- * @param cssclass: CSS class of the calendar widget (default is commandCal)
+ * :attr:`containerId`: the DOM node's ID where the calendar will be displayed
+ * :attr:`inputId`: which input needs to be updated when a date is selected
+ * :attr:`year`, :attr:`month`: year and month to be displayed
+ * :attr:`cssclass`: CSS class of the calendar widget (default is 'commandCal')
*
* show() / hide():
* show or hide the calendar widget
--- a/web/data/cubicweb.compat.js Thu Jan 20 10:10:22 2011 +0100
+++ b/web/data/cubicweb.compat.js Mon Jan 24 17:02:38 2011 +0100
@@ -32,14 +32,15 @@
/**
* .. function:: cw.utils.deprecatedFunction(msg, function)
*
- * jQUery flattens arrays returned by the mapping function:
- * >>> y = ['a:b:c', 'd:e']
- * >>> jQuery.map(y, function(y) { return y.split(':');})
- * ["a", "b", "c", "d", "e"]
- * // where one would expect:
- * [ ["a", "b", "c"], ["d", "e"] ]
- * XXX why not the same argument order as $.map and forEach ?
+ * jQUery flattens arrays returned by the mapping function: ::
+ *
+ * >>> y = ['a:b:c', 'd:e']
+ * >>> jQuery.map(y, function(y) { return y.split(':');})
+ * ["a", "b", "c", "d", "e"]
+ * // where one would expect:
+ * [ ["a", "b", "c"], ["d", "e"] ]
*/
+ // XXX why not the same argument order as $.map and forEach ?
map = cw.utils.deprecatedFunction(
'[3.9] map() is deprecated, use $.map instead',
function(func, array) {
--- a/web/data/cubicweb.htmlhelpers.js Thu Jan 20 10:10:22 2011 +0100
+++ b/web/data/cubicweb.htmlhelpers.js Mon Jan 24 17:02:38 2011 +0100
@@ -18,9 +18,9 @@
function baseuri() {
var uri = document.baseURI;
if (uri) { // some browsers don't define baseURI
- return uri;
+ return uri.toLowerCase();
}
- return jQuery('base').attr('href');
+ return jQuery('base').attr('href').toLowerCase();
}
/**
--- a/web/data/cubicweb.preferences.js Thu Jan 20 10:10:22 2011 +0100
+++ b/web/data/cubicweb.preferences.js Mon Jan 24 17:02:38 2011 +0100
@@ -1,8 +1,6 @@
/**
- * toggle visibility of an element by its id
- * & set current visibility status in a cookie
- * XXX whenever used outside of preferences, don't forget to
- * move me in a more appropriate place
+ * toggle visibility of an element by its id & set current visibility status in a cookie
+ *
*/
var prefsValues = {};
--- a/web/data/cubicweb.python.js Thu Jan 20 10:10:22 2011 +0100
+++ b/web/data/cubicweb.python.js Mon Jan 24 17:02:38 2011 +0100
@@ -248,9 +248,11 @@
* this is a js class factory. objects returned by this function behave
* more or less like a python class. The `class` function prototype is
* inspired by the python `type` builtin
- * Important notes :
- * -> methods are _STATICALLY_ attached when the class it created
- * -> multiple inheritance was never tested, which means it doesn't work ;-)
+ *
+ * .. Note::
+ *
+ * * methods are _STATICALLY_ attached when the class it created
+ * * multiple inheritance was never tested, which means it doesn't work ;-)
*/
function defclass(name, bases, classdict) {
var baseclasses = bases || [];
--- a/web/formwidgets.py Thu Jan 20 10:10:22 2011 +0100
+++ b/web/formwidgets.py Mon Jan 24 17:02:38 2011 +0100
@@ -28,6 +28,7 @@
.. autoclass:: cubicweb.web.formwidgets.FieldWidget
+
HTML <input> based widgets
''''''''''''''''''''''''''
@@ -37,6 +38,7 @@
.. autoclass:: cubicweb.web.formwidgets.FileInput
.. autoclass:: cubicweb.web.formwidgets.ButtonInput
+
Other standard HTML widgets
'''''''''''''''''''''''''''
@@ -45,6 +47,7 @@
.. autoclass:: cubicweb.web.formwidgets.CheckBox
.. autoclass:: cubicweb.web.formwidgets.Radio
+
Date and time widgets
'''''''''''''''''''''
@@ -53,6 +56,7 @@
.. autoclass:: cubicweb.web.formwidgets.JQueryDatePicker
.. autoclass:: cubicweb.web.formwidgets.JQueryTimePicker
+
Ajax / javascript widgets
'''''''''''''''''''''''''
@@ -64,19 +68,22 @@
.. kill or document LazyRestrictedAutoCompletionWidget
.. kill or document RestrictedAutoCompletionWidget
+
Other widgets
'''''''''''''
+
.. autoclass:: cubicweb.web.formwidgets.PasswordInput
.. autoclass:: cubicweb.web.formwidgets.IntervalWidget
.. autoclass:: cubicweb.web.formwidgets.HorizontalLayoutWidget
.. autoclass:: cubicweb.web.formwidgets.EditableURLWidget
+
Form controls
'''''''''''''
-Those classes are not proper widget (they are not associated to
-field) but are used as form controls. Their API is similar
-to widgets except that `field` argument given to :meth:`render`
-will be `None`.
+
+Those classes are not proper widget (they are not associated to field) but are
+used as form controls. Their API is similar to widgets except that `field`
+argument given to :meth:`render` will be `None`.
.. autoclass:: cubicweb.web.formwidgets.Button
.. autoclass:: cubicweb.web.formwidgets.SubmitButton
@@ -107,15 +114,20 @@
:attr:`needs_js`
list of javascript files needed by the widget.
+
:attr:`needs_css`
list of css files needed by the widget.
+
:attr:`setdomid`
flag telling if HTML DOM identifier should be set on input.
+
:attr:`settabindex`
flag telling if HTML tabindex attribute of inputs should be set.
+
:attr:`suffix`
string to use a suffix when generating input, to ease usage as a
sub-widgets (eg widget used by another widget)
+
:attr:`vocabulary_widget`
flag telling if this widget expect a vocabulary
@@ -212,7 +224,7 @@
generating the form)
4. field's typed value (returned by its
- :meth:`~cubicweb.web.formfields.Field.typed_value` method)
+ :meth:`~cubicweb.web.formfields.Field.typed_value` method)
Values found in 1. and 2. are expected te be already some 'display
value' (eg a string) while those found in 3. and 4. are expected to be
--- a/web/test/unittest_application.py Thu Jan 20 10:10:22 2011 +0100
+++ b/web/test/unittest_application.py Mon Jan 24 17:02:38 2011 +0100
@@ -17,6 +17,8 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""unit tests for cubicweb.web.application"""
+from __future__ import with_statement
+
import base64, Cookie
import sys
from urllib import unquote
--- a/web/test/unittest_views_basecontrollers.py Thu Jan 20 10:10:22 2011 +0100
+++ b/web/test/unittest_views_basecontrollers.py Mon Jan 24 17:02:38 2011 +0100
@@ -17,6 +17,8 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""cubicweb.web.views.basecontrollers unit tests"""
+from __future__ import with_statement
+
from logilab.common.testlib import unittest_main, mock_object
from cubicweb import Binary, NoSelectableObject, ValidationError
--- a/web/views/calendar.py Thu Jan 20 10:10:22 2011 +0100
+++ b/web/views/calendar.py Mon Jan 24 17:02:38 2011 +0100
@@ -44,6 +44,7 @@
class ICalendarableAdapter(EntityAdapter):
+ __needs_bw_compat__ = True
__regid__ = 'ICalendarable'
__select__ = implements(ICalendarable, warn=False) # XXX for bw compat, should be abstract
--- a/web/views/editcontroller.py Thu Jan 20 10:10:22 2011 +0100
+++ b/web/views/editcontroller.py Mon Jan 24 17:02:38 2011 +0100
@@ -34,6 +34,7 @@
class IEditControlAdapter(EntityAdapter):
+ __needs_bw_compat__ = True
__regid__ = 'IEditControl'
__select__ = is_instance('Any')
--- a/web/views/embedding.py Thu Jan 20 10:10:22 2011 +0100
+++ b/web/views/embedding.py Mon Jan 24 17:02:38 2011 +0100
@@ -41,6 +41,7 @@
class IEmbedableAdapter(EntityAdapter):
"""interface for embedable entities"""
+ __needs_bw_compat__ = True
__regid__ = 'IEmbedable'
__select__ = implements(IEmbedable, warn=False) # XXX for bw compat, should be abstract
--- a/web/views/igeocodable.py Thu Jan 20 10:10:22 2011 +0100
+++ b/web/views/igeocodable.py Mon Jan 24 17:02:38 2011 +0100
@@ -26,6 +26,7 @@
class IGeocodableAdapter(EntityAdapter):
"""interface required by geocoding views such as gmap-view"""
+ __needs_bw_compat__ = True
__regid__ = 'IGeocodable'
__select__ = implements(IGeocodable, warn=False) # XXX for bw compat, should be abstract
--- a/web/views/isioc.py Thu Jan 20 10:10:22 2011 +0100
+++ b/web/views/isioc.py Mon Jan 24 17:02:38 2011 +0100
@@ -32,6 +32,7 @@
class ISIOCItemAdapter(EntityAdapter):
"""interface for entities which may be represented as an ISIOC items"""
+ __needs_bw_compat__ = True
__regid__ = 'ISIOCItem'
__select__ = implements(ISiocItem, warn=False) # XXX for bw compat, should be abstract
@@ -63,6 +64,7 @@
class ISIOCContainerAdapter(EntityAdapter):
"""interface for entities which may be represented as an ISIOC container"""
+ __needs_bw_compat__ = True
__regid__ = 'ISIOCContainer'
__select__ = implements(ISiocContainer, warn=False) # XXX for bw compat, should be abstract
--- a/web/views/navigation.py Thu Jan 20 10:10:22 2011 +0100
+++ b/web/views/navigation.py Mon Jan 24 17:02:38 2011 +0100
@@ -187,6 +187,7 @@
"""interface for entities which can be linked to a previous and/or next
entity
"""
+ __needs_bw_compat__ = True
__regid__ = 'IPrevNext'
__select__ = implements(IPrevNext, warn=False) # XXX for bw compat, else should be abstract
--- a/web/views/xmlrss.py Thu Jan 20 10:10:22 2011 +0100
+++ b/web/views/xmlrss.py Mon Jan 24 17:02:38 2011 +0100
@@ -122,6 +122,7 @@
# RSS stuff ###################################################################
class IFeedAdapter(EntityAdapter):
+ __needs_bw_compat__ = True
__regid__ = 'IFeed'
__select__ = is_instance('Any')