backport stable

authorSylvain Th?nault <sylvain.thenault@logilab.fr>
changesetef29d3ea3909
branchdefault
phasepublic
hiddenno
parent revision#611663348158 [merge] backport stable fixes into default, #ba51dac1115d [c-c create] unification of c-c create and its subcommands handling
child revision#215275be4877 backport stable to default
files modified by this revision
.hgtags
__pkginfo__.py
cwconfig.py
cwctl.py
dataimport.py
dbapi.py
debian/changelog
debian/control
devtools/__init__.py
doc/book/en/devrepo/testing.rst
entities/authobjs.py
entity.py
etwist/twctl.py
hooks/syncsession.py
req.py
rqlrewrite.py
server/__init__.py
server/edition.py
server/migractions.py
server/querier.py
server/repository.py
server/serverconfig.py
server/serverctl.py
server/session.py
server/sources/__init__.py
server/sources/ldapuser.py
server/sources/native.py
server/sources/storages.py
server/ssplanner.py
server/test/unittest_ldapuser.py
test/unittest_entity.py
test/unittest_rqlrewrite.py
utils.py
web/data/cubicweb.ajax.js
web/data/cubicweb.old.css
web/data/jquery.ui.datepicker-es.js
web/facet.py
web/formwidgets.py
web/test/unittest_reledit.py
web/webconfig.py
web/webctl.py
# HG changeset patch
# User Sylvain Thénault <sylvain.thenault@logilab.fr>
# Date 1301476678 -7200
# Wed Mar 30 11:17:58 2011 +0200
# Node ID ef29d3ea390917579beb591d5b58a39e1242112f
# Parent 61166334815826a4a7cf8ab7b2a3b7cfd7a2c0c8
# Parent ba51dac1115da19cafdf0cf674f4c4ee3647462e
backport stable

