doc/book/en/tutorials/advanced/index.rst
branchstable
changeset 6876 4b0b9d8207c5
parent 6833 8fe4b003c1bc
equal deleted inserted replaced
6875:a166b51d13f8 6876:4b0b9d8207c5
     1 .. _advanced_tutorial:
       
     2 
     1 
     3 Building a photo gallery with CubicWeb
     2 .. _TutosPhotoWebSite:
     4 ======================================
     3 
       
     4 Building a photo gallery with |cubicweb|
       
     5 ========================================
     5 
     6 
     6 Desired features
     7 Desired features
     7 ----------------
     8 ----------------
     8 
     9 
     9 * basically a photo gallery
    10 * basically a photo gallery
    14   picture... using facets
    15   picture... using facets
    15 
    16 
    16 * advanced security (not everyone can see everything). More on this later.
    17 * advanced security (not everyone can see everything). More on this later.
    17 
    18 
    18 
    19 
    19 Cube creation and schema definition
    20 .. toctree::
    20 -----------------------------------
    21    :maxdepth: 2
    21 
    22 
    22 .. _adv_tuto_create_new_cube:
    23    part01_create-cube
    23 
    24    part02_security
    24 Step 1: creating a new cube for my web site
    25    part03_bfss
    25 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    26    part04_ui-base
    26 
    27    part05_ui-advanced
    27 One note about my development environment: I wanted to use the packaged
       
    28 version of CubicWeb and cubes while keeping my cube in my user
       
    29 directory, let's say `~src/cubes`.  I achieve this by setting the
       
    30 following environment variables::
       
    31 
       
    32   CW_CUBES_PATH=~/src/cubes
       
    33   CW_MODE=user
       
    34 
       
    35 I can now create the cube which will hold custom code for this web
       
    36 site using::
       
    37 
       
    38   cubicweb-ctl newcube --directory=~/src/cubes sytweb
       
    39 
    28 
    40 
    29 
    41 .. _adv_tuto_assemble_cubes:
       
    42 
       
    43 Step 2: pick building blocks into existing cubes
       
    44 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
    45 
       
    46 Almost everything I want to handle in my web-site is somehow already modelized in
       
    47 existing cubes that I'll extend for my need. So I'll pick the following cubes:
       
    48 
       
    49 * `folder`, containing the `Folder` entity type, which will be used as
       
    50   both 'album' and a way to map file system folders. Entities are
       
    51   added to a given folder using the `filed_under` relation.
       
    52 
       
    53 * `file`, containing `File` and `Image` entity types, gallery view,
       
    54   and a file system import utility.
       
    55 
       
    56 * `zone`, containing the `Zone` entity type for hierarchical geographical
       
    57   zones. Entities (including sub-zones) are added to a given zone using the
       
    58   `situated_in` relation.
       
    59 
       
    60 * `person`, containing the `Person` entity type plus some basic views.
       
    61 
       
    62 * `comment`, providing a full commenting system allowing one to comment entity types
       
    63   supporting the `comments` relation by adding a `Comment` entity.
       
    64 
       
    65 * `tag`, providing a full tagging system as an easy and powerful way to classify
       
    66   entities supporting the `tags` relation by linking the to `Tag` entities. This
       
    67   will allows navigation into a large number of picture.
       
    68 
       
    69 Ok, now I'll tell my cube requires all this by editing :file:`cubes/sytweb/__pkginfo__.py`:
       
    70 
       
    71   .. sourcecode:: python
       
    72 
       
    73     __depends__ = {'cubicweb': '>= 3.8.0',
       
    74                    'cubicweb-file': '>= 1.2.0',
       
    75 		   'cubicweb-folder': '>= 1.1.0',
       
    76 		   'cubicweb-person': '>= 1.2.0',
       
    77 		   'cubicweb-comment': '>= 1.2.0',
       
    78 		   'cubicweb-tag': '>= 1.2.0',
       
    79 		   'cubicweb-zone': None}
       
    80 
       
    81 Notice that you can express minimal version of the cube that should be used,
       
    82 `None` meaning whatever version available. All packages starting with 'cubicweb-'
       
    83 will be recognized as being cube, not bare python packages. You can still specify
       
    84 this explicitly using instead the `__depends_cubes__` dictionary which should
       
    85 contains cube's name without the prefix. So the example below would be written
       
    86 as:
       
    87 
       
    88   .. sourcecode:: python
       
    89 
       
    90     __depends__ = {'cubicweb': '>= 3.8.0'}
       
    91     __depends_cubes__ = {'file': '>= 1.2.0',
       
    92 		         'folder': '>= 1.1.0',
       
    93 		   	 'person': '>= 1.2.0',
       
    94 		   	 'comment': '>= 1.2.0',
       
    95 		   	 'tag': '>= 1.2.0',
       
    96 		   	 'zone': None}
       
    97 
       
    98 
       
    99 Step 3: glue everything together in my cube's schema
       
   100 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   101 
       
   102 .. sourcecode:: python
       
   103 
       
   104     from yams.buildobjs import RelationDefinition
       
   105 
       
   106     class comments(RelationDefinition):
       
   107 	subject = 'Comment'
       
   108 	object = ('File', 'Image')
       
   109 	cardinality = '1*'
       
   110 	composite = 'object'
       
   111 
       
   112     class tags(RelationDefinition):
       
   113 	subject = 'Tag'
       
   114 	object = ('File', 'Image')
       
   115 
       
   116     class filed_under(RelationDefinition):
       
   117 	subject = ('File', 'Image')
       
   118 	object = 'Folder'
       
   119 
       
   120     class situated_in(RelationDefinition):
       
   121 	subject = 'Image'
       
   122 	object = 'Zone'
       
   123 
       
   124     class displayed_on(RelationDefinition):
       
   125 	subject = 'Person'
       
   126 	object = 'Image'
       
   127 
       
   128 
       
   129 This schema:
       
   130 
       
   131 * allows to comment and tag on `File` and `Image` entity types by adding the
       
   132   `comments` and `tags` relations. This should be all we've to do for this
       
   133   feature since the related cubes provide 'pluggable section' which are
       
   134   automatically displayed on the primary view of entity types supporting the
       
   135   relation.
       
   136 
       
   137 * adds a `situated_in` relation definition so that image entities can be
       
   138   geolocalized.
       
   139 
       
   140 * add a new relation `displayed_on` relation telling who can be seen on a
       
   141   picture.
       
   142 
       
   143 This schema will probably have to evolve as time goes (for security handling at
       
   144 least), but since the possibility to let a schema evolve is one of CubicWeb's
       
   145 features (and goals), we won't worry about it for now and see that later when needed.
       
   146 
       
   147 
       
   148 Step 4: creating the instance
       
   149 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   150 
       
   151 Now that I have a schema, I want to create an instance. To
       
   152 do so using this new 'sytweb' cube, I run::
       
   153 
       
   154   cubicweb-ctl create sytweb sytweb_instance
       
   155 
       
   156 Hint: if you get an error while the database is initialized, you can
       
   157 avoid having to answer the questions again by running::
       
   158 
       
   159    cubicweb-ctl db-create sytweb_instance
       
   160 
       
   161 This will use your already configured instance and start directly from the create
       
   162 database step, thus skipping questions asked by the 'create' command.
       
   163 
       
   164 Once the instance and database are fully initialized, run ::
       
   165 
       
   166   cubicweb-ctl start sytweb_instance
       
   167 
       
   168 to start the instance, check you can connect on it, etc...
       
   169 
       
   170 
       
   171 Security, testing and migration
       
   172 -------------------------------
       
   173 
       
   174 This part will cover various topics:
       
   175 
       
   176 * configuring security
       
   177 * migrating existing instance
       
   178 * writing some unit tests
       
   179 
       
   180 Here is the ``read`` security model I want:
       
   181 
       
   182 * folders, files, images and comments should have one of the following visibility:
       
   183 
       
   184   - ``public``, everyone can see it
       
   185   - ``authenticated``, only authenticated users can see it
       
   186   - ``restricted``, only a subset of authenticated users can see it
       
   187 
       
   188 * managers (e.g. me) can see everything
       
   189 * only authenticated users can see people
       
   190 * everyone can see classifier entities, such as tag and zone
       
   191 
       
   192 Also, unless explicitly specified, the visibility of an image should be the same as
       
   193 its parent folder, as well as visibility of a comment should be the same as the
       
   194 commented entity. If there is no parent entity, the default visibility is
       
   195 ``authenticated``.
       
   196 
       
   197 Regarding write security, that's much easier:
       
   198 * anonymous can't write anything
       
   199 * authenticated users can only add comment
       
   200 * managers will add the remaining stuff
       
   201 
       
   202 Now, let's implement that!
       
   203 
       
   204 Proper security in CubicWeb is done at the schema level, so you don't have to
       
   205 bother with it in views: users will only see what they can see automatically.
       
   206 
       
   207 .. _adv_tuto_security:
       
   208 
       
   209 Step 1: configuring security into the schema
       
   210 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   211 
       
   212 In schema, you can grant access according to groups, or to some RQL expressions:
       
   213 users get access if the expression returns some results. To implement the read
       
   214 security defined earlier, groups are not enough, we'll need some RQL expression. Here
       
   215 is the idea:
       
   216 
       
   217 * add a `visibility` attribute on Folder, Image and Comment, which may be one of
       
   218   the value explained above
       
   219 
       
   220 * add a `may_be_read_by` relation from Folder, Image and Comment to users,
       
   221   which will define who can see the entity
       
   222 
       
   223 * security propagation will be done in hook.
       
   224 
       
   225 So the first thing to do is to modify my cube's schema.py to define those
       
   226 relations:
       
   227 
       
   228 .. sourcecode:: python
       
   229 
       
   230     from yams.constraints import StaticVocabularyConstraint
       
   231 
       
   232     class visibility(RelationDefinition):
       
   233 	subject = ('Folder', 'File', 'Image', 'Comment')
       
   234 	object = 'String'
       
   235 	constraints = [StaticVocabularyConstraint(('public', 'authenticated',
       
   236 						   'restricted', 'parent'))]
       
   237 	default = 'parent'
       
   238 	cardinality = '11' # required
       
   239 
       
   240     class may_be_read_by(RelationDefinition):
       
   241         __permissions__ = {
       
   242 	    'read':   ('managers', 'users'),
       
   243 	    'add':    ('managers',),
       
   244 	    'delete': ('managers',),
       
   245 	    }
       
   246 
       
   247 	subject = ('Folder', 'File', 'Image', 'Comment',)
       
   248 	object = 'CWUser'
       
   249 
       
   250 We can note the following points:
       
   251 
       
   252 * we've added a new `visibility` attribute to folder, file, image and comment
       
   253   using a `RelationDefinition`
       
   254 
       
   255 * `cardinality = '11'` means this attribute is required. This is usually hidden
       
   256   under the `required` argument given to the `String` constructor, but we can
       
   257   rely on this here (same thing for StaticVocabularyConstraint, which is usually
       
   258   hidden by the `vocabulary` argument)
       
   259 
       
   260 * the `parent` possible value will be used for visibility propagation
       
   261 
       
   262 * think to secure the `may_be_read_by` permissions, else any user can add/delte it
       
   263   by default, which somewhat breaks our security model...
       
   264 
       
   265 Now, we should be able to define security rules in the schema, based on these new
       
   266 attribute and relation. Here is the code to add to *schema.py*:
       
   267 
       
   268 .. sourcecode:: python
       
   269 
       
   270     from cubicweb.schema import ERQLExpression
       
   271 
       
   272     VISIBILITY_PERMISSIONS = {
       
   273 	'read':   ('managers',
       
   274 		   ERQLExpression('X visibility "public"'),
       
   275 		   ERQLExpression('X may_be_read_by U')),
       
   276 	'add':    ('managers',),
       
   277 	'update': ('managers', 'owners',),
       
   278 	'delete': ('managers', 'owners'),
       
   279 	}
       
   280     AUTH_ONLY_PERMISSIONS = {
       
   281 	    'read':   ('managers', 'users'),
       
   282 	    'add':    ('managers',),
       
   283 	    'update': ('managers', 'owners',),
       
   284 	    'delete': ('managers', 'owners'),
       
   285 	    }
       
   286     CLASSIFIERS_PERMISSIONS = {
       
   287 	    'read':   ('managers', 'users', 'guests'),
       
   288 	    'add':    ('managers',),
       
   289 	    'update': ('managers', 'owners',),
       
   290 	    'delete': ('managers', 'owners'),
       
   291 	    }
       
   292 
       
   293     from cubes.folder.schema import Folder
       
   294     from cubes.file.schema import File, Image
       
   295     from cubes.comment.schema import Comment
       
   296     from cubes.person.schema import Person
       
   297     from cubes.zone.schema import Zone
       
   298     from cubes.tag.schema import Tag
       
   299 
       
   300     Folder.__permissions__ = VISIBILITY_PERMISSIONS
       
   301     File.__permissions__ = VISIBILITY_PERMISSIONS
       
   302     Image.__permissions__ = VISIBILITY_PERMISSIONS
       
   303     Comment.__permissions__ = VISIBILITY_PERMISSIONS.copy()
       
   304     Comment.__permissions__['add'] = ('managers', 'users',)
       
   305     Person.__permissions__ = AUTH_ONLY_PERMISSIONS
       
   306     Zone.__permissions__ = CLASSIFIERS_PERMISSIONS
       
   307     Tag.__permissions__ = CLASSIFIERS_PERMISSIONS
       
   308 
       
   309 What's important in there:
       
   310 
       
   311 * `VISIBILITY_PERMISSIONS` provides read access to managers group, if
       
   312   `visibility` attribute's value is 'public', or if user (designed by the 'U'
       
   313   variable in the expression) is linked to the entity (the 'X' variable) through
       
   314   the `may_read` permission
       
   315 
       
   316 * we modify permissions of the entity types we use by importing them and
       
   317   modifying their `__permissions__` attribute
       
   318 
       
   319 * notice the `.copy()`: we only want to modify 'add' permission for `Comment`,
       
   320   not for all entity types using `VISIBILITY_PERMISSIONS`!
       
   321 
       
   322 * the remaining part of the security model is done using regular groups:
       
   323 
       
   324   - `users` is the group to which all authenticated users will belong
       
   325   - `guests` is the group of anonymous users
       
   326 
       
   327 
       
   328 .. _adv_tuto_security_propagation:
       
   329 
       
   330 Step 2: security propagation in hooks
       
   331 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   332 
       
   333 To fullfill the requirements, we have to implement::
       
   334 
       
   335   Also, unless explicity specified, visibility of an image should be the same as
       
   336   its parent folder, as well as visibility of a comment should be the same as the
       
   337   commented entity.
       
   338 
       
   339 This kind of `active` rule will be done using CubicWeb's hook
       
   340 system. Hooks are triggered on database event such as addition of new
       
   341 entity or relation.
       
   342 
       
   343 The tricky part of the requirement is in *unless explicitly specified*, notably
       
   344 because when the entity is added, we don't know yet its 'parent'
       
   345 entity (e.g. Folder of an Image, Image commented by a Comment). To handle such things,
       
   346 CubicWeb provides `Operation`, which allow to schedule things to do at commit time.
       
   347 
       
   348 In our case we will:
       
   349 
       
   350 * on entity creation, schedule an operation that will set default visibility
       
   351 
       
   352 * when a "parent" relation is added, propagate parent's visibility unless the
       
   353   child already has a visibility set
       
   354 
       
   355 Here is the code in cube's *hooks.py*:
       
   356 
       
   357 .. sourcecode:: python
       
   358 
       
   359     from cubicweb.selectors import is_instance
       
   360     from cubicweb.server import hook
       
   361 
       
   362     class SetVisibilityOp(hook.Operation):
       
   363 	def precommit_event(self):
       
   364 	    for eid in self.session.transaction_data.pop('pending_visibility'):
       
   365 		entity = self.session.entity_from_eid(eid)
       
   366 		if entity.visibility == 'parent':
       
   367 		    entity.set_attributes(visibility=u'authenticated')
       
   368 
       
   369     class SetVisibilityHook(hook.Hook):
       
   370 	__regid__ = 'sytweb.setvisibility'
       
   371 	__select__ = hook.Hook.__select__ & is_instance('Folder', 'File', 'Image', 'Comment')
       
   372 	events = ('after_add_entity',)
       
   373 	def __call__(self):
       
   374 	    hook.set_operation(self._cw, 'pending_visibility', self.entity.eid,
       
   375 			       SetVisibilityOp)
       
   376 
       
   377     class SetParentVisibilityHook(hook.Hook):
       
   378 	__regid__ = 'sytweb.setparentvisibility'
       
   379 	__select__ = hook.Hook.__select__ & hook.match_rtype('filed_under', 'comments')
       
   380 	events = ('after_add_relation',)
       
   381 
       
   382 	def __call__(self):
       
   383 	    parent = self._cw.entity_from_eid(self.eidto)
       
   384 	    child = self._cw.entity_from_eid(self.eidfrom)
       
   385 	    if child.visibility == 'parent':
       
   386 		child.set_attributes(visibility=parent.visibility)
       
   387 
       
   388 Notice:
       
   389 
       
   390 * hooks are application objects, hence have selectors that should match entity or
       
   391   relation types to which the hook applies. To match a relation type, we use the
       
   392   hook specific `match_rtype` selector.
       
   393 
       
   394 * usage of `set_operation`: instead of adding an operation for each added entity,
       
   395   set_operation allows to create a single one and to store entity's eids to be
       
   396   processed in session's transaction data. This is a good pratice to avoid heavy
       
   397   operations manipulation cost when creating a lot of entities in the same
       
   398   transaction.
       
   399 
       
   400 * the `precommit_event` method of the operation will be called at transaction's
       
   401   commit time.
       
   402 
       
   403 * in a hook, `self._cw` is the repository session, not a web request as usually
       
   404   in views
       
   405 
       
   406 * according to hook's event, you have access to different attributes on the hook
       
   407   instance. Here:
       
   408 
       
   409   - `self.entity` is the newly added entity on 'after_add_entity' events
       
   410 
       
   411   - `self.eidfrom` / `self.eidto` are the eid of the subject / object entity on
       
   412     'after_add_relatiohn' events (you may also get the relation type using
       
   413     `self.rtype`)
       
   414 
       
   415 The `parent` visibility value is used to tell "propagate using parent security"
       
   416 because we want that attribute to be required, so we can't use None value else
       
   417 we'll get an error before we get any chance to propagate...
       
   418 
       
   419 Now, we also want to propagate the `may_be_read_by` relation. Fortunately,
       
   420 CubicWeb provides some base hook classes for such things, so we only have to add
       
   421 the following code to *hooks.py*:
       
   422 
       
   423 .. sourcecode:: python
       
   424 
       
   425     # relations where the "parent" entity is the subject
       
   426     S_RELS = set()
       
   427     # relations where the "parent" entity is the object
       
   428     O_RELS = set(('filed_under', 'comments',))
       
   429 
       
   430     class AddEntitySecurityPropagationHook(hook.PropagateSubjectRelationHook):
       
   431 	"""propagate permissions when new entity are added"""
       
   432 	__regid__ = 'sytweb.addentity_security_propagation'
       
   433 	__select__ = (hook.PropagateSubjectRelationHook.__select__
       
   434 		      & hook.match_rtype_sets(S_RELS, O_RELS))
       
   435 	main_rtype = 'may_be_read_by'
       
   436 	subject_relations = S_RELS
       
   437 	object_relations = O_RELS
       
   438 
       
   439     class AddPermissionSecurityPropagationHook(hook.PropagateSubjectRelationAddHook):
       
   440 	"""propagate permissions when new entity are added"""
       
   441 	__regid__ = 'sytweb.addperm_security_propagation'
       
   442 	__select__ = (hook.PropagateSubjectRelationAddHook.__select__
       
   443 		      & hook.match_rtype('may_be_read_by',))
       
   444 	subject_relations = S_RELS
       
   445 	object_relations = O_RELS
       
   446 
       
   447     class DelPermissionSecurityPropagationHook(hook.PropagateSubjectRelationDelHook):
       
   448 	__regid__ = 'sytweb.delperm_security_propagation'
       
   449 	__select__ = (hook.PropagateSubjectRelationDelHook.__select__
       
   450 		      & hook.match_rtype('may_be_read_by',))
       
   451 	subject_relations = S_RELS
       
   452 	object_relations = O_RELS
       
   453 
       
   454 * the `AddEntitySecurityPropagationHook` will propagate the relation
       
   455   when `filed_under` or `comments` relations are added
       
   456 
       
   457   - the `S_RELS` and `O_RELS` set as well as the `match_rtype_sets` selector are
       
   458     used here so that if my cube is used by another one, it'll be able to
       
   459     configure security propagation by simply adding relation to one of the two
       
   460     sets.
       
   461 
       
   462 * the two others will propagate permissions changes on parent entities to
       
   463   children entities
       
   464 
       
   465 
       
   466 .. _adv_tuto_tesing_security:
       
   467 
       
   468 Step 3: testing our security
       
   469 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   470 
       
   471 Security is tricky. Writing some tests for it is a very good idea. You should
       
   472 even write them first, as Test Driven Development recommends!
       
   473 
       
   474 Here is a small test case that will check the basis of our security
       
   475 model, in *test/unittest_sytweb.py*:
       
   476 
       
   477 .. sourcecode:: python
       
   478 
       
   479     from cubicweb.devtools.testlib import CubicWebTC
       
   480     from cubicweb import Binary
       
   481 
       
   482     class SecurityTC(CubicWebTC):
       
   483 
       
   484 	def test_visibility_propagation(self):
       
   485 	    # create a user for later security checks
       
   486 	    toto = self.create_user('toto')
       
   487 	    # init some data using the default manager connection
       
   488 	    req = self.request()
       
   489 	    folder = req.create_entity('Folder',
       
   490 				       name=u'restricted',
       
   491 				       visibility=u'restricted')
       
   492 	    photo1 = req.create_entity('Image',
       
   493 				       data_name=u'photo1.jpg',
       
   494 				       data=Binary('xxx'),
       
   495 				       filed_under=folder)
       
   496 	    self.commit()
       
   497 	    photo1.clear_all_caches() # good practice, avoid request cache effects
       
   498 	    # visibility propagation
       
   499 	    self.assertEquals(photo1.visibility, 'restricted')
       
   500 	    # unless explicitly specified
       
   501 	    photo2 = req.create_entity('Image',
       
   502 				       data_name=u'photo2.jpg',
       
   503 				       data=Binary('xxx'),
       
   504 				       visibility=u'public',
       
   505 				       filed_under=folder)
       
   506 	    self.commit()
       
   507 	    self.assertEquals(photo2.visibility, 'public')
       
   508 	    # test security
       
   509 	    self.login('toto')
       
   510 	    req = self.request()
       
   511 	    self.assertEquals(len(req.execute('Image X')), 1) # only the public one
       
   512 	    self.assertEquals(len(req.execute('Folder X')), 0) # restricted...
       
   513 	    # may_be_read_by propagation
       
   514 	    self.restore_connection()
       
   515 	    folder.set_relations(may_be_read_by=toto)
       
   516 	    self.commit()
       
   517 	    photo1.clear_all_caches()
       
   518 	    self.failUnless(photo1.may_be_read_by)
       
   519 	    # test security with permissions
       
   520 	    self.login('toto')
       
   521 	    req = self.request()
       
   522 	    self.assertEquals(len(req.execute('Image X')), 2) # now toto has access to photo2
       
   523 	    self.assertEquals(len(req.execute('Folder X')), 1) # and to restricted folder
       
   524 
       
   525     if __name__ == '__main__':
       
   526 	from logilab.common.testlib import unittest_main
       
   527 	unittest_main()
       
   528 
       
   529 It's not complete, but show most things you'll want to do in tests: adding some
       
   530 content, creating users and connecting as them in the test, etc...
       
   531 
       
   532 To run it type:
       
   533 
       
   534 .. sourcecode:: bash
       
   535 
       
   536     $ pytest unittest_sytweb.py
       
   537     ========================  unittest_sytweb.py  ========================
       
   538     -> creating tables [....................]
       
   539     -> inserting default user and default groups.
       
   540     -> storing the schema in the database [....................]
       
   541     -> database for instance data initialized.
       
   542     .
       
   543     ----------------------------------------------------------------------
       
   544     Ran 1 test in 22.547s
       
   545 
       
   546     OK
       
   547 
       
   548 
       
   549 The first execution is taking time, since it creates a sqlite database for the
       
   550 test instance. The second one will be much quicker:
       
   551 
       
   552 .. sourcecode:: bash
       
   553     
       
   554     $ pytest unittest_sytweb.py
       
   555     ========================  unittest_sytweb.py  ========================
       
   556     .
       
   557     ----------------------------------------------------------------------
       
   558     Ran 1 test in 2.662s
       
   559 
       
   560     OK
       
   561 
       
   562 If you do some changes in your schema, you'll have to force regeneration of that
       
   563 database. You do that by removing the tmpdb files before running the test: ::
       
   564 
       
   565     $ rm data/tmpdb*
       
   566 
       
   567 
       
   568 .. Note::
       
   569   pytest is a very convenient utility used to control test execution. It is available from the `logilab-common`_ package.
       
   570 
       
   571 .. _`logilab-common`: http://www.logilab.org/project/logilab-common
       
   572 
       
   573 .. _adv_tuto_migration_script:
       
   574 
       
   575 Step 4: writing the migration script and migrating the instance
       
   576 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   577 
       
   578 Prior to those changes, I  created an instance, feeded it with some data, so I
       
   579 don't want to create a new one, but to migrate the existing one. Let's see how to
       
   580 do that.
       
   581 
       
   582 Migration commands should be put in the cube's *migration* directory, in a
       
   583 file named file:`<X.Y.Z>_Any.py` ('Any' being there mostly for historical reason).
       
   584 
       
   585 Here I'll create a *migration/0.2.0_Any.py* file containing the following
       
   586 instructions:
       
   587 
       
   588 .. sourcecode:: python
       
   589 
       
   590   add_relation_type('may_be_read_by')
       
   591   add_relation_type('visibility')
       
   592   sync_schema_props_perms()
       
   593 
       
   594 Then I update the version number in cube's *__pkginfo__.py* to 0.2.0. And
       
   595 that's it! Those instructions will:
       
   596 
       
   597 * update the instance's schema by adding our two new relations and update the
       
   598   underlying database tables accordingly (the two first instructions)
       
   599 
       
   600 * update schema's permissions definition (the last instruction)
       
   601 
       
   602 
       
   603 To migrate my instance I simply type::
       
   604 
       
   605    cubicweb-ctl upgrade sytweb
       
   606 
       
   607 I'll then be asked some questions to do the migration step by step. You should say
       
   608 YES when it asks if a backup of your database should be done, so you can get back
       
   609 to initial state if anything goes wrong...
       
   610