# HG changeset patch # User Sylvain Thénault # Date 1295884958 -3600 # Node ID b5e34836f84e3b5ae96370825d9c6dc47cc75691 # Parent ac092197c09903a2b647eafbf877376e26acbc86# Parent a473bf557456324f8d4b1613400c2bc11175c336 backport stable diff -r ac092197c099 -r b5e34836f84e appobject.py --- 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'' % (self.__class__.__name__, id(self)) + class MultiSelector(Selector): """base class for compound selector classes""" diff -r ac092197c099 -r b5e34836f84e cwconfig.py --- 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) diff -r ac092197c099 -r b5e34836f84e cwctl.py --- 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) diff -r ac092197c099 -r b5e34836f84e devtools/__init__.py --- 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 diff -r ac092197c099 -r b5e34836f84e doc/book/en/admin/setup.rst --- 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 ` listed on the `CubicWeb.org Forge`_ -available as debian packages and tarball. +There is also a wide variety of :ref:`cubes ` 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": diff -r ac092197c099 -r b5e34836f84e doc/book/en/annexes/index.rst --- 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 diff -r ac092197c099 -r b5e34836f84e doc/book/en/annexes/rql/index.rst --- 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) ============================= diff -r ac092197c099 -r b5e34836f84e doc/book/en/devrepo/cubes/available-cubes.rst --- 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 --------------- diff -r ac092197c099 -r b5e34836f84e doc/book/en/devrepo/datamodel/define-workflows.rst --- 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 --------------------- diff -r ac092197c099 -r b5e34836f84e doc/book/en/devweb/edition/examples.rst --- 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 diff -r ac092197c099 -r b5e34836f84e doc/book/en/devweb/views/basetemplates.rst --- 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 diff -r ac092197c099 -r b5e34836f84e doc/book/en/devweb/views/primary.rst --- 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 diff -r ac092197c099 -r b5e34836f84e doc/book/en/images/tutos-photowebsite_background-image.png Binary file doc/book/en/images/tutos-photowebsite_background-image.png has changed diff -r ac092197c099 -r b5e34836f84e doc/book/en/images/tutos-photowebsite_boxes.png Binary file doc/book/en/images/tutos-photowebsite_boxes.png has changed diff -r ac092197c099 -r b5e34836f84e doc/book/en/images/tutos-photowebsite_breadcrumbs.png Binary file doc/book/en/images/tutos-photowebsite_breadcrumbs.png has changed diff -r ac092197c099 -r b5e34836f84e doc/book/en/images/tutos-photowebsite_facets.png Binary file doc/book/en/images/tutos-photowebsite_facets.png has changed diff -r ac092197c099 -r b5e34836f84e doc/book/en/images/tutos-photowebsite_grey-box.png Binary file doc/book/en/images/tutos-photowebsite_grey-box.png has changed diff -r ac092197c099 -r b5e34836f84e doc/book/en/images/tutos-photowebsite_index-after.png Binary file doc/book/en/images/tutos-photowebsite_index-after.png has changed diff -r ac092197c099 -r b5e34836f84e doc/book/en/images/tutos-photowebsite_index-before.png Binary file doc/book/en/images/tutos-photowebsite_index-before.png has changed diff -r ac092197c099 -r b5e34836f84e doc/book/en/images/tutos-photowebsite_login-box.png Binary file doc/book/en/images/tutos-photowebsite_login-box.png has changed diff -r ac092197c099 -r b5e34836f84e doc/book/en/images/tutos-photowebsite_prevnext.png Binary file doc/book/en/images/tutos-photowebsite_prevnext.png has changed diff -r ac092197c099 -r b5e34836f84e doc/book/en/images/tutos-photowebsite_ui1.png Binary file doc/book/en/images/tutos-photowebsite_ui1.png has changed diff -r ac092197c099 -r b5e34836f84e doc/book/en/images/tutos-photowebsite_ui2.png Binary file doc/book/en/images/tutos-photowebsite_ui2.png has changed diff -r ac092197c099 -r b5e34836f84e doc/book/en/images/tutos-photowebsite_ui3.png Binary file doc/book/en/images/tutos-photowebsite_ui3.png has changed diff -r ac092197c099 -r b5e34836f84e doc/book/en/index.rst --- 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 ` of the application, +* an engine driven by the explicit :ref:`data model + ` of the application, + * a query language named :ref:`RQL ` similar to W3C's SPARQL, -* a :ref:`selection+view ` mechanism for semi-automatic XHTML/XML/JSON/text generation, -* a library of reusable :ref:`components ` (data model and views) that fulfill common needs, + +* a :ref:`selection+view ` + mechanism for semi-automatic XHTML/XML/JSON/text generation, + +* a library of reusable :ref:`components ` (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_. diff -r ac092197c099 -r b5e34836f84e doc/book/en/makefile --- 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 diff -r ac092197c099 -r b5e34836f84e doc/book/en/tutorials/advanced/index.rst --- 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:`_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... - diff -r ac092197c099 -r b5e34836f84e doc/book/en/tutorials/advanced/part01_create-cube.rst --- /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... + diff -r ac092197c099 -r b5e34836f84e doc/book/en/tutorials/advanced/part02_security.rst --- /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:`_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... diff -r ac092197c099 -r b5e34836f84e doc/book/en/tutorials/advanced/part03_bfss.rst --- /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 :) diff -r ac092197c099 -r b5e34836f84e doc/book/en/tutorials/advanced/part04_ui-base.rst --- /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"