diff --git a/.hgtags b/.hgtags
@@ -184,5 +184,7 @@
1  48f468f33704e401a8e7907e258bf1ac61eb8407 cubicweb-version-3.9.x
2  37432cede4fe55b97fc2e9be0a2dd20e8837a848 cubicweb-version-3.11.0
3  8daabda9f571863e8754f8ab722744c417ba3abf cubicweb-debian-version-3.11.0-1
4  d0410eb4d8bbf657d7f32b0c681db09b1f8119a0 cubicweb-version-3.11.1
5  77318f1ec4aae3523d455e884daf3708c3c79af7 cubicweb-debian-version-3.11.1-1
6 +56ae3cd5f8553678a2b1d4121b61241598d0ca68 cubicweb-version-3.11.2
7 +954b5b51cd9278eb45d66be1967064d01ab08453 cubicweb-debian-version-3.11.2-1
diff --git a/__pkginfo__.py b/__pkginfo__.py
@@ -20,11 +20,11 @@
8  software
9  """
10 
11  modname = distname = "cubicweb"
12 
13 -numversion = (3, 11, 1)
14 +numversion = (3, 11, 2)
15  version = '.'.join(str(num) for num in numversion)
16 
17  description = "a repository of entities / relations for knowledge management"
18  author = "Logilab"
19  author_email = "contact@logilab.fr"
@@ -38,11 +38,11 @@
20             'Programming Language :: Python',
21             'Programming Language :: JavaScript',
22  ]
23 
24  __depends__ = {
25 -    'logilab-common': '>= 0.55.0',
26 +    'logilab-common': '>= 0.55.2',
27      'logilab-mtconverter': '>= 0.8.0',
28      'rql': '>= 0.28.0',
29      'yams': '>= 0.30.4',
30      'docutils': '>= 0.6',
31      #gettext                    # for xgettext, msgcat, etc...
diff --git a/cwconfig.py b/cwconfig.py
@@ -1,7 +1,7 @@
32  # -*- coding: utf-8 -*-
33 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
34 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
35  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
36  #
37  # This file is part of CubicWeb.
38  #
39  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -323,11 +323,12 @@
40            }),
41          ('pyro-ns-host',
42           {'type' : 'string',
43            'default': '',
44            'help': 'Pyro name server\'s host. If not set, will be detected by a \
45 -broadcast query. It may contains port information using <host>:<port> notation.',
46 +broadcast query. It may contains port information using <host>:<port> notation. \
47 +Use "NO_PYRONS" to create a Pyro server but not register to a pyro nameserver',
48            'group': 'pyro', 'level': 1,
49            }),
50          ('pyro-ns-group',
51           {'type' : 'string',
52            'default': 'cubicweb',
@@ -841,13 +842,12 @@
53          if not exists(_cubes_init):
54              file(join(_cubes_init), 'w').close()
55          if not exists(_INSTANCES_DIR):
56              os.makedirs(_INSTANCES_DIR)
57 
58 -    # for some commands (creation...) we don't want to initialize gettext
59 -    set_language = True
60 -    # set this to true to allow somethings which would'nt be possible
61 +    # set to true during repair (shell, migration) to allow some things which
62 +    # wouldn't be possible otherwise
63      repairing = False
64 
65      options = CubicWebNoAppConfiguration.options + (
66          ('log-file',
67           {'type' : 'string',
@@ -898,17 +898,17 @@
68          if not exists(mdir):
69              raise ConfigurationError('migration path %s doesn\'t exist' % mdir)
70          return mdir
71 
72      @classmethod
73 -    def config_for(cls, appid, config=None, debugmode=False):
74 +    def config_for(cls, appid, config=None, debugmode=False, creating=False):
75          """return a configuration instance for the given instance identifier
76          """
77          cls.load_available_configs()
78          config = config or guess_configuration(cls.instance_home(appid))
79          configcls = configuration_cls(config)
80 -        return configcls(appid, debugmode)
81 +        return configcls(appid, debugmode, creating)
82 
83      @classmethod
84      def possible_configurations(cls, appid):
85          """return the name of possible configurations for the given
86          instance id
@@ -964,12 +964,10 @@
87              return '/var/log/cubicweb/%s-%s.log' % (self.appid, self.name)
88          else:
89              log_path = os.path.join(_INSTALL_PREFIX, 'var', 'log', 'cubicweb', '%s-%s.log')
90              return log_path % (self.appid, self.name)
91 
92 -
93 -
94      def default_pid_file(self):
95          """return default path to the pid file of the instance'server"""
96          if self.mode == 'system':
97              if _USR_INSTALL:
98                  default = '/var/run/cubicweb/'
@@ -983,12 +981,14 @@
99          rtdir = abspath(os.environ.get('CW_RUNTIME_DIR', default))
100          return join(rtdir, '%s-%s.pid' % (self.appid, self.name))
101 
102      # instance methods used to get instance specific resources #############
103 
104 -    def __init__(self, appid, debugmode=False):
105 +    def __init__(self, appid, debugmode=False, creating=False):
106          self.appid = appid
107 +        # set to true while creating an instance
108 +        self.creating = creating
109          super(CubicWebConfiguration, self).__init__(debugmode)
110          fake_gettext = (unicode, lambda ctx, msgid: unicode(msgid))
111          for lang in self.available_languages():
112              self.translations[lang] = fake_gettext
113          self._cubes = None
@@ -1075,11 +1075,11 @@
114          return hashlib.md5(';'.join(infos)).hexdigest()
115 
116      def load_configuration(self):
117          """load instance's configuration files"""
118          super(CubicWebConfiguration, self).load_configuration()
119 -        if self.apphome and self.set_language:
120 +        if self.apphome and not self.creating:
121              # init gettext
122              self._gettext_init()
123 
124      def _load_site_cubicweb(self, sitefile):
125          # overriden to register cube specific options
diff --git a/cwctl.py b/cwctl.py
@@ -1,6 +1,6 @@
126 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
127 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
128  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
129  #
130  # This file is part of CubicWeb.
131  #
132  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -298,55 +298,65 @@
133                      print 'is not installed, but required by %s' % src
134                  else:
135                      print '* cube %s version %s is installed, but version %s is required by %s' % (
136                          cube, cfgpb.cubes[cube], version, src)
137 
138 +def check_options_consistency(config):
139 +    if config.automatic and config.config_level > 0:
140 +        raise BadCommandUsage('--automatic and --config-level should not be '
141 +                              'used together')
142 +
143  class CreateInstanceCommand(Command):
144      """Create an instance from a cube. This is an unified
145      command which can handle web / server / all-in-one installation
146      according to available parts of the software library and of the
147      desired cube.
148 
149      <cube>
150        the name of cube to use (list available cube names using
151        the "list" command). You can use several cubes by separating
152 -      them using comma (e.g. 'jpl,eemail')
153 +      them using comma (e.g. 'jpl,email')
154      <instance>
155        an identifier for the instance to create
156      """
157      name = 'create'
158      arguments = '<cube> <instance>'
159      min_args = max_args = 2
160      options = (
161 -        ("config-level",
162 +        ('automatic',
163 +         {'short': 'a', 'action' : 'store_true',
164 +          'default': False,
165 +          'help': 'automatic mode: never ask and use default answer to every '
166 +          'question. this may require that your login match a database super '
167 +          'user (allowed to create database & all).',
168 +          }),
169 +        ('config-level',
170           {'short': 'l', 'type' : 'int', 'metavar': '<level>',
171            'default': 0,
172 -          'help': 'configuration level (0..2): 0 will ask for essential \
173 -configuration parameters only while 2 will ask for all parameters',
174 -          }
175 -         ),
176 -        ("config",
177 +          'help': 'configuration level (0..2): 0 will ask for essential '
178 +          'configuration parameters only while 2 will ask for all parameters',
179 +          }),
180 +        ('config',
181           {'short': 'c', 'type' : 'choice', 'metavar': '<install type>',
182            'choices': ('all-in-one', 'repository', 'twisted'),
183            'default': 'all-in-one',
184 -          'help': 'installation type, telling which part of an instance \
185 -should be installed. You can list available configurations using the "list" \
186 -command. Default to "all-in-one", e.g. an installation embedding both the RQL \
187 -repository and the web server.',
188 -          }
189 -         ),
190 +          'help': 'installation type, telling which part of an instance '
191 +          'should be installed. You can list available configurations using the'
192 +          ' "list" command. Default to "all-in-one", e.g. an installation '
193 +          'embedding both the RQL repository and the web server.',
194 +          }),
195          )
196 
197      def run(self, args):
198          """run the command with its specific arguments"""
199          from logilab.common.textutils import splitstrip
200 +        check_options_consistency(self.config)
201          configname = self.config.config
202          cubes, appid = args
203          cubes = splitstrip(cubes)
204          # get the configuration and helper
205 -        config = cwcfg.config_for(appid, configname)
206 -        config.set_language = False
207 +        config = cwcfg.config_for(appid, configname, creating=True)
208          cubes = config.expand_cubes(cubes)
209          config.init_cubes(cubes)
210          helper = self.config_helper(config)
211          # check the cube exists
212          try:
@@ -359,19 +369,23 @@
213              return
214          # create the registry directory for this instance
215          print '\n'+underline_title('Creating the instance %s' % appid)
216          create_dir(config.apphome)
217          # cubicweb-ctl configuration
218 -        print '\n'+underline_title('Configuring the instance (%s.conf)' % configname)
219 -        config.input_config('main', self.config.config_level)
220 +        if not self.config.automatic:
221 +            print '\n'+underline_title('Configuring the instance (%s.conf)'
222 +                                       % configname)
223 +            config.input_config('main', self.config.config_level)
224          # configuration'specific stuff
225          print
226 -        helper.bootstrap(cubes, self.config.config_level)
227 +        helper.bootstrap(cubes, self.config.automatic, self.config.config_level)
228          # input for cubes specific options
229 -        for section in set(sect.lower() for sect, opt, optdict in config.all_options()
230 -                           if optdict.get('level') <= self.config.config_level):
231 -            if section not in ('main', 'email', 'pyro'):
232 +        sections = set(sect.lower() for sect, opt, odict in config.all_options()
233 +                       if 'type' in odict
234 +                       and odict.get('level') <= self.config.config_level)
235 +        for section in sections:
236 +            if section not in ('main', 'email', 'pyro', 'web'):
237                  print '\n' + underline_title('%s options' % section)
238                  config.input_config(section, self.config.config_level)
239          # write down configuration
240          config.save()
241          self._handle_win32(config, appid)
@@ -382,12 +396,13 @@
242          from cubicweb import i18n
243          langs = [lang for lang, _ in i18n.available_catalogs(join(templdirs[0], 'i18n'))]
244          errors = config.i18ncompile(langs)
245          if errors:
246              print '\n'.join(errors)
247 -            if not ASK.confirm('error while compiling message catalogs, '
248 -                               'continue anyway ?'):
249 +            if self.config.automatic \
250 +                   or not ASK.confirm('error while compiling message catalogs, '
251 +                                      'continue anyway ?'):
252                  print 'creation not completed'
253                  return
254          # create the additional data directory for this instance
255          if config.appdatahome != config.apphome: # true in dev mode
256              create_dir(config.appdatahome)
@@ -396,11 +411,11 @@
257              from logilab.common.shellutils import chown
258              # this directory should be owned by the uid of the server process
259              print 'set %s as owner of the data directory' % config['uid']
260              chown(config.appdatahome, config['uid'])
261          print '\n-> creation done for %r.\n' % config.apphome
262 -        helper.postcreate()
263 +        helper.postcreate(self.config.automatic)
264 
265      def _handle_win32(self, config, appid):
266          if sys.platform != 'win32':
267              return
268          service_template = """
diff --git a/dataimport.py b/dataimport.py
@@ -1,7 +1,7 @@
269  # -*- coding: utf-8 -*-
270 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
271 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
272  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
273  #
274  # This file is part of CubicWeb.
275  #
276  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -79,11 +79,11 @@
277  from logilab.common.date import strptime
278  from logilab.common.decorators import cached
279  from logilab.common.deprecation import deprecated
280 
281  from cubicweb.server.utils import eschema_eid
282 -from cubicweb.server.ssplanner import EditedEntity
283 +from cubicweb.server.edition import EditedEntity
284 
285  def count_lines(stream_or_filename):
286      if isinstance(stream_or_filename, basestring):
287          f = open(stream_or_filename)
288      else:
@@ -471,10 +471,15 @@
289          eid_from, rtype, eid_to = super(RQLObjectStore, self).relate(
290              eid_from, rtype, eid_to)
291          self.rql('SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % rtype,
292                   {'x': int(eid_from), 'y': int(eid_to)})
293 
294 +    def find_entities(self, *args, **kwargs):
295 +        return self.session.find_entities(*args, **kwargs)
296 +
297 +    def find_one_entity(self, *args, **kwargs):
298 +        return self.session.find_one_entity(*args, **kwargs)
299 
300  # the import controller ########################################################
301 
302  class CWImportController(object):
303      """Controller of the data import process.
diff --git a/dbapi.py b/dbapi.py
@@ -1,6 +1,6 @@
304 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
305 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
306  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
307  #
308  # This file is part of CubicWeb.
309  #
310  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -100,15 +100,18 @@
311          # get local access to the repository
312          from cubicweb.server.repository import Repository
313          return Repository(config, vreg=vreg)
314      else: # method == 'pyro'
315          # resolve the Pyro object
316 -        from logilab.common.pyro_ext import ns_get_proxy
317 +        from logilab.common.pyro_ext import ns_get_proxy, get_proxy
318          pyroid = database or config['pyro-instance-id'] or config.appid
319          try:
320 -            return ns_get_proxy(pyroid, defaultnsgroup=config['pyro-ns-group'],
321 -                                nshost=config['pyro-ns-host'])
322 +            if config['pyro-ns-host'] == 'NO_PYRONS':
323 +                return get_proxy(pyroid)
324 +            else:
325 +                return ns_get_proxy(pyroid, defaultnsgroup=config['pyro-ns-group'],
326 +                                    nshost=config['pyro-ns-host'])
327          except Exception, ex:
328              raise ConnectionError(str(ex))
329 
330  def repo_connect(repo, login, **kwargs):
331      """Constructor to create a new connection to the CubicWeb repository.
diff --git a/debian/changelog b/debian/changelog
@@ -1,5 +1,11 @@
332 +cubicweb (3.11.2-1) unstable; urgency=low
333 +
334 +  * new upstream release 
335 +
336 + -- Nicolas Chauvat <nicolas.chauvat@logilab.fr>  Mon, 28 Mar 2011 19:18:54 +0200
337 +
338  cubicweb (3.11.1-1) unstable; urgency=low
339 
340    * new upstream release
341 
342   -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Mon, 07 Mar 2011 17:21:28 +0100
diff --git a/debian/control b/debian/control
@@ -95,11 +95,11 @@
343 
344 
345  Package: cubicweb-common
346  Architecture: all
347  XB-Python-Version: ${python:Versions}
348 -Depends: ${misc:Depends}, ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.55.0), python-yams (>= 0.30.4), python-rql (>= 0.28.0), python-lxml
349 +Depends: ${misc:Depends}, ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.55.2), python-yams (>= 0.30.4), python-rql (>= 0.28.0), python-lxml
350  Recommends: python-simpletal (>= 4.0), python-crypto
351  Conflicts: cubicweb-core
352  Replaces: cubicweb-core
353  Description: common library for the CubicWeb framework
354   CubicWeb is a semantic web application framework.
diff --git a/devtools/__init__.py b/devtools/__init__.py
@@ -1,6 +1,6 @@
355 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
356 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
357  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
358  #
359  # This file is part of CubicWeb.
360  #
361  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -125,11 +125,10 @@
362          repo._needs_refresh = False
363 
364 
365  class TestServerConfiguration(ServerConfiguration):
366      mode = 'test'
367 -    set_language = False
368      read_instance_schema = False
369      init_repository = True
370 
371      def __init__(self, appid='data', apphome=None, log_threshold=logging.CRITICAL+10):
372          # must be set before calling parent __init__
@@ -495,11 +494,12 @@
373 
374      @property
375      @cached
376      def dbcnx(self):
377          from cubicweb.server.serverctl import _db_sys_cnx
378 -        return  _db_sys_cnx(self.system_source, 'CREATE DATABASE and / or USER', verbose=0)
379 +        return  _db_sys_cnx(self.system_source, 'CREATE DATABASE and / or USER',
380 +                            interactive=False)
381 
382      @property
383      @cached
384      def cursor(self):
385          return self.dbcnx.cursor()
@@ -512,11 +512,12 @@
386          try:
387              self._drop(self.dbname)
388 
389              createdb(self.helper, self.system_source, self.dbcnx, self.cursor)
390              self.dbcnx.commit()
391 -            cnx = system_source_cnx(self.system_source, special_privs='LANGUAGE C', verbose=0)
392 +            cnx = system_source_cnx(self.system_source, special_privs='LANGUAGE C',
393 +                                    interactive=False)
394              templcursor = cnx.cursor()
395              try:
396                  # XXX factorize with db-create code
397                  self.helper.init_fti_extensions(templcursor)
398                  # install plpythonu/plpgsql language if not installed by the cube
diff --git a/doc/book/en/devrepo/testing.rst b/doc/book/en/devrepo/testing.rst
@@ -57,14 +57,14 @@
399 
400      class ClassificationHooksTC(CubicWebTC):
401 
402          def setup_database(self):
403              req = self.request()
404 -            group_etype = req.execute('Any X WHERE X name "CWGroup"').get_entity(0,0)
405 +            group_etype = req.find_one_entity('CWEType', name='CWGroup')
406              c1 = req.create_entity('Classification', name=u'classif1',
407                                     classifies=group_etype)
408 -            user_etype = req.execute('Any X WHERE X name "CWUser"').get_entity(0,0)
409 +            user_etype = req.find_one_entity('CWEType', name='CWUser')
410              c2 = req.create_entity('Classification', name=u'classif2',
411                                     classifies=user_etype)
412              self.kw1 = req.create_entity('Keyword', name=u'kwgroup', included_in=c1)
413              self.kw2 = req.create_entity('Keyword', name=u'kwuser', included_in=c2)
414 
@@ -226,11 +226,11 @@
415                                             reverse_is_chair_at=chair,
416                                             reverse_is_reviewer_at=reviewer)
417 
418          def test_admin(self):
419              req = self.request()
420 -            rset = req.execute('Any C WHERE C is Conference')
421 +            rset = req.find_entities('Conference')
422              self.assertListEqual(self.pactions(req, rset),
423                                    [('workflow', workflow.WorkflowActions),
424                                     ('edit', confactions.ModifyAction),
425                                     ('managepermission', actions.ManagePermissionsAction),
426                                     ('addrelated', actions.AddRelatedActions),
diff --git a/entities/authobjs.py b/entities/authobjs.py
@@ -1,6 +1,6 @@
427 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
428 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
429  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
430  #
431  # This file is part of CubicWeb.
432  #
433  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -78,10 +78,24 @@
434          except ValueError:
435              self.warning('incorrect value for eproperty %s of user %s',
436                           key, self.login)
437          return self._cw.vreg.property_value(key)
438 
439 +    def set_property(self, pkey, value):
440 +        value = unicode(value)
441 +        try:
442 +            prop = self._cw.execute(
443 +                'CWProperty X WHERE X pkey %(k)s, X for_user U, U eid %(u)s',
444 +                {'k': pkey, 'u': self.eid}).get_entity(0, 0)
445 +        except:
446 +            kwargs = dict(pkey=unicode(pkey), value=value)
447 +            if self.is_in_group('managers'):
448 +                kwargs['for_user'] = self
449 +            self._cw.create_entity('CWProperty', **kwargs)
450 +        else:
451 +            prop.set_attributes(value=value)
452 +
453      def matching_groups(self, groups):
454          """return the number of the given group(s) in which the user is
455 
456          :type groups: str or iterable(str)
457          :param groups: a group name or an iterable on group names
diff --git a/entity.py b/entity.py
@@ -60,11 +60,10 @@
458      if value == u'' or u'?' in value or u'/' in value or u'&' in value:
459          return False
460      return True
461 
462 
463 -
464  class Entity(AppObject):
465      """an entity instance has e_schema automagically set on
466      the class and instances has access to their issuing cursor.
467 
468      A property is set for each attribute and relation on each entity's type
@@ -806,11 +805,15 @@
469              if not self.has_eid():
470                  existant = searchedvar
471              else:
472                  existant = None # instead of 'SO', improve perfs
473              for select in rqlst.children:
474 -                rewriter.rewrite(select, [((searchedvar, searchedvar), rqlexprs)],
475 +                varmap = {}
476 +                for var in 'SO':
477 +                    if var in select.defined_vars:
478 +                        varmap[var] = var
479 +                rewriter.rewrite(select, [(varmap, rqlexprs)],
480                                   select.solutions, args, existant)
481              rql = rqlst.as_string()
482          return rql, args
483 
484      def unrelated(self, rtype, targettype, role='subject', limit=None,
diff --git a/etwist/twctl.py b/etwist/twctl.py
@@ -1,6 +1,6 @@
485 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
486 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
487  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
488  #
489  # This file is part of CubicWeb.
490  #
491  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -49,14 +49,14 @@
492          """configuration to get an instance running in a twisted web server
493          integrating a repository server in the same process
494          """
495          cfgname = 'all-in-one'
496 
497 -        def bootstrap(self, cubes, inputlevel=0):
498 +        def bootstrap(self, cubes, automatic=False, inputlevel=0):
499              """bootstrap this configuration"""
500 -            serverctl.RepositoryCreateHandler.bootstrap(self, cubes, inputlevel)
501 -            TWCreateHandler.bootstrap(self, cubes, inputlevel)
502 +            serverctl.RepositoryCreateHandler.bootstrap(self, cubes, automatic, inputlevel)
503 +            TWCreateHandler.bootstrap(self, cubes, automatic, inputlevel)
504 
505      class AllInOneStartHandler(TWStartHandler):
506          cmdname = 'start'
507          cfgname = 'all-in-one'
508          subcommand = 'cubicweb-twisted'
diff --git a/hooks/syncsession.py b/hooks/syncsession.py
@@ -1,6 +1,6 @@
509 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
510 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
511  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
512  #
513  # This file is part of CubicWeb.
514  #
515  # CubicWeb is free software: you can redistribute it and/or modify it under the
diff --git a/req.py b/req.py
@@ -1,6 +1,6 @@
516 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
517 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
518  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
519  #
520  # This file is part of CubicWeb.
521  #
522  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -33,10 +33,12 @@
523  from cubicweb.rset import ResultSet
524 
525  ONESECOND = timedelta(0, 1, 0)
526  CACHE_REGISTRY = {}
527 
528 +class FindEntityError(Exception):
529 +    """raised when find_one_entity() can not return one and only one entity"""
530 
531  def _check_cw_unsafe(kwargs):
532      if kwargs.pop('_cw_unsafe', False):
533          warn('[3.7] _cw_unsafe argument is deprecated, now unsafe by '
534               'default, control it using cw_[read|write]_security.',
@@ -138,10 +140,37 @@
535          """
536          _check_cw_unsafe(kwargs)
537          cls = self.vreg['etypes'].etype_class(etype)
538          return cls.cw_instantiate(self.execute, **kwargs)
539 
540 +    def find_entities(self, etype, **kwargs):
541 +        """find entities of the given type and attribute values.
542 +
543 +        >>> users = find_entities('CWGroup', name=u'users')
544 +        >>> groups = find_entities('CWGroup')
545 +        """
546 +        parts = ['Any X WHERE X is %s' % etype]
547 +        parts.extend('X %(attr)s %%(%(attr)s)s' % {'attr': attr} for attr in kwargs)
548 +        return self.execute(', '.join(parts), kwargs).entities()
549 +
550 +    def find_one_entity(self, etype, **kwargs):
551 +        """find one entity of the given type and attribute values.
552 +        raise :exc:`FindEntityError` if can not return one and only one entity.
553 +
554 +        >>> users = find_one_entity('CWGroup', name=u'users')
555 +        >>> groups = find_one_entity('CWGroup')
556 +        Exception()
557 +        """
558 +        parts = ['Any X WHERE X is %s' % etype]
559 +        parts.extend('X %(attr)s %%(%(attr)s)s' % {'attr': attr} for attr in kwargs)
560 +        rql = ', '.join(parts)
561 +        rset = self.execute(rql, kwargs)
562 +        if len(rset) != 1:
563 +            raise FindEntityError('Found %i entitie(s) when 1 was expected (rql=%s ; %s)'
564 +                                  % (len(rset), rql, repr(kwargs)))
565 +        return rset.get_entity(0,0)
566 +
567      def ensure_ro_rql(self, rql):
568          """raise an exception if the given rql is not a select query"""
569          first = rql.split(None, 1)[0].lower()
570          if first in ('insert', 'set', 'delete'):
571              raise Unauthorized(self._('only select queries are authorized'))
diff --git a/rqlrewrite.py b/rqlrewrite.py
@@ -1,6 +1,6 @@
572 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
573 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
574  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
575  #
576  # This file is part of CubicWeb.
577  #
578  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -22,10 +22,11 @@
579  """
580 
581  __docformat__ = "restructuredtext en"
582 
583  from rql import nodes as n, stmts, TypeResolverException
584 +from rql.utils import common_parent
585  from yams import BadSchemaDefinition
586  from logilab.common.graph import has_path
587 
588  from cubicweb import Unauthorized, typed_eid
589 
@@ -178,60 +179,70 @@
590          add_types_restriction(self.schema, select)
591 
592      def insert_snippets(self, snippets, varexistsmap=None):
593          self.rewritten = {}
594          for varmap, rqlexprs in snippets:
595 +            if isinstance(varmap, dict):
596 +                varmap = tuple(sorted(varmap.items()))
597 +            else:
598 +                assert isinstance(varmap, tuple), varmap
599              if varexistsmap is not None and not varmap in varexistsmap:
600                  continue
601 -            self.varmap = varmap
602 -            selectvar, snippetvar = varmap
603 +            self.insert_varmap_snippets(varmap, rqlexprs, varexistsmap)
604 +
605 +    def insert_varmap_snippets(self, varmap, rqlexprs, varexistsmap):
606 +        self.varmap = varmap
607 +        self.revvarmap = {}
608 +        self.varinfos = []
609 +        for i, (selectvar, snippetvar) in enumerate(varmap):
610              assert snippetvar in 'SOX'
611 -            self.revvarmap = {snippetvar: selectvar}
612 -            self.varinfo = vi = {}
613 +            self.revvarmap[snippetvar] = (selectvar, i)
614 +            vi = {}
615 +            self.varinfos.append(vi)
616              try:
617                  vi['const'] = typed_eid(selectvar) # XXX gae
618                  vi['rhs_rels'] = vi['lhs_rels'] = {}
619              except ValueError:
620                  try:
621                      vi['stinfo'] = sti = self.select.defined_vars[selectvar].stinfo
622                  except KeyError:
623 -                    # variable has been moved to a newly inserted subquery
624 +                    # variable may have been moved to a newly inserted subquery
625                      # we should insert snippet in that subquery
626                      subquery = self.select.aliases[selectvar].query
627                      assert len(subquery.children) == 1
628                      subselect = subquery.children[0]
629                      RQLRewriter(self.session).rewrite(subselect, [(varmap, rqlexprs)],
630                                                        subselect.solutions, self.kwargs)
631 -                    continue
632 +                    return
633                  if varexistsmap is None:
634                      vi['rhs_rels'] = dict( (r.r_type, r) for r in sti['rhsrelations'])
635                      vi['lhs_rels'] = dict( (r.r_type, r) for r in sti['relations']
636                                             if not r in sti['rhsrelations'])
637                  else:
638                      vi['rhs_rels'] = vi['lhs_rels'] = {}
639 -            parent = None
640 -            inserted = False
641 -            for rqlexpr in rqlexprs:
642 -                self.current_expr = rqlexpr
643 -                if varexistsmap is None:
644 -                    try:
645 -                        new = self.insert_snippet(varmap, rqlexpr.snippet_rqlst, parent)
646 -                    except Unsupported:
647 -                        continue
648 -                    inserted = True
649 -                    if new is not None:
650 -                        self.exists_snippet[rqlexpr] = new
651 -                    parent = parent or new
652 -                else:
653 -                    # called to reintroduce snippet due to ambiguity creation,
654 -                    # so skip snippets which are not introducing this ambiguity
655 -                    exists = varexistsmap[varmap]
656 -                    if self.exists_snippet[rqlexpr] is exists:
657 -                        self.insert_snippet(varmap, rqlexpr.snippet_rqlst, exists)
658 -            if varexistsmap is None and not inserted:
659 -                # no rql expression found matching rql solutions. User has no access right
660 -                raise Unauthorized()
661 +        parent = None
662 +        inserted = False
663 +        for rqlexpr in rqlexprs:
664 +            self.current_expr = rqlexpr
665 +            if varexistsmap is None:
666 +                try:
667 +                    new = self.insert_snippet(varmap, rqlexpr.snippet_rqlst, parent)
668 +                except Unsupported:
669 +                    continue
670 +                inserted = True
671 +                if new is not None:
672 +                    self.exists_snippet[rqlexpr] = new
673 +                parent = parent or new
674 +            else:
675 +                # called to reintroduce snippet due to ambiguity creation,
676 +                # so skip snippets which are not introducing this ambiguity
677 +                exists = varexistsmap[varmap]
678 +                if self.exists_snippet[rqlexpr] is exists:
679 +                    self.insert_snippet(varmap, rqlexpr.snippet_rqlst, exists)
680 +        if varexistsmap is None and not inserted:
681 +            # no rql expression found matching rql solutions. User has no access right
682 +            raise Unauthorized() # XXX bad constraint when inserting constraints
683 
684      def insert_snippet(self, varmap, snippetrqlst, parent=None):
685          new = snippetrqlst.where.accept(self)
686          existing = self.existingvars
687          self.existingvars = None
@@ -241,20 +252,27 @@
688              self.existingvars = existing
689 
690      def _insert_snippet(self, varmap, parent, new):
691          if new is not None:
692              if self._insert_scope is None:
693 -                insert_scope = self.varinfo.get('stinfo', {}).get('scope', self.select)
694 +                insert_scope = None
695 +                for vi in self.varinfos:
696 +                    scope = vi.get('stinfo', {}).get('scope', self.select)
697 +                    if insert_scope is None:
698 +                        insert_scope = scope
699 +                    else:
700 +                        insert_scope = common_parent(scope, insert_scope)
701              else:
702                  insert_scope = self._insert_scope
703 -            if self.varinfo.get('stinfo', {}).get('optrelations'):
704 +            if any(vi.get('stinfo', {}).get('optrelations') for vi in self.varinfos):
705                  assert parent is None
706                  self._insert_scope = self.snippet_subquery(varmap, new)
707                  self.insert_pending()
708                  self._insert_scope = None
709                  return
710 -            new = n.Exists(new)
711 +            if not isinstance(new, (n.Exists, n.Not)):
712 +                new = n.Exists(new)
713              if parent is None:
714                  insert_scope.add_restriction(new)
715              else:
716                  grandpa = parent.parent
717                  or_ = n.Or(parent, new)
@@ -289,11 +307,11 @@
718              key, action = self.pending_keys.pop()
719              try:
720                  varname = self.rewritten[key]
721              except KeyError:
722                  try:
723 -                    varname = self.revvarmap[key[-1]]
724 +                    varname = self.revvarmap[key[-1]][0]
725                  except KeyError:
726                      # variable isn't used anywhere else, we can't insert security
727                      raise Unauthorized()
728              ptypes = self.select.defined_vars[varname].stinfo['possibletypes']
729              if len(ptypes) > 1:
@@ -306,49 +324,55 @@
730              eschema = self.schema.eschema(etype)
731              if not eschema.has_perm(self.session, action):
732                  rqlexprs = eschema.get_rqlexprs(action)
733                  if not rqlexprs:
734                      raise Unauthorized()
735 -                self.insert_snippets([((varname, 'X'), rqlexprs)])
736 +                self.insert_snippets([({varname: 'X'}, rqlexprs)])
737 
738      def snippet_subquery(self, varmap, transformedsnippet):
739          """introduce the given snippet in a subquery"""
740          subselect = stmts.Select()
741 -        selectvar = varmap[0]
742 -        subselectvar = subselect.get_variable(selectvar)
743 -        subselect.append_selected(n.VariableRef(subselectvar))
744          snippetrqlst = n.Exists(transformedsnippet.copy(subselect))
745 -        aliases = [selectvar]
746 -        stinfo = self.varinfo['stinfo']
747 -        need_null_test = False
748 -        for rel in stinfo['relations']:
749 -            rschema = self.schema.rschema(rel.r_type)
750 -            if rschema.final or (rschema.inlined and
751 -                                 not rel in stinfo['rhsrelations']):
752 -                rel.children[0].name = selectvar # XXX explain why
753 -                subselect.add_restriction(rel.copy(subselect))
754 -                for vref in rel.children[1].iget_nodes(n.VariableRef):
755 -                    if isinstance(vref.variable, n.ColumnAlias):
756 -                        # XXX could probably be handled by generating the subquery
757 -                        # into the detected subquery
758 -                        raise BadSchemaDefinition(
759 -                            "cant insert security because of usage two inlined "
760 -                            "relations in this query. You should probably at "
761 -                            "least uninline %s" % rel.r_type)
762 -                    subselect.append_selected(vref.copy(subselect))
763 -                    aliases.append(vref.name)
764 -                self.select.remove_node(rel)
765 -                # when some inlined relation has to be copied in the subquery,
766 -                # we need to test that either value is NULL or that the snippet
767 -                # condition is satisfied
768 -                if rschema.inlined and rel.optional:
769 -                    need_null_test = True
770 -        if need_null_test:
771 -            snippetrqlst = n.Or(
772 -                n.make_relation(subselectvar, 'is', (None, None), n.Constant,
773 -                                operator='='),
774 -                snippetrqlst)
775 +        aliases = []
776 +        rels_done = set()
777 +        for i, (selectvar, snippetvar) in enumerate(varmap):
778 +            subselectvar = subselect.get_variable(selectvar)
779 +            subselect.append_selected(n.VariableRef(subselectvar))
780 +            aliases.append(selectvar)
781 +            vi = self.varinfos[i]
782 +            need_null_test = False
783 +            stinfo = vi['stinfo']
784 +            for rel in stinfo['relations']:
785 +                if rel in rels_done:
786 +                    continue
787 +                rels_done.add(rel)
788 +                rschema = self.schema.rschema(rel.r_type)
789 +                if rschema.final or (rschema.inlined and
790 +                                     not rel in stinfo['rhsrelations']):
791 +                    rel.children[0].name = selectvar # XXX explain why
792 +                    subselect.add_restriction(rel.copy(subselect))
793 +                    for vref in rel.children[1].iget_nodes(n.VariableRef):
794 +                        if isinstance(vref.variable, n.ColumnAlias):
795 +                            # XXX could probably be handled by generating the
796 +                            # subquery into the detected subquery
797 +                            raise BadSchemaDefinition(
798 +                                "cant insert security because of usage two inlined "
799 +                                "relations in this query. You should probably at "
800 +                                "least uninline %s" % rel.r_type)
801 +                        subselect.append_selected(vref.copy(subselect))
802 +                        aliases.append(vref.name)
803 +                    self.select.remove_node(rel)
804 +                    # when some inlined relation has to be copied in the
805 +                    # subquery, we need to test that either value is NULL or
806 +                    # that the snippet condition is satisfied
807 +                    if rschema.inlined and rel.optional:
808 +                        need_null_test = True
809 +            if need_null_test:
810 +                snippetrqlst = n.Or(
811 +                    n.make_relation(subselectvar, 'is', (None, None), n.Constant,
812 +                                    operator='='),
813 +                    snippetrqlst)
814          subselect.add_restriction(snippetrqlst)
815          if self.u_varname:
816              # generate an identifier for the substitution
817              argname = subselect.allocate_varname()
818              while argname in self.kwargs:
@@ -431,39 +455,41 @@
819              vref.unregister_reference()
820              if not vref.variable.stinfo['references']:
821                  # no more references, undefine the variable
822                  del self.select.defined_vars[vref.name]
823 
824 -    def _may_be_shared_with(self, sniprel, target, searchedvarname):
825 +    def _may_be_shared_with(self, sniprel, target):
826          """if the snippet relation can be skipped to use a relation from the
827          original query, return that relation node
828          """
829          rschema = self.schema.rschema(sniprel.r_type)
830 -        try:
831 -            if target == 'object':
832 -                orel = self.varinfo['lhs_rels'][sniprel.r_type]
833 -                cardindex = 0
834 -                ttypes_func = rschema.objects
835 -                rdef = rschema.rdef
836 -            else: # target == 'subject':
837 -                orel = self.varinfo['rhs_rels'][sniprel.r_type]
838 -                cardindex = 1
839 -                ttypes_func = rschema.subjects
840 -                rdef = lambda x, y: rschema.rdef(y, x)
841 -        except KeyError:
842 -            # may be raised by self.varinfo['xhs_rels'][sniprel.r_type]
843 -            return None
844 -        # can't share neged relation or relations with different outer join
845 -        if (orel.neged(strict=True) or sniprel.neged(strict=True)
846 -            or (orel.optional and orel.optional != sniprel.optional)):
847 -            return None
848 -        # if cardinality is in '?1', we can ignore the snippet relation and use
849 -        # variable from the original query
850 -        for etype in self.varinfo['stinfo']['possibletypes']:
851 -            for ttype in ttypes_func(etype):
852 -                if rdef(etype, ttype).cardinality[cardindex] in '+*':
853 -                    return None
854 +        for vi in self.varinfos:
855 +            try:
856 +                if target == 'object':
857 +                    orel = vi['lhs_rels'][sniprel.r_type]
858 +                    cardindex = 0
859 +                    ttypes_func = rschema.objects
860 +                    rdef = rschema.rdef
861 +                else: # target == 'subject':
862 +                    orel = vi['rhs_rels'][sniprel.r_type]
863 +                    cardindex = 1
864 +                    ttypes_func = rschema.subjects
865 +                    rdef = lambda x, y: rschema.rdef(y, x)
866 +            except KeyError:
867 +                # may be raised by vi['xhs_rels'][sniprel.r_type]
868 +                return None
869 +            # can't share neged relation or relations with different outer join
870 +            if (orel.neged(strict=True) or sniprel.neged(strict=True)
871 +                or (orel.optional and orel.optional != sniprel.optional)):
872 +                return None
873 +            # if cardinality is in '?1', we can ignore the snippet relation and use
874 +            # variable from the original query
875 +            for etype in vi['stinfo']['possibletypes']:
876 +                for ttype in ttypes_func(etype):
877 +                    if rdef(etype, ttype).cardinality[cardindex] in '+*':
878 +                        return None
879 +            break
880          return orel
881 
882      def _use_orig_term(self, snippet_varname, term):
883          key = (self.current_expr, self.varmap, snippet_varname)
884          if key in self.rewritten:
@@ -558,16 +584,16 @@
885              return
886          if isinstance(rhs, n.VariableRef):
887              if self.existingvars and not self.keep_var(rhs.name):
888                  return
889              if lhs.name in self.revvarmap and rhs.name != 'U':
890 -                orel = self._may_be_shared_with(node, 'object', lhs.name)
891 +                orel = self._may_be_shared_with(node, 'object')
892                  if orel is not None:
893                      self._use_orig_term(rhs.name, orel.children[1].children[0])
894                      return
895              elif rhs.name in self.revvarmap and lhs.name != 'U':
896 -                orel = self._may_be_shared_with(node, 'subject', rhs.name)
897 +                orel = self._may_be_shared_with(node, 'subject')
898                  if orel is not None:
899                      self._use_orig_term(lhs.name, orel.children[0])
900                      return
901          rel = n.Relation(node.r_type, node.optional)
902          for c in node.children:
@@ -598,14 +624,15 @@
903          return n.Constant(node.value, node.type)
904 
905      def visit_variableref(self, node):
906          """get the sql name for a variable reference"""
907          if node.name in self.revvarmap:
908 -            if self.varinfo.get('const') is not None:
909 -                return n.Constant(self.varinfo['const'], 'Int') # XXX gae
910 -            return n.VariableRef(self.select.get_variable(
911 -                self.revvarmap[node.name]))
912 +            selectvar, index = self.revvarmap[node.name]
913 +            vi = self.varinfos[index]
914 +            if vi.get('const') is not None:
915 +                return n.Constant(vi['const'], 'Int') # XXX gae
916 +            return n.VariableRef(self.select.get_variable(selectvar))
917          vname_or_term = self._get_varname_or_term(node.name)
918          if isinstance(vname_or_term, basestring):
919              return n.VariableRef(self.select.get_variable(vname_or_term))
920          # shared term
921          return vname_or_term.copy(self.select)
diff --git a/server/__init__.py b/server/__init__.py
@@ -1,6 +1,6 @@
922 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
923 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
924  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
925  #
926  # This file is part of CubicWeb.
927  #
928  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -127,11 +127,10 @@
929      from cubicweb.server.sqlutils import sqlexec, sqlschema, sqldropschema
930      # configuration to avoid db schema loading and user'state checking
931      # on connection
932      config.creating = True
933      config.consider_user_state = False
934 -    config.set_language = False
935      # only enable the system source at initialization time
936      repo = Repository(config, vreg=vreg)
937      schema = repo.schema
938      sourcescfg = config.sources()
939      _title = '-> creating tables '
@@ -208,11 +207,10 @@
940      session.close()
941      repo.shutdown()
942      # restore initial configuration
943      config.creating = False
944      config.consider_user_state = True
945 -    config.set_language = True
946      print '-> database for instance %s initialized.' % config.appid
947 
948 
949  def initialize_schema(config, schema, mhandler, event='create'):
950      from cubicweb.server.schemaserial import serialize_schema
diff --git a/server/edition.py b/server/edition.py
@@ -0,0 +1,150 @@
951 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
952 +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
953 +#
954 +# This file is part of CubicWeb.
955 +#
956 +# CubicWeb is free software: you can redistribute it and/or modify it under the
957 +# terms of the GNU Lesser General Public License as published by the Free
958 +# Software Foundation, either version 2.1 of the License, or (at your option)
959 +# any later version.
960 +#
961 +# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
962 +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
963 +# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
964 +# details.
965 +#
966 +# You should have received a copy of the GNU Lesser General Public License along
967 +# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
968 +"""helper classes to handle server-side edition of entities"""
969 +
970 +from __future__ import with_statement
971 +
972 +__docformat__ = "restructuredtext en"
973 +
974 +from copy import copy
975 +from yams import ValidationError
976 +
977 +
978 +_MARKER = object()
979 +
980 +class dict_protocol_catcher(object):
981 +    def __init__(self, entity):
982 +        self.__entity = entity
983 +    def __getitem__(self, attr):
984 +        return self.__entity.cw_edited[attr]
985 +    def __setitem__(self, attr, value):
986 +        self.__entity.cw_edited[attr] = value
987 +    def __getattr__(self, attr):
988 +        return getattr(self.__entity, attr)
989 +
990 +
991 +class EditedEntity(dict):
992 +    """encapsulate entities attributes being written by an RQL query"""
993 +    def __init__(self, entity, **kwargs):
994 +        dict.__init__(self, **kwargs)
995 +        self.entity = entity
996 +        self.skip_security = set()
997 +        self.querier_pending_relations = {}
998 +        self.saved = False
999 +
1000 +    def __hash__(self):
1001 +        # dict|set keyable
1002 +        return hash(id(self))
1003 +
1004 +    def __cmp__(self, other):
1005 +        # we don't want comparison by value inherited from dict
1006 +        return cmp(id(self), id(other))
1007 +
1008 +    def __setitem__(self, attr, value):
1009 +        assert attr != 'eid'
1010 +        # don't add attribute into skip_security if already in edited
1011 +        # attributes, else we may accidentaly skip a desired security check
1012 +        if attr not in self:
1013 +            self.skip_security.add(attr)
1014 +        self.edited_attribute(attr, value)
1015 +
1016 +    def __delitem__(self, attr):
1017 +        assert not self.saved, 'too late to modify edited attributes'
1018 +        super(EditedEntity, self).__delitem__(attr)
1019 +        self.entity.cw_attr_cache.pop(attr, None)
1020 +
1021 +    def pop(self, attr, *args):
1022 +        # don't update skip_security by design (think to storage api)
1023 +        assert not self.saved, 'too late to modify edited attributes'
1024 +        value = super(EditedEntity, self).pop(attr, *args)
1025 +        self.entity.cw_attr_cache.pop(attr, *args)
1026 +        return value
1027 +
1028 +    def setdefault(self, attr, default):
1029 +        assert attr != 'eid'
1030 +        # don't add attribute into skip_security if already in edited
1031 +        # attributes, else we may accidentaly skip a desired security check
1032 +        if attr not in self:
1033 +            self[attr] = default
1034 +        return self[attr]
1035 +
1036 +    def update(self, values, skipsec=True):
1037 +        if skipsec:
1038 +            setitem = self.__setitem__
1039 +        else:
1040 +            setitem = self.edited_attribute
1041 +        for attr, value in values.iteritems():
1042 +            setitem(attr, value)
1043 +
1044 +    def edited_attribute(self, attr, value):
1045 +        """attribute being edited by a rql query: should'nt be added to
1046 +        skip_security
1047 +        """
1048 +        assert not self.saved, 'too late to modify edited attributes'
1049 +        super(EditedEntity, self).__setitem__(attr, value)
1050 +        self.entity.cw_attr_cache[attr] = value
1051 +
1052 +    def oldnewvalue(self, attr):
1053 +        """returns the couple (old attr value, new attr value)
1054 +
1055 +        NOTE: will only work in a before_update_entity hook
1056 +        """
1057 +        assert not self.saved, 'too late to get the old value'
1058 +        # get new value and remove from local dict to force a db query to
1059 +        # fetch old value
1060 +        newvalue = self.entity.cw_attr_cache.pop(attr, _MARKER)
1061 +        oldvalue = getattr(self.entity, attr)
1062 +        if newvalue is not _MARKER:
1063 +            self.entity.cw_attr_cache[attr] = newvalue
1064 +        else:
1065 +            newvalue = oldvalue
1066 +        return oldvalue, newvalue
1067 +
1068 +    def set_defaults(self):
1069 +        """set default values according to the schema"""
1070 +        for attr, value in self.entity.e_schema.defaults():
1071 +            if not attr in self:
1072 +                self[str(attr)] = value
1073 +
1074 +    def check(self, creation=False):
1075 +        """check the entity edition against its schema. Only final relation
1076 +        are checked here, constraint on actual relations are checked in hooks
1077 +        """
1078 +        entity = self.entity
1079 +        if creation:
1080 +            # on creations, we want to check all relations, especially
1081 +            # required attributes
1082 +            relations = [rschema for rschema in entity.e_schema.subject_relations()
1083 +                         if rschema.final and rschema.type != 'eid']
1084 +        else:
1085 +            relations = [entity._cw.vreg.schema.rschema(rtype)
1086 +                         for rtype in self]
1087 +        try:
1088 +            entity.e_schema.check(dict_protocol_catcher(entity),
1089 +                                  creation=creation, _=entity._cw._,
1090 +                                  relations=relations)
1091 +        except ValidationError, ex:
1092 +            ex.entity = self.entity
1093 +            raise
1094 +
1095 +    def clone(self):
1096 +        thecopy = EditedEntity(copy(self.entity))
1097 +        thecopy.entity.cw_attr_cache = copy(self.entity.cw_attr_cache)
1098 +        thecopy.entity._cw_related_cache = {}
1099 +        thecopy.update(self, skipsec=False)
1100 +        return thecopy
diff --git a/server/migractions.py b/server/migractions.py
@@ -1,6 +1,6 @@
1101 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1102 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1103  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
1104  #
1105  # This file is part of CubicWeb.
1106  #
1107  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -1305,24 +1305,36 @@
1108              self.commit()
1109 
1110      # CWProperty handling ######################################################
1111 
1112      def cmd_property_value(self, pkey):
1113 -        rql = 'Any V WHERE X is CWProperty, X pkey %(k)s, X value V'
1114 -        rset = self.rqlexec(rql, {'k': pkey}, ask_confirm=False)
1115 +        """retreive the site-wide persistent property value for the given key.
1116 +
1117 +        To get a user specific property value, use appropriate method on CWUser
1118 +        instance.
1119 +        """
1120 +        rset = self.rqlexec(
1121 +            'Any V WHERE X is CWProperty, X pkey %(k)s, X value V, NOT X for_user U',
1122 +            {'k': pkey}, ask_confirm=False)
1123          return rset[0][0]
1124 
1125      def cmd_set_property(self, pkey, value):
1126 +        """set the site-wide persistent property value for the given key to the
1127 +        given value.
1128 +
1129 +        To set a user specific property value, use appropriate method on CWUser
1130 +        instance.
1131 +        """
1132          value = unicode(value)
1133          try:
1134 -            prop = self.rqlexec('CWProperty X WHERE X pkey %(k)s', {'k': pkey},
1135 -                                ask_confirm=False).get_entity(0, 0)
1136 +            prop = self.rqlexec(
1137 +                'CWProperty X WHERE X pkey %(k)s, NOT X for_user U',
1138 +                {'k': pkey}, ask_confirm=False).get_entity(0, 0)
1139          except:
1140              self.cmd_create_entity('CWProperty', pkey=unicode(pkey), value=value)
1141          else:
1142 -            self.rqlexec('SET X value %(v)s WHERE X pkey %(k)s',
1143 -                         {'k': pkey, 'v': value}, ask_confirm=False)
1144 +            prop.set_attributes(value=value)
1145 
1146      # other data migration commands ###########################################
1147 
1148      @property
1149      def _cw(self):
@@ -1358,10 +1370,22 @@
1150          entity = self._cw.create_entity(etype, **kwargs)
1151          if commit:
1152              self.commit()
1153          return entity
1154 
1155 +    def cmd_find_entities(self, etype, **kwargs):
1156 +        """find entities of the given type and attribute values"""
1157 +        return self._cw.find_entities(etype, **kwargs)
1158 +
1159 +    def cmd_find_one_entity(self, etype, **kwargs):
1160 +        """find one entity of the given type and attribute values.
1161 +
1162 +        raise :exc:`cubicweb.req.FindEntityError` if can not return one and only
1163 +        one entity.
1164 +        """
1165 +        return self._cw.find_one_entity(etype, **kwargs)
1166 +
1167      def cmd_update_etype_fti_weight(self, etype, weight):
1168          if self.repo.system_source.dbdriver == 'postgres':
1169              self.sqlexec('UPDATE appears SET weight=%(weight)s '
1170                           'FROM entities as X '
1171                           'WHERE X.eid=appears.uid AND X.type=%(type)s',
diff --git a/server/querier.py b/server/querier.py
@@ -1,6 +1,6 @@
1172 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1173 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1174  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
1175  #
1176  # This file is part of CubicWeb.
1177  #
1178  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -36,11 +36,12 @@
1179  from cubicweb import server, typed_eid
1180  from cubicweb.rset import ResultSet
1181 
1182  from cubicweb.server.utils import cleanup_solutions
1183  from cubicweb.server.rqlannotation import SQLGenAnnotator, set_qdata
1184 -from cubicweb.server.ssplanner import READ_ONLY_RTYPES, add_types_restriction, EditedEntity
1185 +from cubicweb.server.ssplanner import READ_ONLY_RTYPES, add_types_restriction
1186 +from cubicweb.server.edition import EditedEntity
1187  from cubicweb.server.session import security_enabled
1188 
1189  def empty_rset(rql, args, rqlst=None):
1190      """build an empty result set object"""
1191      return ResultSet([], rql, args, rqlst=rqlst)
@@ -351,11 +352,11 @@
1192                      if not lcheckdef:
1193                          continue
1194                      myrqlst = select.copy(solutions=lchecksolutions)
1195                      myunion.append(myrqlst)
1196                      # in-place rewrite + annotation / simplification
1197 -                    lcheckdef = [((var, 'X'), rqlexprs) for var, rqlexprs in lcheckdef]
1198 +                    lcheckdef = [({var: 'X'}, rqlexprs) for var, rqlexprs in lcheckdef]
1199                      rewrite(myrqlst, lcheckdef, lchecksolutions, self.args)
1200                      add_noinvariant(noinvariant, restricted, myrqlst, nbtrees)
1201                  if () in localchecks:
1202                      select.set_possible_types(localchecks[()])
1203                      add_types_restriction(self.schema, select)
diff --git a/server/repository.py b/server/repository.py
@@ -57,10 +57,11 @@
1204  from cubicweb.server import utils, hook, pool, querier, sources
1205  from cubicweb.server.session import Session, InternalSession, InternalManager, \
1206       security_enabled
1207  from cubicweb.server.ssplanner import EditedEntity
1208 
1209 +
1210  def prefill_entity_caches(entity, relations):
1211      session = entity._cw
1212      # prefill entity relation caches
1213      for rschema in entity.e_schema.subject_relations():
1214          rtype = str(rschema)
@@ -132,10 +133,11 @@
1215          self.config = config
1216          if vreg is None:
1217              vreg = cwvreg.CubicWebVRegistry(config)
1218          self.vreg = vreg
1219          self.pyro_registered = False
1220 +        self.pyro_uri = None
1221          self.info('starting repository from %s', self.config.apphome)
1222          # dictionary of opened sessions
1223          self._sessions = {}
1224          # list of functions to be called at regular interval
1225          self._looping_tasks = []
@@ -413,11 +415,13 @@
1226                  pool.close(True)
1227              except:
1228                  self.exception('error while closing %s' % pool)
1229                  continue
1230          if self.pyro_registered:
1231 -            pyro_unregister(self.config)
1232 +            if self._use_pyrons():
1233 +                pyro_unregister(self.config)
1234 +            self.pyro_uri = None
1235          hits, misses = self.querier.cache_hit, self.querier.cache_miss
1236          try:
1237              self.info('rql st cache hit/miss: %s/%s (%s%% hits)', hits, misses,
1238                        (hits * 100) / (hits + misses))
1239              hits, misses = self.system_source.cache_hit, self.system_source.cache_miss
@@ -1417,24 +1421,36 @@
1240              config['pyro-ns-group'])
1241          # ensure config['pyro-instance-id'] is a full qualified pyro name
1242          config['pyro-instance-id'] = appid
1243          return appid
1244 
1245 +    def _use_pyrons(self):
1246 +        """return True if the pyro-ns-host is set to something else
1247 +        than NO_PYRONS, meaning we want to go through a pyro
1248 +        nameserver"""
1249 +        return self.config['pyro-ns-host'] != 'NO_PYRONS'
1250 +
1251      def pyro_register(self, host=''):
1252          """register the repository as a pyro object"""
1253          from logilab.common import pyro_ext as pyro
1254          daemon = pyro.register_object(self, self.pyro_appid,
1255                                        daemonhost=self.config['pyro-host'],
1256 -                                      nshost=self.config['pyro-ns-host'])
1257 +                                      nshost=self.config['pyro-ns-host'],
1258 +                                      use_pyrons=self._use_pyrons())
1259          self.info('repository registered as a pyro object %s', self.pyro_appid)
1260 +        self.pyro_uri =  pyro.get_object_uri(self.pyro_appid)
1261 +        self.info('pyro uri is: %s', self.pyro_uri)
1262          self.pyro_registered = True
1263          # register a looping task to regularly ensure we're still registered
1264          # into the pyro name server
1265 -        self.looping_task(60*10, self._ensure_pyro_ns)
1266 +        if self._use_pyrons():
1267 +            self.looping_task(60*10, self._ensure_pyro_ns)
1268          return daemon
1269 
1270      def _ensure_pyro_ns(self):
1271 +        if not self._use_pyrons():
1272 +            return
1273          from logilab.common import pyro_ext as pyro
1274          pyro.ns_reregister(self.pyro_appid, nshost=self.config['pyro-ns-host'])
1275          self.info('repository re-registered as a pyro object %s',
1276                    self.pyro_appid)
1277 
diff --git a/server/serverconfig.py b/server/serverconfig.py
@@ -1,6 +1,6 @@
1278 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1279 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1280  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
1281  #
1282  # This file is part of CubicWeb.
1283  #
1284  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -213,12 +213,10 @@
1285      # necessary...
1286      open_connections_pools = True
1287 
1288      # read the schema from the database
1289      read_instance_schema = True
1290 -    # set to true while creating an instance
1291 -    creating = False
1292      # set this to true to get a minimal repository, for instance to get cubes
1293      # information on commands such as i18ninstance, db-restore, etc...
1294      quick_start = False
1295      # check user's state at login time
1296      consider_user_state = True
diff --git a/server/serverctl.py b/server/serverctl.py
@@ -1,6 +1,6 @@
1297 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1298 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1299  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
1300  #
1301  # This file is part of CubicWeb.
1302  #
1303  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -25,48 +25,47 @@
1304  import sys
1305  import os
1306 
1307  from logilab.common import nullobject
1308  from logilab.common.configuration import Configuration
1309 -from logilab.common.shellutils import ASK
1310 +from logilab.common.shellutils import ASK, generate_password
1311 
1312  from cubicweb import AuthenticationError, ExecutionError, ConfigurationError
1313  from cubicweb.toolsutils import Command, CommandHandler, underline_title
1314 -from cubicweb.cwctl import CWCTL
1315 +from cubicweb.cwctl import CWCTL, check_options_consistency
1316  from cubicweb.server import SOURCE_TYPES
1317  from cubicweb.server.serverconfig import (
1318      USER_OPTIONS, ServerConfiguration, SourceConfiguration,
1319      ask_source_config, generate_source_config)
1320 
1321  # utility functions ###########################################################
1322 
1323 -def source_cnx(source, dbname=None, special_privs=False, verbose=True):
1324 +def source_cnx(source, dbname=None, special_privs=False, interactive=True):
1325      """open and return a connection to the system database defined in the
1326      given server.serverconfig
1327      """
1328      from getpass import getpass
1329      from logilab.database import get_connection, get_db_helper
1330      dbhost = source.get('db-host')
1331      if dbname is None:
1332          dbname = source['db-name']
1333      driver = source['db-driver']
1334      dbhelper = get_db_helper(driver)
1335 -    if verbose:
1336 +    if interactive:
1337          print '-> connecting to %s database' % driver,
1338          if dbhost:
1339              print '%s@%s' % (dbname, dbhost),
1340          else:
1341              print dbname,
1342      if dbhelper.users_support:
1343 -        if not special_privs and source.get('db-user'):
1344 -            user = source['db-user']
1345 -            if verbose:
1346 +        if not interactive or (not special_privs and source.get('db-user')):
1347 +            user = source.get('db-user', os.environ.get('USER', ''))
1348 +            if interactive:
1349                  print 'as', user
1350              password = source.get('db-password')
1351          else:
1352 -            if verbose:
1353 -                print
1354 +            print
1355              if special_privs:
1356                  print 'WARNING'
1357                  print ('the user will need the following special access rights '
1358                         'on the database:')
1359                  print special_privs
@@ -93,34 +92,36 @@
1360          cnx = _SimpleConnectionWrapper(cnx)
1361          cnx.logged_user = user
1362      return cnx
1363 
1364  def system_source_cnx(source, dbms_system_base=False,
1365 -                      special_privs='CREATE/DROP DATABASE', verbose=True):
1366 +                      special_privs='CREATE/DROP DATABASE', interactive=True):
1367      """shortcut to get a connextion to the instance system database
1368      defined in the given config. If <dbms_system_base> is True,
1369      connect to the dbms system database instead (for task such as
1370      create/drop the instance database)
1371      """
1372      if dbms_system_base:
1373          from logilab.database import get_db_helper
1374          system_db = get_db_helper(source['db-driver']).system_database()
1375 -        return source_cnx(source, system_db, special_privs=special_privs, verbose=verbose)
1376 -    return source_cnx(source, special_privs=special_privs, verbose=verbose)
1377 +        return source_cnx(source, system_db, special_privs=special_privs,
1378 +                          interactive=interactive)
1379 +    return source_cnx(source, special_privs=special_privs,
1380 +                      interactive=interactive)
1381 
1382 -def _db_sys_cnx(source, special_privs, verbose=True):
1383 +def _db_sys_cnx(source, special_privs, interactive=True):
1384      """return a connection on the RDMS system table (to create/drop a user or a
1385      database)
1386      """
1387      import logilab.common as lgp
1388      from logilab.database import get_db_helper
1389      lgp.USE_MX_DATETIME = False
1390      driver = source['db-driver']
1391      helper = get_db_helper(driver)
1392      # connect on the dbms system base to create our base
1393      cnx = system_source_cnx(source, True, special_privs=special_privs,
1394 -                            verbose=verbose)
1395 +                            interactive=interactive)
1396      # disable autocommit (isolation_level(1)) because DROP and
1397      # CREATE DATABASE can't be executed in a transaction
1398      try:
1399          cnx.set_isolation_level(0)
1400      except AttributeError:
@@ -151,42 +152,53 @@
1401 
1402  class RepositoryCreateHandler(CommandHandler):
1403      cmdname = 'create'
1404      cfgname = 'repository'
1405 
1406 -    def bootstrap(self, cubes, inputlevel=0):
1407 +    def bootstrap(self, cubes, automatic=False, inputlevel=0):
1408          """create an instance by copying files from the given cube and by asking
1409          information necessary to build required configuration files
1410          """
1411          config = self.config
1412 -        print underline_title('Configuring the repository')
1413 -        config.input_config('email', inputlevel)
1414 -        # ask for pyro configuration if pyro is activated and we're not using a
1415 -        # all-in-one config, in which case this is done by the web side command
1416 -        # handler
1417 -        if config.pyro_enabled() and config.name != 'all-in-one':
1418 -            config.input_config('pyro', inputlevel)
1419 -        print '\n'+underline_title('Configuring the sources')
1420 +        if not automatic:
1421 +            print underline_title('Configuring the repository')
1422 +            config.input_config('email', inputlevel)
1423 +            # ask for pyro configuration if pyro is activated and we're not
1424 +            # using a all-in-one config, in which case this is done by the web
1425 +            # side command handler
1426 +            if config.pyro_enabled() and config.name != 'all-in-one':
1427 +                config.input_config('pyro', inputlevel)
1428 +            print '\n'+underline_title('Configuring the sources')
1429          sourcesfile = config.sources_file()
1430 -        # XXX hack to make Method('default_instance_id') usable in db option
1431 -        # defs (in native.py)
1432 +        # hack to make Method('default_instance_id') usable in db option defs
1433 +        # (in native.py)
1434          sconfig = SourceConfiguration(config,
1435                                        options=SOURCE_TYPES['native'].options)
1436 -        sconfig.input_config(inputlevel=inputlevel)
1437 +        if not automatic:
1438 +            sconfig.input_config(inputlevel=inputlevel)
1439 +            print
1440          sourcescfg = {'system': sconfig}
1441 -        print
1442 -        sconfig = Configuration(options=USER_OPTIONS)
1443 -        sconfig.input_config(inputlevel=inputlevel)
1444 +        if automatic:
1445 +            # XXX modify a copy
1446 +            password = generate_password()
1447 +            print 'Administration account is admin / %s' % password
1448 +            USER_OPTIONS[1][1]['default'] = password
1449 +            sconfig = Configuration(options=USER_OPTIONS)
1450 +        else:
1451 +            sconfig = Configuration(options=USER_OPTIONS)
1452 +            sconfig.input_config(inputlevel=inputlevel)
1453          sourcescfg['admin'] = sconfig
1454          config.write_sources_file(sourcescfg)
1455          # remember selected cubes for later initialization of the database
1456          config.write_bootstrap_cubes_file(cubes)
1457 
1458 -    def postcreate(self):
1459 -        if ASK.confirm('Run db-create to create the system database ?'):
1460 -            verbosity = (self.config.mode == 'installed') and 'y' or 'n'
1461 -            CWCTL.run(['db-create', self.config.appid, '--verbose=%s' % verbosity])
1462 +    def postcreate(self, automatic=False, inputlevel=0):
1463 +        if automatic:
1464 +            CWCTL.run(['db-create', '--automatic', self.config.appid])
1465 +        elif ASK.confirm('Run db-create to create the system database ?'):
1466 +            CWCTL.run(['db-create', '--config-level', str(inputlevel),
1467 +                       self.config.appid])
1468          else:
1469              print ('-> nevermind, you can do it later with '
1470                     '"cubicweb-ctl db-create %s".' % self.config.appid)
1471 
1472  ERROR = nullobject()
@@ -290,31 +302,34 @@
1473      """
1474      name = 'db-create'
1475      arguments = '<instance>'
1476      min_args = max_args = 1
1477      options = (
1478 +        ('automatic',
1479 +         {'short': 'a', 'action' : 'store_true',
1480 +          'default': False,
1481 +          'help': 'automatic mode: never ask and use default answer to every '
1482 +          'question. this may require that your login match a database super '
1483 +          'user (allowed to create database & all).',
1484 +          }),
1485 +        ('config-level',
1486 +         {'short': 'l', 'type' : 'int', 'metavar': '<level>',
1487 +          'default': 0,
1488 +          'help': 'configuration level (0..2): 0 will ask for essential '
1489 +          'configuration parameters only while 2 will ask for all parameters',
1490 +          }),
1491          ('create-db',
1492           {'short': 'c', 'type': 'yn', 'metavar': '<y or n>',
1493            'default': True,
1494 -          'help': 'create the database (yes by default)'}),
1495 -        ('verbose',
1496 -         {'short': 'v', 'type' : 'yn', 'metavar': '<verbose>',
1497 -          'default': 'n',
1498 -          'help': 'verbose mode: will ask all possible configuration questions',
1499 -          }
1500 -         ),
1501 -        ('automatic',
1502 -         {'short': 'a', 'type' : 'yn', 'metavar': '<auto>',
1503 -          'default': 'n',
1504 -          'help': 'automatic mode: never ask and use default answer to every question',
1505 -          }
1506 -         ),
1507 +          'help': 'create the database (yes by default)'
1508 +          }),
1509          )
1510 +
1511      def run(self, args):
1512          """run the command with its specific arguments"""
1513          from logilab.database import get_db_helper
1514 -        verbose = self.get('verbose')
1515 +        check_options_consistency(self.config)
1516          automatic = self.get('automatic')
1517          appid = args.pop()
1518          config = ServerConfiguration.config_for(appid)
1519          source = config.sources()['system']
1520          dbname = source['db-name']
@@ -327,11 +342,11 @@
1521                  os.unlink(dbname)
1522          elif self.config.create_db:
1523              print '\n'+underline_title('Creating the system database')
1524              # connect on the dbms system base to create our base
1525              dbcnx = _db_sys_cnx(source, 'CREATE/DROP DATABASE and / or USER',
1526 -                                verbose=verbose)
1527 +                                interactive=not automatic)
1528              cursor = dbcnx.cursor()
1529              try:
1530                  if helper.users_support:
1531                      user = source['db-user']
1532                      if not helper.user_exists(cursor, user) and (automatic or \
@@ -340,19 +355,21 @@
1533                          print '-> user %s created.' % user
1534                  if dbname in helper.list_databases(cursor):
1535                      if automatic or ASK.confirm('Database %s already exists -- do you want to drop it ?' % dbname):
1536                          cursor.execute('DROP DATABASE %s' % dbname)
1537                      else:
1538 +                        print ('you may want to run "cubicweb-ctl db-init '
1539 +                               '--drop %s" manually to continue.' % config.appid)
1540                          return
1541                  createdb(helper, source, dbcnx, cursor)
1542                  dbcnx.commit()
1543                  print '-> database %s created.' % dbname
1544              except:
1545                  dbcnx.rollback()
1546                  raise
1547          cnx = system_source_cnx(source, special_privs='CREATE LANGUAGE',
1548 -                                verbose=verbose)
1549 +                                interactive=not automatic)
1550          cursor = cnx.cursor()
1551          helper.init_fti_extensions(cursor)
1552          # postgres specific stuff
1553          if driver == 'postgres':
1554              # install plpythonu/plpgsql language if not installed by the cube
@@ -361,12 +378,16 @@
1555                  helper.create_language(cursor, extlang)
1556          cursor.close()
1557          cnx.commit()
1558          print '-> database for instance %s created and necessary extensions installed.' % appid
1559          print
1560 -        if automatic or ASK.confirm('Run db-init to initialize the system database ?'):
1561 -            CWCTL.run(['db-init', config.appid])
1562 +        if automatic:
1563 +            CWCTL.run(['db-init', '--automatic', '--config-level', '0',
1564 +                       config.appid])
1565 +        elif ASK.confirm('Run db-init to initialize the system database ?'):
1566 +            CWCTL.run(['db-init', '--config-level',
1567 +                       str(self.config.config_level), config.appid])
1568          else:
1569              print ('-> nevermind, you can do it later with '
1570                     '"cubicweb-ctl db-init %s".' % config.appid)
1571 
1572 
@@ -381,22 +402,31 @@
1573      """
1574      name = 'db-init'
1575      arguments = '<instance>'
1576      min_args = max_args = 1
1577      options = (
1578 +        ('automatic',
1579 +         {'short': 'a', 'action' : 'store_true',
1580 +          'default': False,
1581 +          'help': 'automatic mode: never ask and use default answer to every '
1582 +          'question.',
1583 +          }),
1584 +        ('config-level',
1585 +         {'short': 'l', 'type': 'int', 'default': 1,
1586 +          'help': 'level threshold for questions asked when configuring '
1587 +          'another source'
1588 +          }),
1589          ('drop',
1590           {'short': 'd', 'action': 'store_true',
1591            'default': False,
1592 -          'help': 'insert drop statements to remove previously existant \
1593 -tables, indexes... (no by default)'}),
1594 -        ('config-level',
1595 -         {'short': 'l', 'type': 'int', 'default': 1,
1596 -          'help': 'level threshold for questions asked when configuring another source'
1597 +          'help': 'insert drop statements to remove previously existant '
1598 +          'tables, indexes... (no by default)'
1599            }),
1600          )
1601 
1602      def run(self, args):
1603 +        check_options_consistency(self.config)
1604          print '\n'+underline_title('Initializing the system database')
1605          from cubicweb.server import init_repository
1606          from logilab.database import get_connection
1607          appid = args[0]
1608          config = ServerConfiguration.config_for(appid)
@@ -413,12 +443,14 @@
1609              raise ConfigurationError(
1610                  'You seem to have provided wrong connection information in '\
1611                  'the %s file. Resolve this first (error: %s).'
1612                  % (config.sources_file(), str(ex).strip()))
1613          init_repository(config, drop=self.config.drop)
1614 -        while ASK.confirm('Enter another source ?', default_is_yes=False):
1615 -            CWCTL.run(['add-source', '--config-level', self.config.config_level, config.appid])
1616 +        if not self.config.automatic:
1617 +            while ASK.confirm('Enter another source ?', default_is_yes=False):
1618 +                CWCTL.run(['add-source', '--config-level',
1619 +                           str(self.config.config_level), config.appid])
1620 
1621 
1622  class AddSourceCommand(Command):
1623      """Add a data source to an instance.
1624 
diff --git a/server/session.py b/server/session.py
@@ -1,6 +1,6 @@
1625 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1626 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1627  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
1628  #
1629  # This file is part of CubicWeb.
1630  #
1631  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -35,10 +35,11 @@
1632  from cubicweb import Binary, UnknownEid, QueryError, schema
1633  from cubicweb.req import RequestSessionBase
1634  from cubicweb.dbapi import ConnectionProperties
1635  from cubicweb.utils import make_uid, RepeatList
1636  from cubicweb.rqlrewrite import RQLRewriter
1637 +from cubicweb.server.edition import EditedEntity
1638 
1639  ETYPE_PYOBJ_MAP[Binary] = 'Bytes'
1640 
1641  NO_UNDO_TYPES = schema.SCHEMA_TYPES.copy()
1642  NO_UNDO_TYPES.add('CWCache')
@@ -213,12 +214,13 @@
1643          want to add.
1644          """
1645          with security_enabled(self, False, False):
1646              if self.vreg.schema[rtype].inlined:
1647                  entity = self.entity_from_eid(fromeid)
1648 -                entity[rtype] = toeid
1649 -                self.repo.glob_update_entity(self, entity, set((rtype,)))
1650 +                edited = EditedEntity(entity)
1651 +                edited.edited_attribute(rtype, toeid)
1652 +                self.repo.glob_update_entity(self, edited)
1653              else:
1654                  self.repo.glob_add_relation(self, fromeid, rtype, toeid)
1655 
1656      def delete_relation(self, fromeid, rtype, toeid):
1657          """provide direct access to the repository method to delete a relation.
@@ -232,11 +234,11 @@
1658          want to delete.
1659          """
1660          with security_enabled(self, False, False):
1661              if self.vreg.schema[rtype].inlined:
1662                  entity = self.entity_from_eid(fromeid)
1663 -                entity[rtype] = None
1664 +                entity.cw_attr_cache[rtype] = None
1665                  self.repo.glob_update_entity(self, entity, set((rtype,)))
1666              else:
1667                  self.repo.glob_delete_relation(self, fromeid, rtype, toeid)
1668 
1669      # relations cache handling #################################################
diff --git a/server/sources/__init__.py b/server/sources/__init__.py
@@ -29,11 +29,11 @@
1670  from yams.schema import role_name
1671 
1672  from cubicweb import ValidationError, set_log_methods, server
1673  from cubicweb.schema import VIRTUAL_RTYPES
1674  from cubicweb.server.sqlutils import SQL_PREFIX
1675 -from cubicweb.server.ssplanner import EditedEntity
1676 +from cubicweb.server.edition import EditedEntity
1677 
1678 
1679  def dbg_st_search(uri, union, varmap, args, cachekey=None, prefix='rql for'):
1680      if server.DEBUG & server.DBG_RQL:
1681          print '  %s %s source: %s' % (prefix, uri, union.as_string())
diff --git a/server/sources/ldapuser.py b/server/sources/ldapuser.py
@@ -520,11 +520,12 @@
1682          #conn.sasl_interactive_bind_s('', sasl.gssapi())
1683 
1684      def _search(self, session, base, scope,
1685                  searchstr='(objectClass=*)', attrs=()):
1686          """make an ldap query"""
1687 -        self.debug('ldap search %s %s %s %s %s', self.uri, base, scope, searchstr, list(attrs))
1688 +        self.debug('ldap search %s %s %s %s %s', self.uri, base, scope,
1689 +                   searchstr, list(attrs))
1690          # XXX for now, we do not have connection pool support for LDAP, so
1691          # this is always self._conn
1692          cnx = session.pool.connection(self.uri).cnx
1693          try:
1694              res = cnx.search_s(base, scope, searchstr, attrs)
diff --git a/server/sources/native.py b/server/sources/native.py
@@ -56,11 +56,11 @@
1695  from cubicweb.server.utils import crypt_password, eschema_eid
1696  from cubicweb.server.sqlutils import SQL_PREFIX, SQLAdapterMixIn
1697  from cubicweb.server.rqlannotation import set_qdata
1698  from cubicweb.server.hook import CleanupDeletedEidsCacheOp
1699  from cubicweb.server.session import hooks_control, security_enabled
1700 -from cubicweb.server.ssplanner import EditedEntity
1701 +from cubicweb.server.edition import EditedEntity
1702  from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results
1703  from cubicweb.server.sources.rql2sql import SQLGenerator
1704 
1705 
1706  ATTR_MAP = {}
diff --git a/server/sources/storages.py b/server/sources/storages.py
@@ -1,6 +1,6 @@
1707 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1708 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1709  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
1710  #
1711  # This file is part of CubicWeb.
1712  #
1713  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -22,11 +22,11 @@
1714 
1715  from yams.schema import role_name
1716 
1717  from cubicweb import Binary, ValidationError
1718  from cubicweb.server import hook
1719 -from cubicweb.server.ssplanner import EditedEntity
1720 +from cubicweb.server.edition import EditedEntity
1721 
1722 
1723  def set_attribute_storage(repo, etype, attr, storage):
1724      repo.system_source.set_storage(etype, attr, storage)
1725 
diff --git a/server/ssplanner.py b/server/ssplanner.py
@@ -1,6 +1,6 @@
1726 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1727 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1728  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
1729  #
1730  # This file is part of CubicWeb.
1731  #
1732  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -19,20 +19,19 @@
1733 
1734  from __future__ import with_statement
1735 
1736  __docformat__ = "restructuredtext en"
1737 
1738 -from copy import copy
1739 -
1740  from rql.stmts import Union, Select
1741  from rql.nodes import Constant, Relation
1742 
1743  from cubicweb import QueryError, typed_eid
1744  from cubicweb.schema import VIRTUAL_RTYPES
1745  from cubicweb.rqlrewrite import add_types_restriction
1746  from cubicweb.server.session import security_enabled
1747  from cubicweb.server.hook import CleanupDeletedEidsCacheOp
1748 +from cubicweb.server.edition import EditedEntity
1749 
1750  READ_ONLY_RTYPES = set(('eid', 'has_text', 'is', 'is_instance_of', 'identity'))
1751 
1752  _CONSTANT = object()
1753  _FROM_SUBSTEP = object()
@@ -126,136 +125,10 @@
1754          # no selection, append one randomly
1755          select.append_selected(rel.children[0].copy(select))
1756      return select
1757 
1758 
1759 -_MARKER = object()
1760 -
1761 -class dict_protocol_catcher(object):
1762 -    def __init__(self, entity):
1763 -        self.__entity = entity
1764 -    def __getitem__(self, attr):
1765 -        return self.__entity.cw_edited[attr]
1766 -    def __setitem__(self, attr, value):
1767 -        self.__entity.cw_edited[attr] = value
1768 -    def __getattr__(self, attr):
1769 -        return getattr(self.__entity, attr)
1770 -
1771 -
1772 -class EditedEntity(dict):
1773 -    """encapsulate entities attributes being written by an RQL query"""
1774 -    def __init__(self, entity, **kwargs):
1775 -        dict.__init__(self, **kwargs)
1776 -        self.entity = entity
1777 -        self.skip_security = set()
1778 -        self.querier_pending_relations = {}
1779 -        self.saved = False
1780 -
1781 -    def __hash__(self):
1782 -        # dict|set keyable
1783 -        return hash(id(self))
1784 -
1785 -    def __cmp__(self, other):
1786 -        # we don't want comparison by value inherited from dict
1787 -        return cmp(id(self), id(other))
1788 -
1789 -    def __setitem__(self, attr, value):
1790 -        assert attr != 'eid'
1791 -        # don't add attribute into skip_security if already in edited
1792 -        # attributes, else we may accidentaly skip a desired security check
1793 -        if attr not in self:
1794 -            self.skip_security.add(attr)
1795 -        self.edited_attribute(attr, value)
1796 -
1797 -    def __delitem__(self, attr):
1798 -        assert not self.saved, 'too late to modify edited attributes'
1799 -        super(EditedEntity, self).__delitem__(attr)
1800 -        self.entity.cw_attr_cache.pop(attr, None)
1801 -
1802 -    def pop(self, attr, *args):
1803 -        # don't update skip_security by design (think to storage api)
1804 -        assert not self.saved, 'too late to modify edited attributes'
1805 -        value = super(EditedEntity, self).pop(attr, *args)
1806 -        self.entity.cw_attr_cache.pop(attr, *args)
1807 -        return value
1808 -
1809 -    def setdefault(self, attr, default):
1810 -        assert attr != 'eid'
1811 -        # don't add attribute into skip_security if already in edited
1812 -        # attributes, else we may accidentaly skip a desired security check
1813 -        if attr not in self:
1814 -            self[attr] = default
1815 -        return self[attr]
1816 -
1817 -    def update(self, values, skipsec=True):
1818 -        if skipsec:
1819 -            setitem = self.__setitem__
1820 -        else:
1821 -            setitem = self.edited_attribute
1822 -        for attr, value in values.iteritems():
1823 -            setitem(attr, value)
1824 -
1825 -    def edited_attribute(self, attr, value):
1826 -        """attribute being edited by a rql query: should'nt be added to
1827 -        skip_security
1828 -        """
1829 -        assert not self.saved, 'too late to modify edited attributes'
1830 -        super(EditedEntity, self).__setitem__(attr, value)
1831 -        self.entity.cw_attr_cache[attr] = value
1832 -
1833 -    def oldnewvalue(self, attr):
1834 -        """returns the couple (old attr value, new attr value)
1835 -
1836 -        NOTE: will only work in a before_update_entity hook
1837 -        """
1838 -        assert not self.saved, 'too late to get the old value'
1839 -        # get new value and remove from local dict to force a db query to
1840 -        # fetch old value
1841 -        newvalue = self.entity.cw_attr_cache.pop(attr, _MARKER)
1842 -        oldvalue = getattr(self.entity, attr)
1843 -        if newvalue is not _MARKER:
1844 -            self.entity.cw_attr_cache[attr] = newvalue
1845 -        else:
1846 -            newvalue = oldvalue
1847 -        return oldvalue, newvalue
1848 -
1849 -    def set_defaults(self):
1850 -        """set default values according to the schema"""
1851 -        for attr, value in self.entity.e_schema.defaults():
1852 -            if not attr in self:
1853 -                self[str(attr)] = value
1854 -
1855 -    def check(self, creation=False):
1856 -        """check the entity edition against its schema. Only final relation
1857 -        are checked here, constraint on actual relations are checked in hooks
1858 -        """
1859 -        entity = self.entity
1860 -        if creation:
1861 -            # on creations, we want to check all relations, especially
1862 -            # required attributes
1863 -            relations = [rschema for rschema in entity.e_schema.subject_relations()
1864 -                         if rschema.final and rschema.type != 'eid']
1865 -        else:
1866 -            relations = [entity._cw.vreg.schema.rschema(rtype)
1867 -                         for rtype in self]
1868 -        from yams import ValidationError
1869 -        try:
1870 -            entity.e_schema.check(dict_protocol_catcher(entity),
1871 -                                  creation=creation, _=entity._cw._,
1872 -                                  relations=relations)
1873 -        except ValidationError, ex:
1874 -            ex.entity = self.entity
1875 -            raise
1876 -
1877 -    def clone(self):
1878 -        thecopy = EditedEntity(copy(self.entity))
1879 -        thecopy.entity.cw_attr_cache = copy(self.entity.cw_attr_cache)
1880 -        thecopy.entity._cw_related_cache = {}
1881 -        thecopy.update(self, skipsec=False)
1882 -        return thecopy
1883 -
1884 -
1885  class SSPlanner(object):
1886      """SingleSourcePlanner: build execution plan for rql queries
1887 
1888      optimized for single source repositories
1889      """
diff --git a/server/test/unittest_ldapuser.py b/server/test/unittest_ldapuser.py
@@ -1,6 +1,6 @@
1890 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1891 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1892  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
1893  #
1894  # This file is part of CubicWeb.
1895  #
1896  # CubicWeb is free software: you can redistribute it and/or modify it under the
diff --git a/test/unittest_entity.py b/test/unittest_entity.py
@@ -221,42 +221,52 @@
1897              self.vreg['etypes'].etype_class(ttype).fetch_attrs = ('modification_date',)
1898          self.assertEqual(tag.cw_related_rql('tags', 'subject'),
1899                            'Any X,AA ORDERBY AA DESC '
1900                            'WHERE E eid %(x)s, E tags X, X modification_date AA')
1901 
1902 -    def test_unrelated_rql_security_1(self):
1903 +    def test_unrelated_rql_security_1_manager(self):
1904          user = self.request().user
1905          rql = user.cw_unrelated_rql('use_email', 'EmailAddress', 'subject')[0]
1906          self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC '
1907 -                          'WHERE NOT S use_email O, S eid %(x)s, O is EmailAddress, O address AA, O alias AB, O modification_date AC')
1908 +                         'WHERE NOT S use_email O, S eid %(x)s, '
1909 +                         'O is EmailAddress, O address AA, O alias AB, O modification_date AC')
1910 +
1911 +    def test_unrelated_rql_security_1_user(self):
1912          self.create_user('toto')
1913          self.login('toto')
1914          user = self.request().user
1915          rql = user.cw_unrelated_rql('use_email', 'EmailAddress', 'subject')[0]
1916          self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC '
1917 -                          'WHERE NOT S use_email O, S eid %(x)s, O is EmailAddress, O address AA, O alias AB, O modification_date AC')
1918 +                          'WHERE NOT S use_email O, S eid %(x)s, '
1919 +                         'O is EmailAddress, O address AA, O alias AB, O modification_date AC')
1920          user = self.execute('Any X WHERE X login "admin"').get_entity(0, 0)
1921 -        self.assertRaises(Unauthorized, user.cw_unrelated_rql, 'use_email', 'EmailAddress', 'subject')
1922 +        rql = user.cw_unrelated_rql('use_email', 'EmailAddress', 'subject')[0]
1923 +        self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC WHERE '
1924 +                         'NOT EXISTS(S use_email O), S eid %(x)s, '
1925 +                         'O is EmailAddress, O address AA, O alias AB, O modification_date AC, '
1926 +                         'A eid %(B)s, EXISTS(S identity A, NOT A in_group C, C name "guests", C is CWGroup)')
1927 +
1928 +    def test_unrelated_rql_security_1_anon(self):
1929          self.login('anon')
1930          user = self.request().user
1931 -        self.assertRaises(Unauthorized, user.cw_unrelated_rql, 'use_email', 'EmailAddress', 'subject')
1932 +        rql = user.cw_unrelated_rql('use_email', 'EmailAddress', 'subject')[0]
1933 +        self.assertEqual(rql, 'Any O,AA,AB,AC ORDERBY AC DESC WHERE '
1934 +                         'NOT EXISTS(S use_email O), S eid %(x)s, '
1935 +                         'O is EmailAddress, O address AA, O alias AB, O modification_date AC, '
1936 +                         'A eid %(B)s, EXISTS(S identity A, NOT A in_group C, C name "guests", C is CWGroup)')
1937 
1938      def test_unrelated_rql_security_2(self):
1939          email = self.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0)
1940          rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0]
1941          self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AA ASC '
1942                            'WHERE NOT S use_email O, O eid %(x)s, S is CWUser, S login AA, S firstname AB, S surname AC, S modification_date AD')
1943 -        #rql = email.cw_unrelated_rql('use_email', 'Person', 'object')[0]
1944 -        #self.assertEqual(rql, '')
1945          self.login('anon')
1946          email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}).get_entity(0, 0)
1947          rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0]
1948          self.assertEqual(rql, 'Any S,AA,AB,AC,AD ORDERBY AA '
1949                            'WHERE NOT EXISTS(S use_email O), O eid %(x)s, S is CWUser, S login AA, S firstname AB, S surname AC, S modification_date AD, '
1950                            'A eid %(B)s, EXISTS(S identity A, NOT A in_group C, C name "guests", C is CWGroup)')
1951 -        #rql = email.cw_unrelated_rql('use_email', 'Person', 'object')[0]
1952 -        #self.assertEqual(rql, '')
1953 
1954      def test_unrelated_rql_security_nonexistant(self):
1955          self.login('anon')
1956          email = self.vreg['etypes'].etype_class('EmailAddress')(self.request())
1957          rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0]
diff --git a/test/unittest_rqlrewrite.py b/test/unittest_rqlrewrite.py
@@ -1,6 +1,6 @@
1958 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1959 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1960  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
1961  #
1962  # This file is part of CubicWeb.
1963  #
1964  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -60,19 +60,21 @@
1965                  rqlhelper.annotate(rqlst)
1966              @staticmethod
1967              def simplify(mainrqlst, needcopy=False):
1968                  rqlhelper.simplify(rqlst, needcopy)
1969      rewriter = RQLRewriter(mock_object(vreg=FakeVReg, user=(mock_object(eid=1))))
1970 -    for v, snippets in snippets_map.items():
1971 -        snippets_map[v] = [isinstance(snippet, basestring)
1972 -                           and mock_object(snippet_rqlst=parse('Any X WHERE '+snippet).children[0],
1973 -                                           expression='Any X WHERE '+snippet)
1974 -                           or snippet
1975 -                           for snippet in snippets]
1976 +    snippets = []
1977 +    for v, exprs in snippets_map.items():
1978 +        rqlexprs = [isinstance(snippet, basestring)
1979 +                    and mock_object(snippet_rqlst=parse('Any X WHERE '+snippet).children[0],
1980 +                                    expression='Any X WHERE '+snippet)
1981 +                    or snippet
1982 +                    for snippet in exprs]
1983 +        snippets.append((dict([v]), rqlexprs))
1984      rqlhelper.compute_solutions(rqlst.children[0], {'eid': eid_func_map}, kwargs=kwargs)
1985      solutions = rqlst.children[0].solutions
1986 -    rewriter.rewrite(rqlst.children[0], snippets_map.items(), solutions, kwargs,
1987 +    rewriter.rewrite(rqlst.children[0], snippets, solutions, kwargs,
1988                       existingvars)
1989      test_vrefs(rqlst.children[0])
1990      return rewriter.rewritten
1991 
1992  def test_vrefs(node):
diff --git a/utils.py b/utils.py
@@ -1,6 +1,6 @@
1993 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1994 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1995  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
1996  #
1997  # This file is part of CubicWeb.
1998  #
1999  # CubicWeb is free software: you can redistribute it and/or modify it under the
diff --git a/web/data/cubicweb.ajax.js b/web/data/cubicweb.ajax.js
@@ -281,11 +281,11 @@
2000   * and return a deferred whose callbacks args are decoded according to the
2001   * Content-Type response header. `form` should be additional form params
2002   * dictionary, `reqtype` the HTTP request type (get 'GET' or 'POST').
2003   */
2004  function loadRemote(url, form, reqtype, sync) {
2005 -    if (!url.toLowerCase().startswith(baseuri())) {
2006 +    if (!url.toLowerCase().startswith(baseuri().toLowerCase())) {
2007          url = baseuri() + url;
2008      }
2009      if (!sync) {
2010          var deferred = new Deferred();
2011          jQuery.ajax({
diff --git a/web/data/cubicweb.old.css b/web/data/cubicweb.old.css
@@ -281,11 +281,11 @@
2012    position: relative;
2013    min-height: 800px;
2014  }
2015 
2016  table#mainLayout{
2017 - margin:0px 3px;
2018 + padding: 0px 3px;
2019  }
2020 
2021  table#mainLayout td#contentColumn {
2022    padding: 8px 10px 5px;
2023  }
diff --git a/web/data/jquery.ui.datepicker-es.js b/web/data/jquery.ui.datepicker-es.js
@@ -8,13 +8,13 @@
2024  		currentText: 'Hoy',
2025  		monthNames: ['Enero','Febrero','Marzo','Abril','Mayo','Junio',
2026  		'Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'],
2027  		monthNamesShort: ['Ene','Feb','Mar','Abr','May','Jun',
2028  		'Jul','Ago','Sep','Oct','Nov','Dic'],
2029 -		dayNames: ['Domingo','Lunes','Martes','Mi&eacute;rcoles','Jueves','Viernes','S&aacute;bado'],
2030 -		dayNamesShort: ['Dom','Lun','Mar','Mi&eacute;','Juv','Vie','S&aacute;b'],
2031 -		dayNamesMin: ['Do','Lu','Ma','Mi','Ju','Vi','S&aacute;'],
2032 +		dayNames: ['Domingo','Lunes','Martes','Miércoles','Jueves','Viernes','Sábado'],
2033 +		dayNamesShort: ['Dom','Lun','Mar','Mié','Juv','Vie','Sáb'],
2034 +		dayNamesMin: ['Do','Lu','Ma','Mi','Ju','Vi','Sá'],
2035  		weekHeader: 'Sm',
2036  		dateFormat: 'dd/mm/yy',
2037  		firstDay: 1,
2038  		isRTL: false,
2039  		showMonthAfterYear: false,
diff --git a/web/facet.py b/web/facet.py
@@ -51,11 +51,11 @@
2040  from datetime import date, datetime, timedelta
2041 
2042  from logilab.mtconverter import xml_escape
2043  from logilab.common.graph import has_path
2044  from logilab.common.decorators import cached
2045 -from logilab.common.date import datetime2ticks
2046 +from logilab.common.date import datetime2ticks, ustrftime, ticks2datetime
2047  from logilab.common.compat import all
2048 
2049  from rql import parse, nodes, utils
2050 
2051  from cubicweb import Unauthorized, typed_eid
@@ -979,11 +979,15 @@
2052      def wdgclass(self):
2053          return DateFacetRangeWidget
2054 
2055      def formatvalue(self, value):
2056          """format `value` before in order to insert it in the RQL query"""
2057 -        return '"%s"' % date.fromtimestamp(float(value) / 1000).strftime('%Y/%m/%d')
2058 +        try:
2059 +            date_value = ticks2datetime(float(value))
2060 +        except (ValueError, OverflowError):
2061 +            return u'"date out-of-range"'
2062 +        return '"%s"' % ustrftime(date_value, '%Y/%m/%d')
2063 
2064 
2065  class HasRelationFacet(AbstractFacet):
2066      """This class simply filter according to the presence of a relation
2067      (whatever the entity at the other end). It display a simple checkbox that
diff --git a/web/formwidgets.py b/web/formwidgets.py
@@ -592,12 +592,15 @@
2068                             domid, req.uiprops['CALENDAR_ICON'], fmt))
2069          if self.datestr is None:
2070              value = self.values(form, field)[0]
2071          else:
2072              value = self.datestr
2073 +        attrs = {}
2074 +        if self.settabindex:
2075 +            attrs['tabindex'] = req.next_tabindex()
2076          return tags.input(id=domid, name=domid, value=value,
2077 -                          type='text', size='10')
2078 +                          type='text', size='10', **attrs)
2079 
2080 
2081  class JQueryTimePicker(FieldWidget):
2082      """Use jquery.timePicker to define a time picker. Will return the time as an
2083      unicode string.
@@ -618,10 +621,13 @@
2084              domid, self.timestr, self.timesteps, self.separator))
2085          if self.timestr is None:
2086              value = self.values(form, field)[0]
2087          else:
2088              value = self.timestr
2089 +        attrs = {}
2090 +        if self.settabindex:
2091 +            attrs['tabindex'] = req.next_tabindex()
2092          return tags.input(id=domid, name=domid, value=value,
2093                            type='text', size='5')
2094 
2095 
2096  class JQueryDateTimePicker(FieldWidget):
diff --git a/web/test/unittest_reledit.py b/web/test/unittest_reledit.py
@@ -1,6 +1,6 @@
2097 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
2098 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
2099  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
2100  #
2101  # This file is part of CubicWeb.
2102  #
2103  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -31,13 +31,13 @@
2104          self.toto = self.req.create_entity('Personne', nom=u'Toto')
2105 
2106  class ClickAndEditFormTC(ReleditMixinTC, CubicWebTC):
2107 
2108      def test_default_config(self):
2109 -        reledit = {'title': """<div id="title-subject-%(eid)s-reledit" onmouseout="jQuery('#title-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#title-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="title-subject-%(eid)s-value" class="editableFieldValue">cubicweb-world-domination</div><div id="title-subject-%(eid)s" class="editableField hidden"><div id="title-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, %(eid)s, &#39;title&#39;, &#39;subject&#39;, &#39;title-subject-%(eid)s&#39;, false, &#39;&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
2110 -                   'long_desc': """<div id="long_desc-subject-%(eid)s-reledit" onmouseout="jQuery('#long_desc-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#long_desc-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="long_desc-subject-%(eid)s-value" class="editableFieldValue">&lt;not specified&gt;</div><div id="long_desc-subject-%(eid)s" class="editableField hidden"><div id="long_desc-subject-%(eid)s-add" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;edition&#39;, %(eid)s, &#39;long_desc&#39;, &#39;subject&#39;, &#39;long_desc-subject-%(eid)s&#39;, false, &#39;autolimited&#39;);" title="click to add a value"><img title="click to add a value" src="data/plus.png" alt="click to add a value"/></div></div></div>""",
2111 -                   'manager': """<div id="manager-subject-%(eid)s-reledit" onmouseout="jQuery('#manager-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#manager-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="manager-subject-%(eid)s-value" class="editableFieldValue">&lt;not specified&gt;</div><div id="manager-subject-%(eid)s" class="editableField hidden"><div id="manager-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, %(eid)s, &#39;manager&#39;, &#39;subject&#39;, &#39;manager-subject-%(eid)s&#39;, false, &#39;autolimited&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
2112 +        reledit = {'title': """<div id="title-subject-%(eid)s-reledit" onmouseout="jQuery('#title-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#title-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="title-subject-%(eid)s-value" class="editableFieldValue">cubicweb-world-domination</div><div id="title-subject-%(eid)s" class="editableField hidden"><div id="title-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, %(eid)s, &#39;title&#39;, &#39;subject&#39;, &#39;title-subject-%(eid)s&#39;, false, &#39;&#39;);" title="click to edit this field"><img title="click to edit this field" src="http://testing.fr/cubicweb/data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
2113 +                   'long_desc': """<div id="long_desc-subject-%(eid)s-reledit" onmouseout="jQuery('#long_desc-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#long_desc-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="long_desc-subject-%(eid)s-value" class="editableFieldValue">&lt;not specified&gt;</div><div id="long_desc-subject-%(eid)s" class="editableField hidden"><div id="long_desc-subject-%(eid)s-add" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;edition&#39;, %(eid)s, &#39;long_desc&#39;, &#39;subject&#39;, &#39;long_desc-subject-%(eid)s&#39;, false, &#39;autolimited&#39;);" title="click to add a value"><img title="click to add a value" src="http://testing.fr/cubicweb/data/plus.png" alt="click to add a value"/></div></div></div>""",
2114 +                   'manager': """<div id="manager-subject-%(eid)s-reledit" onmouseout="jQuery('#manager-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#manager-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="manager-subject-%(eid)s-value" class="editableFieldValue">&lt;not specified&gt;</div><div id="manager-subject-%(eid)s" class="editableField hidden"><div id="manager-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, %(eid)s, &#39;manager&#39;, &#39;subject&#39;, &#39;manager-subject-%(eid)s&#39;, false, &#39;autolimited&#39;);" title="click to edit this field"><img title="click to edit this field" src="http://testing.fr/cubicweb/data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
2115                     'composite_card11_2ttypes': """&lt;not specified&gt;""",
2116                     'concerns': """&lt;not specified&gt;"""}
2117 
2118          for rschema, ttypes, role in self.proj.e_schema.relation_definitions(includefinal=True):
2119              if rschema not in reledit:
@@ -74,11 +74,11 @@
2120  <tr>
2121  <td><button class="validateButton" tabindex="2" type="submit" value="button_ok"><img alt="OK_ICON" src="http://testing.fr/cubicweb/data/ok.png" />button_ok</button></td>
2122  <td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel(&#39;title-subject-%(eid)s&#39;)" tabindex="3" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://testing.fr/cubicweb/data/cancel.png" />button_cancel</button></td>
2123  </tr></table>
2124  </fieldset>
2125 -</form><div id="title-subject-%(eid)s" class="editableField hidden"><div id="title-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, %(eid)s, &#39;title&#39;, &#39;subject&#39;, &#39;title-subject-%(eid)s&#39;, false, &#39;&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
2126 +</form><div id="title-subject-%(eid)s" class="editableField hidden"><div id="title-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, %(eid)s, &#39;title&#39;, &#39;subject&#39;, &#39;title-subject-%(eid)s&#39;, false, &#39;&#39;);" title="click to edit this field"><img title="click to edit this field" src="http://testing.fr/cubicweb/data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
2127 
2128                       'long_desc': """<div id="long_desc-subject-%(eid)s-reledit" onmouseout="jQuery('#long_desc-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#long_desc-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="long_desc-subject-%(eid)s-value" class="editableFieldValue">&lt;not specified&gt;</div><form action="http://testing.fr/cubicweb/validateform?__onsuccess=window.parent.cw.reledit.onSuccess" method="post" enctype="application/x-www-form-urlencoded" id="long_desc-subject-%(eid)s-form" onsubmit="return freezeFormButtons(&#39;long_desc-subject-%(eid)s-form&#39;);" class="releditForm" cubicweb:target="eformframe">
2129  <fieldset>
2130  <input name="__form_id" type="hidden" value="edition" />
2131  <input name="__errorurl" type="hidden" value="http://testing.fr/cubicweb/view?rql=Blop&amp;vid=blop#long_desc-subject-%(eid)s-form" />
@@ -118,11 +118,11 @@
2132  <tr>
2133  <td><button class="validateButton" tabindex="7" type="submit" value="button_ok"><img alt="OK_ICON" src="http://testing.fr/cubicweb/data/ok.png" />button_ok</button></td>
2134  <td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel(&#39;long_desc-subject-%(eid)s&#39;)" tabindex="8" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://testing.fr/cubicweb/data/cancel.png" />button_cancel</button></td>
2135  </tr></table>
2136  </fieldset>
2137 -</form><div id="long_desc-subject-%(eid)s" class="editableField hidden"><div id="long_desc-subject-%(eid)s-add" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;edition&#39;, %(eid)s, &#39;long_desc&#39;, &#39;subject&#39;, &#39;long_desc-subject-%(eid)s&#39;, false, &#39;autolimited&#39;);" title="click to add a value"><img title="click to add a value" src="data/plus.png" alt="click to add a value"/></div></div></div>""",
2138 +</form><div id="long_desc-subject-%(eid)s" class="editableField hidden"><div id="long_desc-subject-%(eid)s-add" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;edition&#39;, %(eid)s, &#39;long_desc&#39;, &#39;subject&#39;, &#39;long_desc-subject-%(eid)s&#39;, false, &#39;autolimited&#39;);" title="click to add a value"><img title="click to add a value" src="http://testing.fr/cubicweb/data/plus.png" alt="click to add a value"/></div></div></div>""",
2139 
2140                       'manager': """<div id="manager-subject-%(eid)s-reledit" onmouseout="jQuery('#manager-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#manager-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="manager-subject-%(eid)s-value" class="editableFieldValue">&lt;not specified&gt;</div><form action="http://testing.fr/cubicweb/validateform?__onsuccess=window.parent.cw.reledit.onSuccess" method="post" enctype="application/x-www-form-urlencoded" id="manager-subject-%(eid)s-form" onsubmit="return freezeFormButtons(&#39;manager-subject-%(eid)s-form&#39;);" class="releditForm" cubicweb:target="eformframe">
2141  <fieldset>
2142  <input name="__form_id" type="hidden" value="base" />
2143  <input name="__errorurl" type="hidden" value="http://testing.fr/cubicweb/view?rql=Blop&amp;vid=blop#manager-subject-%(eid)s-form" />
@@ -154,11 +154,11 @@
2144  <tr>
2145  <td><button class="validateButton" tabindex="10" type="submit" value="button_ok"><img alt="OK_ICON" src="http://testing.fr/cubicweb/data/ok.png" />button_ok</button></td>
2146  <td><button class="validateButton" onclick="cw.reledit.cleanupAfterCancel(&#39;manager-subject-%(eid)s&#39;)" tabindex="11" type="button" value="button_cancel"><img alt="CANCEL_ICON" src="http://testing.fr/cubicweb/data/cancel.png" />button_cancel</button></td>
2147  </tr></table>
2148  </fieldset>
2149 -</form><div id="manager-subject-%(eid)s" class="editableField hidden"><div id="manager-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, %(eid)s, &#39;manager&#39;, &#39;subject&#39;, &#39;manager-subject-%(eid)s&#39;, false, &#39;autolimited&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
2150 +</form><div id="manager-subject-%(eid)s" class="editableField hidden"><div id="manager-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, %(eid)s, &#39;manager&#39;, &#39;subject&#39;, &#39;manager-subject-%(eid)s&#39;, false, &#39;autolimited&#39;);" title="click to edit this field"><img title="click to edit this field" src="http://testing.fr/cubicweb/data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
2151                       'composite_card11_2ttypes': """&lt;not specified&gt;""",
2152                       'concerns': """&lt;not specified&gt;"""
2153              }
2154          for rschema, ttypes, role in self.proj.e_schema.relation_definitions(includefinal=True):
2155              if rschema not in doreledit:
@@ -188,15 +188,15 @@
2156          reledit_ctrl.tag_subject_of(('Project', 'composite_card11_2ttypes', '*'),
2157                                     {'edit_target': 'related'})
2158          reledit_ctrl.tag_object_of(('Ticket', 'concerns', 'Project'),
2159                                     {'edit_target': 'rtype'})
2160          reledit = {
2161 -            'title': """<div id="title-subject-%(eid)s-reledit" onmouseout="jQuery('#title-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#title-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="title-subject-%(eid)s-value" class="editableFieldValue">cubicweb-world-domination</div><div id="title-subject-%(eid)s" class="editableField hidden"><div id="title-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, %(eid)s, &#39;title&#39;, &#39;subject&#39;, &#39;title-subject-%(eid)s&#39;, true, &#39;&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
2162 -            'long_desc': """<div id="long_desc-subject-%(eid)s-reledit" onmouseout="jQuery('#long_desc-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#long_desc-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="long_desc-subject-%(eid)s-value" class="editableFieldValue">&lt;long_desc is required&gt;</div><div id="long_desc-subject-%(eid)s" class="editableField hidden"><div id="long_desc-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, %(eid)s, &#39;long_desc&#39;, &#39;subject&#39;, &#39;long_desc-subject-%(eid)s&#39;, true, &#39;autolimited&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
2163 -            'manager': """<div id="manager-subject-%(eid)s-reledit" onmouseout="jQuery('#manager-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#manager-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="manager-subject-%(eid)s-value" class="editableFieldValue"><a href="http://testing.fr/cubicweb/personne/%(toto)s" title="">Toto</a></div><div id="manager-subject-%(eid)s" class="editableField hidden"><div id="manager-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;edition&#39;, %(eid)s, &#39;manager&#39;, &#39;subject&#39;, &#39;manager-subject-%(eid)s&#39;, false, &#39;autolimited&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div><div id="manager-subject-%(eid)s-delete" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;deleteconf&#39;, %(eid)s, &#39;manager&#39;, &#39;subject&#39;, &#39;manager-subject-%(eid)s&#39;, false, &#39;autolimited&#39;);" title="click to delete this value"><img title="click to delete this value" src="data/cancel.png" alt="click to delete this value"/></div></div></div>""",
2164 +            'title': """<div id="title-subject-%(eid)s-reledit" onmouseout="jQuery('#title-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#title-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="title-subject-%(eid)s-value" class="editableFieldValue">cubicweb-world-domination</div><div id="title-subject-%(eid)s" class="editableField hidden"><div id="title-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, %(eid)s, &#39;title&#39;, &#39;subject&#39;, &#39;title-subject-%(eid)s&#39;, true, &#39;&#39;);" title="click to edit this field"><img title="click to edit this field" src="http://testing.fr/cubicweb/data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
2165 +            'long_desc': """<div id="long_desc-subject-%(eid)s-reledit" onmouseout="jQuery('#long_desc-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#long_desc-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="long_desc-subject-%(eid)s-value" class="editableFieldValue">&lt;long_desc is required&gt;</div><div id="long_desc-subject-%(eid)s" class="editableField hidden"><div id="long_desc-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, %(eid)s, &#39;long_desc&#39;, &#39;subject&#39;, &#39;long_desc-subject-%(eid)s&#39;, true, &#39;autolimited&#39;);" title="click to edit this field"><img title="click to edit this field" src="http://testing.fr/cubicweb/data/pen_icon.png" alt="click to edit this field"/></div></div></div>""",
2166 +            'manager': """<div id="manager-subject-%(eid)s-reledit" onmouseout="jQuery('#manager-subject-%(eid)s').addClass('hidden')" onmouseover="jQuery('#manager-subject-%(eid)s').removeClass('hidden')" class="releditField"><div id="manager-subject-%(eid)s-value" class="editableFieldValue"><a href="http://testing.fr/cubicweb/personne/%(toto)s" title="">Toto</a></div><div id="manager-subject-%(eid)s" class="editableField hidden"><div id="manager-subject-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;edition&#39;, %(eid)s, &#39;manager&#39;, &#39;subject&#39;, &#39;manager-subject-%(eid)s&#39;, false, &#39;autolimited&#39;);" title="click to edit this field"><img title="click to edit this field" src="http://testing.fr/cubicweb/data/pen_icon.png" alt="click to edit this field"/></div><div id="manager-subject-%(eid)s-delete" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;deleteconf&#39;, %(eid)s, &#39;manager&#39;, &#39;subject&#39;, &#39;manager-subject-%(eid)s&#39;, false, &#39;autolimited&#39;);" title="click to delete this value"><img title="click to delete this value" src="http://testing.fr/cubicweb/data/cancel.png" alt="click to delete this value"/></div></div></div>""",
2167              'composite_card11_2ttypes': """&lt;not specified&gt;""",
2168 -            'concerns': """<div id="concerns-object-%(eid)s-reledit" onmouseout="jQuery('#concerns-object-%(eid)s').addClass('hidden')" onmouseover="jQuery('#concerns-object-%(eid)s').removeClass('hidden')" class="releditField"><div id="concerns-object-%(eid)s-value" class="editableFieldValue"><a href="http://testing.fr/cubicweb/ticket/%(tick)s" title="">write the code</a></div><div id="concerns-object-%(eid)s" class="editableField hidden"><div id="concerns-object-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, %(eid)s, &#39;concerns&#39;, &#39;object&#39;, &#39;concerns-object-%(eid)s&#39;, false, &#39;autolimited&#39;);" title="click to edit this field"><img title="click to edit this field" src="data/pen_icon.png" alt="click to edit this field"/></div></div></div>"""
2169 +            'concerns': """<div id="concerns-object-%(eid)s-reledit" onmouseout="jQuery('#concerns-object-%(eid)s').addClass('hidden')" onmouseover="jQuery('#concerns-object-%(eid)s').removeClass('hidden')" class="releditField"><div id="concerns-object-%(eid)s-value" class="editableFieldValue"><a href="http://testing.fr/cubicweb/ticket/%(tick)s" title="">write the code</a></div><div id="concerns-object-%(eid)s" class="editableField hidden"><div id="concerns-object-%(eid)s-update" class="editableField" onclick="cw.reledit.loadInlineEditionForm(&#39;base&#39;, %(eid)s, &#39;concerns&#39;, &#39;object&#39;, &#39;concerns-object-%(eid)s&#39;, false, &#39;autolimited&#39;);" title="click to edit this field"><img title="click to edit this field" src="http://testing.fr/cubicweb/data/pen_icon.png" alt="click to edit this field"/></div></div></div>"""
2170              }
2171          for rschema, ttypes, role in self.proj.e_schema.relation_definitions(includefinal=True):
2172              if rschema not in reledit:
2173                  continue
2174              rtype = rschema.type
diff --git a/web/webconfig.py b/web/webconfig.py
@@ -1,6 +1,6 @@
2175 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
2176 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
2177  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
2178  #
2179  # This file is part of CubicWeb.
2180  #
2181  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -295,11 +295,11 @@
2182      def _init_base_url(self):
2183          # normalize base url(s)
2184          baseurl = self['base-url'] or self.default_base_url()
2185          if baseurl and baseurl[-1] != '/':
2186              baseurl += '/'
2187 -        if not self.repairing:
2188 +        if not (self.repairing or self.creating):
2189              self.global_set_option('base-url', baseurl)
2190          httpsurl = self['https-url']
2191          if httpsurl:
2192              if httpsurl[-1] != '/':
2193                  httpsurl += '/'
diff --git a/web/webctl.py b/web/webctl.py
@@ -1,6 +1,6 @@
2194 -# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
2195 +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
2196  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
2197  #
2198  # This file is part of CubicWeb.
2199  #
2200  # CubicWeb is free software: you can redistribute it and/or modify it under the
@@ -15,28 +15,31 @@
2201  #
2202  # You should have received a copy of the GNU Lesser General Public License along
2203  # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
2204  """cubicweb-ctl commands and command handlers common to twisted/modpython
2205  web configuration
2206 +"""
2207 
2208 -"""
2209  __docformat__ = "restructuredtext en"
2210 
2211 +from logilab.common.shellutils import ASK
2212 +
2213  from cubicweb.toolsutils import CommandHandler, underline_title
2214 -from logilab.common.shellutils import ASK
2215 
2216  class WebCreateHandler(CommandHandler):
2217      cmdname = 'create'
2218 
2219 -    def bootstrap(self, cubes, inputlevel=0):
2220 +    def bootstrap(self, cubes, automatic=False, inputlevel=0):
2221          """bootstrap this configuration"""
2222 -        print '\n' + underline_title('Generic web configuration')
2223 -        config = self.config
2224 -        if config.repo_method == 'pyro' or config.pyro_enabled():
2225 -            print '\n' + underline_title('Pyro configuration')
2226 -            config.input_config('pyro', inputlevel)
2227 -        if ASK.confirm('Allow anonymous access ?', False):
2228 -            config.global_set_option('anonymous-user', 'anon')
2229 -            config.global_set_option('anonymous-password', 'anon')
2230 +        if not automatic:
2231 +            print '\n' + underline_title('Generic web configuration')
2232 +            config = self.config
2233 +            if config.repo_method == 'pyro' or config.pyro_enabled():
2234 +                print '\n' + underline_title('Pyro configuration')
2235 +                config.input_config('pyro', inputlevel)
2236 +            config.input_config('web', inputlevel)
2237 +            if ASK.confirm('Allow anonymous access ?', False):
2238 +                config.global_set_option('anonymous-user', 'anon')
2239 +                config.global_set_option('anonymous-password', 'anon')
2240 
2241 -    def postcreate(self):
2242 +    def postcreate(self, *args, **kwargs):
2243          """hooks called once instance's initialization has been completed"""