%s

" % self._cw._('this resource does not exist')) + self.w(u"

%s

" % 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'
\n') + if self._cw.cnx.anonymous_connection: + self.w(u'

%s

\n' % self._cw._('Public Albums')) + else: + self.w(u'

%s

\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'
\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 diff -r ac092197c099 -r b5e34836f84e doc/book/en/tutorials/advanced/part05_ui-advanced.rst --- /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 ```` tag as first element after the +```` 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 ```` 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 + def call(self, **kwargs): + self.w(u'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 '}) + 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 diff -r ac092197c099 -r b5e34836f84e doc/book/en/tutorials/base/customizing-the-application.rst --- 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 ~~~~~~~~~~~~~~~~~~~ diff -r ac092197c099 -r b5e34836f84e doc/book/en/tutorials/base/index.rst --- 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: diff -r ac092197c099 -r b5e34836f84e doc/book/en/tutorials/tools/windmill.rst --- 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 diff -r ac092197c099 -r b5e34836f84e doc/tools/pyjsrest.py --- 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() diff -r ac092197c099 -r b5e34836f84e entities/adapters.py --- 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 diff -r ac092197c099 -r b5e34836f84e entities/test/unittest_wfobjs.py --- 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, diff -r ac092197c099 -r b5e34836f84e etwist/twconfig.py --- 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 diff -r ac092197c099 -r b5e34836f84e i18n/de.po --- 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" diff -r ac092197c099 -r b5e34836f84e i18n/en.po --- 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 "" diff -r ac092197c099 -r b5e34836f84e i18n/es.po --- 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 "" diff -r ac092197c099 -r b5e34836f84e i18n/fr.po --- 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" diff -r ac092197c099 -r b5e34836f84e misc/migration/3.10.7_Any.py --- 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') diff -r ac092197c099 -r b5e34836f84e misc/migration/3.10.8_Any.py --- /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) diff -r ac092197c099 -r b5e34836f84e rset.py --- 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: diff -r ac092197c099 -r b5e34836f84e schema.py --- 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 diff -r ac092197c099 -r b5e34836f84e schemas/base.py --- 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')) diff -r ac092197c099 -r b5e34836f84e selectors.py --- 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'' % (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 diff -r ac092197c099 -r b5e34836f84e server/hook.py --- 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 diff -r ac092197c099 -r b5e34836f84e server/serverconfig.py --- 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) diff -r ac092197c099 -r b5e34836f84e server/serverctl.py --- 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 diff -r ac092197c099 -r b5e34836f84e server/sources/native.py --- 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] diff -r ac092197c099 -r b5e34836f84e test/data/lowered_etype.py --- /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 diff -r ac092197c099 -r b5e34836f84e test/data/uppered_rtype.py --- /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' diff -r ac092197c099 -r b5e34836f84e test/unittest_rset.py --- 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(), diff -r ac092197c099 -r b5e34836f84e test/unittest_schema.py --- 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") diff -r ac092197c099 -r b5e34836f84e test/unittest_selectors.py --- 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() diff -r ac092197c099 -r b5e34836f84e view.py --- 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) diff -r ac092197c099 -r b5e34836f84e vregistry.py --- 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: diff -r ac092197c099 -r b5e34836f84e web/controller.py --- 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'(%s)' % ( - 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'(%s)' % (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 diff -r ac092197c099 -r b5e34836f84e web/data/cubicweb.ajax.js --- 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) { diff -r ac092197c099 -r b5e34836f84e web/data/cubicweb.calendar.js --- 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 diff -r ac092197c099 -r b5e34836f84e web/data/cubicweb.compat.js --- 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) { diff -r ac092197c099 -r b5e34836f84e web/data/cubicweb.htmlhelpers.js --- 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(); } /** diff -r ac092197c099 -r b5e34836f84e web/data/cubicweb.preferences.js --- 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 = {}; diff -r ac092197c099 -r b5e34836f84e web/data/cubicweb.python.js --- 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 || []; diff -r ac092197c099 -r b5e34836f84e web/formwidgets.py --- 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 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 diff -r ac092197c099 -r b5e34836f84e web/test/unittest_application.py --- 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 . """unit tests for cubicweb.web.application""" +from __future__ import with_statement + import base64, Cookie import sys from urllib import unquote diff -r ac092197c099 -r b5e34836f84e web/test/unittest_views_basecontrollers.py --- 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 . """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 diff -r ac092197c099 -r b5e34836f84e web/views/calendar.py --- 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 diff -r ac092197c099 -r b5e34836f84e web/views/editcontroller.py --- 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') diff -r ac092197c099 -r b5e34836f84e web/views/embedding.py --- 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 diff -r ac092197c099 -r b5e34836f84e web/views/igeocodable.py --- 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 diff -r ac092197c099 -r b5e34836f84e web/views/isioc.py --- 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 diff -r ac092197c099 -r b5e34836f84e web/views/navigation.py --- 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 diff -r ac092197c099 -r b5e34836f84e web/views/xmlrss.py --- 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')