--- a/appobject.py Thu May 19 10:35:20 2011 +0200
+++ b/appobject.py Thu May 19 10:36:26 2011 +0200
@@ -180,12 +180,13 @@
return self.__class__.__name__
def search_selector(self, selector):
- """search for the given selector or selector instance in the selectors
- tree. Return it of None if not found
+ """search for the given selector, selector instance or tuple of
+ selectors in the selectors tree. Return None if not found.
"""
if self is selector:
return self
- if isinstance(selector, type) and isinstance(self, selector):
+ if (isinstance(selector, type) or isinstance(selector, tuple)) and \
+ isinstance(self, selector):
return self
return None
@@ -250,8 +251,8 @@
return merged_selectors
def search_selector(self, selector):
- """search for the given selector or selector instance in the selectors
- tree. Return it of None if not found
+ """search for the given selector or selector instance (or tuple of
+ selectors) in the selectors tree. Return None if not found
"""
for childselector in self.selectors:
if childselector is selector:
@@ -259,7 +260,8 @@
found = childselector.search_selector(selector)
if found is not None:
return found
- return None
+ # if not found in children, maybe we are looking for self?
+ return super(MultiSelector, self).search_selector(selector)
class AndSelector(MultiSelector):
--- a/cwvreg.py Thu May 19 10:35:20 2011 +0200
+++ b/cwvreg.py Thu May 19 10:36:26 2011 +0200
@@ -412,10 +412,8 @@
if not isinstance(view, class_deprecated)]
try:
view = self._select_best(views, req, rset=rset, **kwargs)
- if view.linkable():
+ if view is not None and view.linkable():
yield view
- except NoSelectableObject:
- continue
except Exception:
self.exception('error while trying to select %s view for %s',
vid, rset)
--- a/devtools/__init__.py Thu May 19 10:35:20 2011 +0200
+++ b/devtools/__init__.py Thu May 19 10:36:26 2011 +0200
@@ -28,15 +28,17 @@
import pickle
import glob
import warnings
+import hashlib
from datetime import timedelta
from os.path import (abspath, join, exists, basename, dirname, normpath, split,
isfile, isabs, splitext, isdir, expanduser)
from functools import partial
-import hashlib
from logilab.common.date import strptime
from logilab.common.decorators import cached, clear_cache
-from cubicweb import CW_SOFTWARE_ROOT, ConfigurationError, schema, cwconfig, BadConnectionId
+
+from cubicweb import ConfigurationError, ExecutionError, BadConnectionId
+from cubicweb import CW_SOFTWARE_ROOT, schema, cwconfig
from cubicweb.server.serverconfig import ServerConfiguration
from cubicweb.etwist.twconfig import TwistedConfiguration
@@ -197,7 +199,10 @@
directory from wich tests are launched or by specifying an alternative
sources file using self.sourcefile.
"""
- sources = super(TestServerConfiguration, self).sources()
+ try:
+ sources = super(TestServerConfiguration, self).sources()
+ except ExecutionError:
+ sources = {}
if not sources:
sources = DEFAULT_SOURCES
if 'admin' not in sources:
@@ -207,9 +212,6 @@
# web config methods needed here for cases when we use this config as a web
# config
- def instance_md5_version(self):
- return ''
-
def default_base_url(self):
return BASE_URL
--- a/devtools/fake.py Thu May 19 10:35:20 2011 +0200
+++ b/devtools/fake.py Thu May 19 10:36:26 2011 +0200
@@ -138,12 +138,14 @@
class FakeSession(RequestSessionBase):
- read_security = write_security = True
- set_read_security = set_write_security = lambda *args, **kwargs: None
- def __init__(self, repo=None, user=None):
+ def __init__(self, repo=None, user=None, vreg=None):
self.repo = repo
- self.vreg = getattr(self.repo, 'vreg', CubicWebVRegistry(FakeConfig(), initlog=False))
+ if vreg is None:
+ vreg = getattr(self.repo, 'vreg', None)
+ if vreg is None:
+ vreg = CubicWebVRegistry(FakeConfig(), initlog=False)
+ self.vreg = vreg
self.pool = FakePool()
self.user = user or FakeUser()
self.is_internal_session = False
@@ -162,6 +164,13 @@
def set_entity_cache(self, entity):
pass
+ # for use with enabled_security context manager
+ read_security = write_security = True
+ def init_security(self, *args):
+ return None, None
+ def reset_security(self, *args):
+ return
+
class FakeRepo(object):
querier = None
def __init__(self, schema, vreg=None, config=None):
--- a/devtools/repotest.py Thu May 19 10:35:20 2011 +0200
+++ b/devtools/repotest.py Thu May 19 10:36:26 2011 +0200
@@ -264,6 +264,7 @@
u._groups = set(groups)
s = Session(u, self.repo)
s._threaddata.pool = self.pool
+ s._threaddata.ctx_count = 1
# register session to ensure it gets closed
self._dumb_sessions.append(s)
return s
--- a/devtools/testlib.py Thu May 19 10:35:20 2011 +0200
+++ b/devtools/testlib.py Thu May 19 10:36:26 2011 +0200
@@ -568,6 +568,8 @@
if views:
try:
view = viewsvreg._select_best(views, req, rset=rset)
+ if view is None:
+ raise NoSelectableObject((req,), {'rset':rset}, views)
if view.linkable():
yield view
else:
--- a/entities/test/unittest_wfobjs.py Thu May 19 10:35:20 2011 +0200
+++ b/entities/test/unittest_wfobjs.py Thu May 19 10:36:26 2011 +0200
@@ -165,7 +165,7 @@
user = self.user()
iworkflowable = user.cw_adapt_to('IWorkflowable')
iworkflowable.fire_transition('deactivate', comment=u'deactivate user')
- user.clear_all_caches()
+ user.cw_clear_all_caches()
self.assertEqual(iworkflowable.state, 'deactivated')
self._test_manager_deactivate(user)
trinfo = self._test_manager_deactivate(user)
@@ -192,7 +192,7 @@
self.commit()
iworkflowable.fire_transition('wake up')
self.commit()
- user.clear_all_caches()
+ user.cw_clear_all_caches()
self.assertEqual(iworkflowable.state, 'deactivated')
# XXX test managers can change state without matching transition
@@ -274,14 +274,14 @@
self.assertEqual(iworkflowable.subworkflow_input_transition(), None)
iworkflowable.fire_transition('swftr1', u'go')
self.commit()
- group.clear_all_caches()
+ group.cw_clear_all_caches()
self.assertEqual(iworkflowable.current_state.eid, swfstate1.eid)
self.assertEqual(iworkflowable.current_workflow.eid, swf.eid)
self.assertEqual(iworkflowable.main_workflow.eid, mwf.eid)
self.assertEqual(iworkflowable.subworkflow_input_transition().eid, swftr1.eid)
iworkflowable.fire_transition('tr1', u'go')
self.commit()
- group.clear_all_caches()
+ group.cw_clear_all_caches()
self.assertEqual(iworkflowable.current_state.eid, state2.eid)
self.assertEqual(iworkflowable.current_workflow.eid, mwf.eid)
self.assertEqual(iworkflowable.main_workflow.eid, mwf.eid)
@@ -295,10 +295,10 @@
# force back to state1
iworkflowable.change_state('state1', u'gadget')
iworkflowable.fire_transition('swftr1', u'au')
- group.clear_all_caches()
+ group.cw_clear_all_caches()
iworkflowable.fire_transition('tr2', u'chapeau')
self.commit()
- group.clear_all_caches()
+ group.cw_clear_all_caches()
self.assertEqual(iworkflowable.current_state.eid, state3.eid)
self.assertEqual(iworkflowable.current_workflow.eid, mwf.eid)
self.assertEqual(iworkflowable.main_workflow.eid, mwf.eid)
@@ -390,7 +390,7 @@
):
iworkflowable.fire_transition(trans)
self.commit()
- group.clear_all_caches()
+ group.cw_clear_all_caches()
self.assertEqual(iworkflowable.state, nextstate)
@@ -408,11 +408,11 @@
wf.add_state('asleep', initial=True)
self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
{'wf': wf.eid, 'x': self.member.eid})
- self.member.clear_all_caches()
+ self.member.cw_clear_all_caches()
iworkflowable = self.member.cw_adapt_to('IWorkflowable')
self.assertEqual(iworkflowable.state, 'activated')# no change before commit
self.commit()
- self.member.clear_all_caches()
+ self.member.cw_clear_all_caches()
self.assertEqual(iworkflowable.current_workflow.eid, wf.eid)
self.assertEqual(iworkflowable.state, 'asleep')
self.assertEqual(iworkflowable.workflow_history, ())
@@ -429,7 +429,7 @@
self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
{'wf': wf.eid, 'x': self.member.eid})
self.commit()
- self.member.clear_all_caches()
+ self.member.cw_clear_all_caches()
self.assertEqual(iworkflowable.current_workflow.eid, wf.eid)
self.assertEqual(iworkflowable.state, 'asleep')
self.assertEqual(parse_hist(iworkflowable.workflow_history),
@@ -472,10 +472,10 @@
self.commit()
self.execute('DELETE X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
{'wf': wf.eid, 'x': self.member.eid})
- self.member.clear_all_caches()
+ self.member.cw_clear_all_caches()
self.assertEqual(iworkflowable.state, 'asleep')# no change before commit
self.commit()
- self.member.clear_all_caches()
+ self.member.cw_clear_all_caches()
self.assertEqual(iworkflowable.current_workflow.name, "default user workflow")
self.assertEqual(iworkflowable.state, 'activated')
self.assertEqual(parse_hist(iworkflowable.workflow_history),
@@ -504,13 +504,13 @@
self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
{'wf': wf.eid, 'x': user.eid})
self.commit()
- user.clear_all_caches()
+ user.cw_clear_all_caches()
self.assertEqual(iworkflowable.state, 'asleep')
self.assertEqual([t.name for t in iworkflowable.possible_transitions()],
['rest'])
iworkflowable.fire_transition('rest')
self.commit()
- user.clear_all_caches()
+ user.cw_clear_all_caches()
self.assertEqual(iworkflowable.state, 'asleep')
self.assertEqual([t.name for t in iworkflowable.possible_transitions()],
['rest'])
@@ -520,7 +520,7 @@
self.commit()
iworkflowable.fire_transition('rest')
self.commit()
- user.clear_all_caches()
+ user.cw_clear_all_caches()
self.assertEqual(iworkflowable.state, 'dead')
self.assertEqual(parse_hist(iworkflowable.workflow_history),
[('asleep', 'asleep', 'rest', None),
--- a/entities/wfobjs.py Thu May 19 10:35:20 2011 +0200
+++ b/entities/wfobjs.py Thu May 19 10:36:26 2011 +0200
@@ -326,8 +326,8 @@
result[ep.subwf_state.eid] = ep.destination and ep.destination.eid
return result
- def clear_all_caches(self):
- super(WorkflowTransition, self).clear_all_caches()
+ def cw_clear_all_caches(self):
+ super(WorkflowTransition, self).cw_clear_all_caches()
clear_cache(self, 'exit_points')
--- a/entity.py Thu May 19 10:35:20 2011 +0200
+++ b/entity.py Thu May 19 10:36:26 2011 +0200
@@ -920,7 +920,7 @@
assert role
self._cw_related_cache.pop('%s_%s' % (rtype, role), None)
- def clear_all_caches(self): # XXX cw_clear_all_caches
+ def cw_clear_all_caches(self):
"""flush all caches on this entity. Further attributes/relations access
will triggers new database queries to get back values.
@@ -1002,6 +1002,10 @@
# deprecated stuff #########################################################
+ @deprecated('[3.13] use entity.cw_clear_all_caches()')
+ def clear_all_caches(self):
+ return self.cw_clear_all_caches()
+
@deprecated('[3.9] use entity.cw_attr_value(attr)')
def get_value(self, name):
return self.cw_attr_value(name)
--- a/etwist/server.py Thu May 19 10:35:20 2011 +0200
+++ b/etwist/server.py Thu May 19 10:36:26 2011 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -17,14 +17,19 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""twisted server for CubicWeb web instances"""
+from __future__ import with_statement
+
__docformat__ = "restructuredtext en"
import sys
import os
+import os.path as osp
import select
import errno
import traceback
import threading
+import re
+import hashlib
from os.path import join
from time import mktime
from datetime import date, timedelta
@@ -41,7 +46,8 @@
from logilab.common.decorators import monkeypatch
-from cubicweb import AuthenticationError, ConfigurationError, CW_EVENT_MANAGER
+from cubicweb import (AuthenticationError, ConfigurationError,
+ CW_EVENT_MANAGER, CubicWebException)
from cubicweb.utils import json_dumps
from cubicweb.web import Redirect, DirectResponse, StatusResponse, LogOut
from cubicweb.web.application import CubicWebPublisher
@@ -70,13 +76,73 @@
code=http.FORBIDDEN,
stream='Access forbidden')
-class File(static.File):
- """Prevent from listing directories"""
+
+class NoListingFile(static.File):
def directoryListing(self):
return ForbiddenDirectoryLister()
-class LongTimeExpiringFile(File):
+class DataLookupDirectory(NoListingFile):
+ def __init__(self, config, path):
+ self.md5_version = config.instance_md5_version()
+ NoListingFile.__init__(self, path)
+ self.config = config
+ self.here = path
+ self._defineChildResources()
+ if self.config.debugmode:
+ self.data_modconcat_basepath = '/data/??'
+ else:
+ self.data_modconcat_basepath = '/data/%s/??' % self.md5_version
+
+ def _defineChildResources(self):
+ self.putChild(self.md5_version, self)
+
+ def getChild(self, path, request):
+ if not path:
+ uri = request.uri
+ if uri.startswith('/https/'):
+ uri = uri[6:]
+ if uri.startswith(self.data_modconcat_basepath):
+ resource_relpath = uri[len(self.data_modconcat_basepath):]
+ if resource_relpath:
+ paths = resource_relpath.split(',')
+ try:
+ return ConcatFiles(self.config, paths)
+ except ConcatFileNotFoundError:
+ return self.childNotFound
+ return self.directoryListing()
+ childpath = join(self.here, path)
+ dirpath, rid = self.config.locate_resource(childpath)
+ if dirpath is None:
+ # resource not found
+ return self.childNotFound
+ filepath = os.path.join(dirpath, rid)
+ if os.path.isdir(filepath):
+ resource = DataLookupDirectory(self.config, childpath)
+ # cache resource for this segment path to avoid recomputing
+ # directory lookup
+ self.putChild(path, resource)
+ return resource
+ else:
+ return NoListingFile(filepath)
+
+
+class FCKEditorResource(NoListingFile):
+ def __init__(self, config, path):
+ NoListingFile.__init__(self, path)
+ self.config = config
+
+ def getChild(self, path, request):
+ pre_path = request.path.split('/')[1:]
+ if pre_path[0] == 'https':
+ pre_path.pop(0)
+ uiprops = self.config.https_uiprops
+ else:
+ uiprops = self.config.uiprops
+ return static.File(osp.join(uiprops['FCKEDITOR_PATH'], path))
+
+
+class LongTimeExpiringFile(DataLookupDirectory):
"""overrides static.File and sets a far future ``Expires`` date
on the resouce.
@@ -88,28 +154,77 @@
etc.
"""
+ def _defineChildResources(self):
+ pass
+
def render(self, request):
# XXX: Don't provide additional resource information to error responses
#
# the HTTP RFC recommands not going further than 1 year ahead
expires = date.today() + timedelta(days=6*30)
request.setHeader('Expires', generateDateTime(mktime(expires.timetuple())))
- return File.render(self, request)
+ return DataLookupDirectory.render(self, request)
+
+
+class ConcatFileNotFoundError(CubicWebException):
+ pass
+
+
+class ConcatFiles(LongTimeExpiringFile):
+ def __init__(self, config, paths):
+ _, ext = osp.splitext(paths[0])
+ # create a unique / predictable filename
+ fname = 'cache_concat_' + hashlib.md5(';'.join(paths)).hexdigest() + ext
+ filepath = osp.join(config.appdatahome, 'uicache', fname)
+ LongTimeExpiringFile.__init__(self, config, filepath)
+ self._concat_cached_filepath(filepath, paths)
+ def _concat_cached_filepath(self, filepath, paths):
+ if not self._up_to_date(filepath, paths):
+ concat_data = []
+ for path in paths:
+ # FIXME locate_resource is called twice() in debug-mode, but
+ # it's a @cached method
+ dirpath, rid = self.config.locate_resource(path)
+ if rid is None:
+ raise ConcatFileNotFoundError(path)
+ concat_data.append(open(osp.join(dirpath, rid)).read())
+ with open(filepath, 'wb') as f:
+ f.write('\n'.join(concat_data))
+
+ def _up_to_date(self, filepath, paths):
+ """
+ The concat-file is considered up-to-date if it exists.
+ In debug mode, an additional check is performed to make sure that
+ concat-file is more recent than all concatenated files
+ """
+ if not osp.isfile(filepath):
+ return False
+ if self.config.debugmode:
+ concat_lastmod = os.stat(filepath).st_mtime
+ for path in paths:
+ dirpath, rid = self.config.locate_resource(path)
+ if rid is None:
+ raise ConcatFileNotFoundError(path)
+ path = osp.join(dirpath, rid)
+ if os.stat(path).st_mtime > concat_lastmod:
+ return False
+ return True
class CubicWebRootResource(resource.Resource):
def __init__(self, config, vreg=None):
+ resource.Resource.__init__(self)
self.config = config
# instantiate publisher here and not in init_publisher to get some
# checks done before daemonization (eg versions consistency)
self.appli = CubicWebPublisher(config, vreg=vreg)
self.base_url = config['base-url']
self.https_url = config['https-url']
- self.children = {}
- self.static_directories = set(('data%s' % config.instance_md5_version(),
- 'data', 'static', 'fckeditor'))
global MAX_POST_LENGTH
MAX_POST_LENGTH = config['max-post-length']
+ self.putChild('static', NoListingFile(config.static_directory))
+ self.putChild('fckeditor', FCKEditorResource(self.config, ''))
+ self.putChild('data', DataLookupDirectory(self.config, ''))
def init_publisher(self):
config = self.config
@@ -152,38 +267,6 @@
def getChild(self, path, request):
"""Indicate which resource to use to process down the URL's path"""
- pre_path = request.path.split('/')[1:]
- if pre_path[0] == 'https':
- pre_path.pop(0)
- uiprops = self.config.https_uiprops
- else:
- uiprops = self.config.uiprops
- directory = pre_path[0]
- # Anything in data/, static/, fckeditor/ and the generated versioned
- # data directory is treated as static files
- if directory in self.static_directories:
- # take care fckeditor may appears as root directory or as a data
- # subdirectory
- if directory == 'static':
- return File(self.config.static_directory)
- if directory == 'fckeditor':
- return File(uiprops['FCKEDITOR_PATH'])
- if directory != 'data':
- # versioned directory, use specific file with http cache
- # headers so their are cached for a very long time
- cls = LongTimeExpiringFile
- else:
- cls = File
- if path == 'fckeditor':
- return cls(uiprops['FCKEDITOR_PATH'])
- if path == directory: # recurse
- return self
- datadir, path = self.config.locate_resource(path)
- if datadir is None:
- return self # recurse
- self.debug('static file %s from %s', path, datadir)
- return cls(join(datadir, path))
- # Otherwise we use this single resource
return self
def render(self, request):
--- a/hooks/metadata.py Thu May 19 10:35:20 2011 +0200
+++ b/hooks/metadata.py Thu May 19 10:36:26 2011 +0200
@@ -68,8 +68,9 @@
def precommit_event(self):
session = self.session
relations = [(eid, session.user.eid) for eid in self.get_data()
- # don't consider entities that have been created and
- # deleted in the same transaction
+ # don't consider entities that have been created and deleted in
+ # the same transaction, nor ones where created_by has been
+ # explicitly set
if not session.deleted_in_transaction(eid) and \
not session.entity_from_eid(eid).created_by]
session.add_relations([('created_by', relations)])
--- a/hooks/workflow.py Thu May 19 10:35:20 2011 +0200
+++ b/hooks/workflow.py Thu May 19 10:36:26 2011 +0200
@@ -148,7 +148,7 @@
class WorkflowHook(hook.Hook):
__abstract__ = True
- category = 'workflow'
+ category = 'metadata'
class SetInitialStateHook(WorkflowHook):
@@ -160,21 +160,15 @@
_SetInitialStateOp(self._cw, entity=self.entity)
-class PrepareStateChangeHook(WorkflowHook):
- """record previous state information"""
- __regid__ = 'cwdelstate'
- __select__ = WorkflowHook.__select__ & hook.match_rtype('in_state')
- events = ('before_delete_relation',)
+class FireTransitionHook(WorkflowHook):
+ """check the transition is allowed and add missing information into the
+ TrInfo entity.
- def __call__(self):
- self._cw.transaction_data.setdefault('pendingrelations', []).append(
- (self.eidfrom, self.rtype, self.eidto))
-
-
-class FireTransitionHook(WorkflowHook):
- """check the transition is allowed, add missing information. Expect that:
+ Expect that:
* wf_info_for inlined relation is set
* by_transition or to_state (managers only) inlined relation is set
+
+ Check for automatic transition to be fired at the end
"""
__regid__ = 'wffiretransition'
__select__ = WorkflowHook.__select__ & is_instance('TrInfo')
@@ -273,7 +267,7 @@
class FiredTransitionHook(WorkflowHook):
- """change related entity state"""
+ """change related entity state and handle exit of subworkflow"""
__regid__ = 'wffiretransition'
__select__ = WorkflowHook.__select__ & is_instance('TrInfo')
events = ('after_add_entity',)
@@ -296,6 +290,7 @@
__regid__ = 'wfcheckinstate'
__select__ = WorkflowHook.__select__ & hook.match_rtype('in_state')
events = ('before_add_relation',)
+ category = 'integrity'
def __call__(self):
session = self._cw
--- a/i18n/de.po Thu May 19 10:35:20 2011 +0200
+++ b/i18n/de.po Thu May 19 10:36:26 2011 +0200
@@ -2382,6 +2382,9 @@
msgid "external page"
msgstr "externe Seite"
+msgid "facet-loading-msg"
+msgstr ""
+
msgid "facet.filters"
msgstr ""
@@ -3178,6 +3181,12 @@
msgid "no associated permissions"
msgstr "keine entsprechende Berechtigung"
+msgid "no content next link"
+msgstr ""
+
+msgid "no content prev link"
+msgstr ""
+
#, python-format
msgid "no edited fields specified for entity %s"
msgstr "kein Eingabefeld spezifiziert Für Entität %s"
@@ -3926,6 +3935,12 @@
msgstr ""
"Der Wert \"%s\" wird bereits benutzt, bitte verwenden Sie einen anderen Wert"
+msgid "there is no next page"
+msgstr ""
+
+msgid "there is no previous page"
+msgstr ""
+
msgid "this action is not reversible!"
msgstr "Achtung! Diese Aktion ist unumkehrbar."
--- a/i18n/en.po Thu May 19 10:35:20 2011 +0200
+++ b/i18n/en.po Thu May 19 10:36:26 2011 +0200
@@ -5,7 +5,7 @@
msgstr ""
"Project-Id-Version: 2.0\n"
"POT-Creation-Date: 2006-01-12 17:35+CET\n"
-"PO-Revision-Date: 2010-09-15 14:55+0200\n"
+"PO-Revision-Date: 2011-04-29 12:57+0200\n"
"Last-Translator: Sylvain Thenault <sylvain.thenault@logilab.fr>\n"
"Language-Team: English <devel@logilab.fr.org>\n"
"Language: en\n"
@@ -2324,6 +2324,9 @@
msgid "external page"
msgstr ""
+msgid "facet-loading-msg"
+msgstr "processing, please wait"
+
msgid "facet.filters"
msgstr "filter"
@@ -3089,6 +3092,12 @@
msgid "no associated permissions"
msgstr ""
+msgid "no content next link"
+msgstr ""
+
+msgid "no content prev link"
+msgstr ""
+
#, python-format
msgid "no edited fields specified for entity %s"
msgstr ""
@@ -3821,6 +3830,12 @@
msgid "the value \"%s\" is already used, use another one"
msgstr ""
+msgid "there is no next page"
+msgstr ""
+
+msgid "there is no previous page"
+msgstr ""
+
msgid "this action is not reversible!"
msgstr ""
--- a/i18n/es.po Thu May 19 10:35:20 2011 +0200
+++ b/i18n/es.po Thu May 19 10:36:26 2011 +0200
@@ -2425,6 +2425,9 @@
msgid "external page"
msgstr "Página externa"
+msgid "facet-loading-msg"
+msgstr ""
+
msgid "facet.filters"
msgstr "Filtros"
@@ -3146,11 +3149,11 @@
msgctxt "CWSource"
msgid "name"
-msgstr "nombre"
+msgstr ""
msgctxt "State"
msgid "name"
-msgstr "Nombre"
+msgstr "nombre"
msgctxt "Transition"
msgid "name"
@@ -3219,6 +3222,12 @@
msgid "no associated permissions"
msgstr "No existe permiso asociado"
+msgid "no content next link"
+msgstr ""
+
+msgid "no content prev link"
+msgstr ""
+
#, python-format
msgid "no edited fields specified for entity %s"
msgstr "Ningún campo editable especificado para la entidad %s"
@@ -3976,6 +3985,12 @@
msgid "the value \"%s\" is already used, use another one"
msgstr "El valor \"%s\" ya esta en uso, favor de utilizar otro"
+msgid "there is no next page"
+msgstr ""
+
+msgid "there is no previous page"
+msgstr ""
+
msgid "this action is not reversible!"
msgstr "Esta acción es irreversible!."
--- a/i18n/fr.po Thu May 19 10:35:20 2011 +0200
+++ b/i18n/fr.po Thu May 19 10:36:26 2011 +0200
@@ -4,7 +4,7 @@
msgid ""
msgstr ""
"Project-Id-Version: cubicweb 2.46.0\n"
-"PO-Revision-Date: 2011-01-03 14:35+0100\n"
+"PO-Revision-Date: 2011-04-29 12:57+0200\n"
"Last-Translator: Logilab Team <contact@logilab.fr>\n"
"Language-Team: fr <contact@logilab.fr>\n"
"Language: \n"
@@ -2423,6 +2423,9 @@
msgid "external page"
msgstr "page externe"
+msgid "facet-loading-msg"
+msgstr "en cours de traitement, merci de patienter"
+
msgid "facet.filters"
msgstr "facettes"
@@ -3218,6 +3221,12 @@
msgid "no associated permissions"
msgstr "aucune permission associée"
+msgid "no content next link"
+msgstr ""
+
+msgid "no content prev link"
+msgstr ""
+
#, python-format
msgid "no edited fields specified for entity %s"
msgstr "aucun champ à éditer spécifié pour l'entité %s"
@@ -3976,6 +3985,12 @@
msgid "the value \"%s\" is already used, use another one"
msgstr "la valeur \"%s\" est déjà utilisée, veuillez utiliser une autre valeur"
+msgid "there is no next page"
+msgstr ""
+
+msgid "there is no previous page"
+msgstr ""
+
msgid "this action is not reversible!"
msgstr ""
"Attention ! Cette opération va détruire les données de façon irréversible."
--- a/rset.py Thu May 19 10:35:20 2011 +0200
+++ b/rset.py Thu May 19 10:36:26 2011 +0200
@@ -475,43 +475,57 @@
entity.eid = eid
# cache entity
req.set_entity_cache(entity)
- eschema = entity.e_schema
# try to complete the entity if there are some additional columns
if len(rowvalues) > 1:
- rqlst = self.syntax_tree()
- if rqlst.TYPE == 'select':
- # UNION query, find the subquery from which this entity has been
- # found
- select, col = rqlst.locate_subquery(col, etype, self.args)
+ eschema = entity.e_schema
+ eid_col, attr_cols, rel_cols = self._rset_structure(eschema, col)
+ entity.eid = rowvalues[eid_col]
+ for attr, col_idx in attr_cols.items():
+ entity.cw_attr_cache[attr] = rowvalues[col_idx]
+ for (rtype, role), col_idx in rel_cols.items():
+ value = rowvalues[col_idx]
+ if value is None:
+ if role == 'subject':
+ rql = 'Any Y WHERE X %s Y, X eid %s'
+ else:
+ rql = 'Any Y WHERE Y %s X, X eid %s'
+ rrset = ResultSet([], rql % (rtype, entity.eid))
+ rrset.req = req
+ else:
+ rrset = self._build_entity(row, col_idx).as_rset()
+ entity.cw_set_relation_cache(rtype, role, rrset)
+ return entity
+
+ @cached
+ def _rset_structure(self, eschema, entity_col):
+ eid_col = col = entity_col
+ rqlst = self.syntax_tree()
+ attr_cols = {}
+ rel_cols = {}
+ if rqlst.TYPE == 'select':
+ # UNION query, find the subquery from which this entity has been
+ # found
+ select, col = rqlst.locate_subquery(entity_col, eschema.type, self.args)
+ else:
+ select = rqlst
+ # take care, due to outer join support, we may find None
+ # values for non final relation
+ for i, attr, role in attr_desc_iterator(select, col, entity_col):
+ if role == 'subject':
+ rschema = eschema.subjrels[attr]
else:
- select = rqlst
- # take care, due to outer join support, we may find None
- # values for non final relation
- for i, attr, role in attr_desc_iterator(select, col, entity.cw_col):
- if role == 'subject':
- rschema = eschema.subjrels[attr]
- if rschema.final:
- if attr == 'eid':
- entity.eid = rowvalues[i]
- else:
- entity.cw_attr_cache[attr] = rowvalues[i]
- continue
+ rschema = eschema.objrels[attr]
+ if rschema.final:
+ if attr == 'eid':
+ eid_col = i
else:
- rschema = eschema.objrels[attr]
+ attr_cols[attr] = i
+ else:
rdef = eschema.rdef(attr, role)
# only keep value if it can't be multivalued
if rdef.role_cardinality(role) in '1?':
- if rowvalues[i] is None:
- if role == 'subject':
- rql = 'Any Y WHERE X %s Y, X eid %s'
- else:
- rql = 'Any Y WHERE Y %s X, X eid %s'
- rrset = ResultSet([], rql % (attr, entity.eid))
- rrset.req = req
- else:
- rrset = self._build_entity(row, i).as_rset()
- entity.cw_set_relation_cache(attr, role, rrset)
- return entity
+ rel_cols[(attr, role)] = i
+ return eid_col, attr_cols, rel_cols
@cached
def syntax_tree(self):
--- a/schema.py Thu May 19 10:35:20 2011 +0200
+++ b/schema.py Thu May 19 10:36:26 2011 +0200
@@ -528,14 +528,15 @@
rschema = self.add_relation_type(ybo.RelationType('identity'))
rschema.final = False
+ etype_name_re = r'[A-Z][A-Za-z0-9]*[a-z]+[0-9]*$'
def add_entity_type(self, edef):
edef.name = edef.name.encode()
edef.name = bw_normalize_etype(edef.name)
- if not re.match(r'[A-Z][A-Za-z0-9]*[a-z]+[0-9]*$', edef.name):
+ if not re.match(self.etype_name_re, edef.name):
raise BadSchemaDefinition(
- '%r is not a valid name for an entity type. It should start '
- 'with an upper cased letter and be followed by at least a '
- 'lower cased letter' % edef.name)
+ '%r is not a valid name for an entity type. It should match '
+ 'the following regular expresion: %r' % (edef.name,
+ self.etype_name_re))
eschema = super(CubicWebSchema, self).add_entity_type(edef)
if not eschema.final:
# automatically add the eid relation to non final entity types
--- a/server/hook.py Thu May 19 10:35:20 2011 +0200
+++ b/server/hook.py Thu May 19 10:36:26 2011 +0200
@@ -248,7 +248,7 @@
from logging import getLogger
from itertools import chain
-from logilab.common.decorators import classproperty
+from logilab.common.decorators import classproperty, cached
from logilab.common.deprecation import deprecated, class_renamed
from logilab.common.logging_ext import set_log_methods
@@ -257,7 +257,7 @@
from cubicweb.cwvreg import CWRegistry, VRegistry
from cubicweb.selectors import (objectify_selector, lltrace, ExpectedValueSelector,
is_instance)
-from cubicweb.appobject import AppObject
+from cubicweb.appobject import AppObject, NotSelector, OrSelector
from cubicweb.server.session import security_enabled
ENTITIES_HOOKS = set(('before_add_entity', 'after_add_entity',
@@ -318,15 +318,83 @@
else:
entities = []
eids_from_to = []
+ pruned = self.get_pruned_hooks(session, event,
+ entities, eids_from_to, kwargs)
# by default, hooks are executed with security turned off
with security_enabled(session, read=False):
for _kwargs in _iter_kwargs(entities, eids_from_to, kwargs):
- hooks = sorted(self.possible_objects(session, **_kwargs),
+ hooks = sorted(self.filtered_possible_objects(pruned, session, **_kwargs),
key=lambda x: x.order)
with security_enabled(session, write=False):
for hook in hooks:
- #print hook.category, hook.__regid__
- hook()
+ hook()
+
+ def get_pruned_hooks(self, session, event, entities, eids_from_to, kwargs):
+ """return a set of hooks that should not be considered by filtered_possible objects
+
+ the idea is to make a first pass over all the hooks in the
+ registry and to mark put some of them in a pruned list. The
+ pruned hooks are the one which:
+
+ * are disabled at the session level
+ * have a match_rtype or an is_instance selector which does not
+ match the rtype / etype of the relations / entities for
+ which we are calling the hooks. This works because the
+ repository calls the hooks grouped by rtype or by etype when
+ using the entities or eids_to_from keyword arguments
+
+ Only hooks with a simple selector or an AndSelector of simple
+ selectors are considered for disabling.
+
+ """
+ if 'entity' in kwargs:
+ entities = [kwargs['entity']]
+ if len(entities):
+ look_for_selector = is_instance
+ etype = entities[0].__regid__
+ elif 'rtype' in kwargs:
+ look_for_selector = match_rtype
+ etype = None
+ else: # nothing to prune, how did we get there ???
+ return set()
+ cache_key = (event, kwargs.get('rtype'), etype)
+ pruned = session.pruned_hooks_cache.get(cache_key)
+ if pruned is not None:
+ return pruned
+ pruned = set()
+ session.pruned_hooks_cache[cache_key] = pruned
+ if look_for_selector is not None:
+ for id, hooks in self.iteritems():
+ for hook in hooks:
+ enabled_cat, main_filter = hook.filterable_selectors()
+ if enabled_cat is not None:
+ if not enabled_cat(hook, session):
+ pruned.add(hook)
+ continue
+ if main_filter is not None:
+ if isinstance(main_filter, match_rtype) and \
+ (main_filter.frometypes is not None or \
+ main_filter.toetypes is not None):
+ continue
+ first_kwargs = _iter_kwargs(entities, eids_from_to, kwargs).next()
+ if not main_filter(hook, session, **first_kwargs):
+ pruned.add(hook)
+ return pruned
+
+
+ def filtered_possible_objects(self, pruned, *args, **kwargs):
+ for appobjects in self.itervalues():
+ if pruned:
+ filtered_objects = [obj for obj in appobjects if obj not in pruned]
+ if not filtered_objects:
+ continue
+ else:
+ filtered_objects = appobjects
+ obj = self._select_best(filtered_objects,
+ *args, **kwargs)
+ if obj is None:
+ continue
+ yield obj
class HooksManager(object):
def __init__(self, vreg):
@@ -464,6 +532,15 @@
# stop pylint from complaining about missing attributes in Hooks classes
eidfrom = eidto = entity = rtype = None
+ @classmethod
+ @cached
+ def filterable_selectors(cls):
+ search = cls.__select__.search_selector
+ if search((NotSelector, OrSelector)):
+ return None, None
+ enabled_cat = search(enabled_category)
+ main_filter = search((is_instance, match_rtype))
+ return enabled_cat, main_filter
@classmethod
def check_events(cls):
--- a/server/serverconfig.py Thu May 19 10:35:20 2011 +0200
+++ b/server/serverconfig.py Thu May 19 10:36:26 2011 +0200
@@ -255,7 +255,7 @@
# configuration file (#16102)
@cached
def read_sources_file(self):
- return read_config(self.sources_file())
+ return read_config(self.sources_file(), raise_if_unreadable=True)
def sources(self):
"""return a dictionnaries containing sources definitions indexed by
--- a/server/session.py Thu May 19 10:35:20 2011 +0200
+++ b/server/session.py Thu May 19 10:36:26 2011 +0200
@@ -98,21 +98,13 @@
self.categories = categories
def __enter__(self):
- self.oldmode = self.session.set_hooks_mode(self.mode)
- if self.mode is self.session.HOOKS_DENY_ALL:
- self.changes = self.session.enable_hook_categories(*self.categories)
- else:
- self.changes = self.session.disable_hook_categories(*self.categories)
+ self.oldmode, self.changes = self.session.init_hooks_mode_categories(
+ self.mode, self.categories)
def __exit__(self, exctype, exc, traceback):
- if self.changes:
- if self.mode is self.session.HOOKS_DENY_ALL:
- self.session.disable_hook_categories(*self.changes)
- else:
- self.session.enable_hook_categories(*self.changes)
- self.session.set_hooks_mode(self.oldmode)
+ self.session.reset_hooks_mode_categories(self.oldmode, self.mode, self.changes)
-INDENT = ''
+
class security_enabled(object):
"""context manager to control security w/ session.execute, since by
default security is disabled on queries executed on the repository
@@ -124,29 +116,18 @@
self.write = write
def __enter__(self):
-# global INDENT
- if self.read is not None:
- self.oldread = self.session.set_read_security(self.read)
-# print INDENT + 'read', self.read, self.oldread
- if self.write is not None:
- self.oldwrite = self.session.set_write_security(self.write)
-# print INDENT + 'write', self.write, self.oldwrite
-# INDENT += ' '
+ self.oldread, self.oldwrite = self.session.init_security(
+ self.read, self.write)
def __exit__(self, exctype, exc, traceback):
-# global INDENT
-# INDENT = INDENT[:-2]
- if self.read is not None:
- self.session.set_read_security(self.oldread)
-# print INDENT + 'reset read to', self.oldread
- if self.write is not None:
- self.session.set_write_security(self.oldwrite)
-# print INDENT + 'reset write to', self.oldwrite
+ self.session.reset_security(self.oldread, self.oldwrite)
class TransactionData(object):
def __init__(self, txid):
self.transactionid = txid
+ self.ctx_count = 0
+
class Session(RequestSessionBase):
"""tie session id, user, connections pool and other session data all
@@ -210,6 +191,9 @@
session = Session(user, self.repo)
threaddata = session._threaddata
threaddata.pool = self.pool
+ # we attributed a pool, need to update ctx_count else it will be freed
+ # while undesired
+ threaddata.ctx_count = 1
# share pending_operations, else operation added in the hi-jacked
# session such as SendMailOp won't ever be processed
threaddata.pending_operations = self.pending_operations
@@ -234,7 +218,7 @@
def add_relations(self, relations):
'''set many relation using a shortcut similar to the one in add_relation
-
+
relations is a list of 2-uples, the first element of each
2-uple is the rtype, and the second is a list of (fromeid,
toeid) tuples
@@ -406,6 +390,29 @@
DEFAULT_SECURITY = object() # evaluated to true by design
+ def init_security(self, read, write):
+ if read is None:
+ oldread = None
+ else:
+ oldread = self.set_read_security(read)
+ if write is None:
+ oldwrite = None
+ else:
+ oldwrite = self.set_write_security(write)
+ self._threaddata.ctx_count += 1
+ return oldread, oldwrite
+
+ def reset_security(self, read, write):
+ txstore = self._threaddata
+ txstore.ctx_count -= 1
+ if txstore.ctx_count == 0:
+ self._clear_thread_storage(txstore)
+ else:
+ if read is not None:
+ self.set_read_security(read)
+ if write is not None:
+ self.set_write_security(write)
+
@property
def read_security(self):
"""return a boolean telling if read security is activated or not"""
@@ -501,6 +508,28 @@
self._threaddata.hooks_mode = mode
return oldmode
+ def init_hooks_mode_categories(self, mode, categories):
+ oldmode = self.set_hooks_mode(mode)
+ if mode is self.HOOKS_DENY_ALL:
+ changes = self.enable_hook_categories(*categories)
+ else:
+ changes = self.disable_hook_categories(*categories)
+ self._threaddata.ctx_count += 1
+ return oldmode, changes
+
+ def reset_hooks_mode_categories(self, oldmode, mode, categories):
+ txstore = self._threaddata
+ txstore.ctx_count -= 1
+ if txstore.ctx_count == 0:
+ self._clear_thread_storage(txstore)
+ else:
+ if categories:
+ if mode is self.HOOKS_DENY_ALL:
+ return self.disable_hook_categories(*categories)
+ else:
+ return self.enable_hook_categories(*categories)
+ self.set_hooks_mode(oldmode)
+
@property
def disabled_hook_categories(self):
try:
@@ -524,17 +553,18 @@
- on HOOKS_ALLOW_ALL mode, ensure those categories are disabled
"""
changes = set()
+ self.pruned_hooks_cache.clear()
if self.hooks_mode is self.HOOKS_DENY_ALL:
- enablecats = self.enabled_hook_categories
+ enabledcats = self.enabled_hook_categories
for category in categories:
- if category in enablecats:
- enablecats.remove(category)
+ if category in enabledcats:
+ enabledcats.remove(category)
changes.add(category)
else:
- disablecats = self.disabled_hook_categories
+ disabledcats = self.disabled_hook_categories
for category in categories:
- if category not in disablecats:
- disablecats.add(category)
+ if category not in disabledcats:
+ disabledcats.add(category)
changes.add(category)
return tuple(changes)
@@ -545,17 +575,18 @@
- on HOOKS_ALLOW_ALL mode, ensure those categories are not disabled
"""
changes = set()
+ self.pruned_hooks_cache.clear()
if self.hooks_mode is self.HOOKS_DENY_ALL:
- enablecats = self.enabled_hook_categories
+ enabledcats = self.enabled_hook_categories
for category in categories:
- if category not in enablecats:
- enablecats.add(category)
+ if category not in enabledcats:
+ enabledcats.add(category)
changes.add(category)
else:
- disablecats = self.disabled_hook_categories
+ disabledcats = self.disabled_hook_categories
for category in categories:
- if category in self.disabled_hook_categories:
- disablecats.remove(category)
+ if category in disabledcats:
+ disabledcats.remove(category)
changes.add(category)
return tuple(changes)
@@ -626,6 +657,7 @@
if self.pool is None:
# get pool first to avoid race-condition
self._threaddata.pool = pool = self.repo._get_pool()
+ self._threaddata.ctx_count += 1
try:
pool.pool_set()
except:
@@ -660,6 +692,7 @@
# even in read mode, we must release the current transaction
self._free_thread_pool(threading.currentThread(), pool)
del self._threaddata.pool
+ self._threaddata.ctx_count -= 1
def _touch(self):
"""update latest session usage timestamp and reset mode to read"""
@@ -759,18 +792,29 @@
pass
else:
if reset_pool:
- self._tx_data.pop(txstore.transactionid, None)
- try:
- del self.__threaddata.txdata
- except AttributeError:
- pass
+ self.reset_pool()
+ if txstore.ctx_count == 0:
+ self._clear_thread_storage(txstore)
+ else:
+ self._clear_tx_storage(txstore)
else:
- for name in ('commit_state', 'transaction_data',
- 'pending_operations', '_rewriter'):
- try:
- delattr(txstore, name)
- except AttributeError:
- continue
+ self._clear_tx_storage(txstore)
+
+ def _clear_thread_storage(self, txstore):
+ self._tx_data.pop(txstore.transactionid, None)
+ try:
+ del self.__threaddata.txdata
+ except AttributeError:
+ pass
+
+ def _clear_tx_storage(self, txstore):
+ for name in ('commit_state', 'transaction_data',
+ 'pending_operations', '_rewriter',
+ 'pruned_hooks_cache'):
+ try:
+ delattr(txstore, name)
+ except AttributeError:
+ continue
def commit(self, reset_pool=True):
"""commit the current session's transaction"""
@@ -917,6 +961,14 @@
self._threaddata.pending_operations = []
return self._threaddata.pending_operations
+ @property
+ def pruned_hooks_cache(self):
+ try:
+ return self._threaddata.pruned_hooks_cache
+ except AttributeError:
+ self._threaddata.pruned_hooks_cache = {}
+ return self._threaddata.pruned_hooks_cache
+
def add_operation(self, operation, index=None):
"""add an observer"""
assert self.commit_state != 'commit'
--- a/server/sources/datafeed.py Thu May 19 10:35:20 2011 +0200
+++ b/server/sources/datafeed.py Thu May 19 10:36:26 2011 +0200
@@ -18,8 +18,14 @@
"""datafeed sources: copy data from an external data stream into the system
database
"""
+
+import urllib2
+import StringIO
from datetime import datetime, timedelta
from base64 import b64decode
+from cookielib import CookieJar
+
+from lxml import etree
from cubicweb import RegistryNotFound, ObjectNotFound, ValidationError
from cubicweb.server.sources import AbstractSource
@@ -132,7 +138,7 @@
self.info('pulling data for source %s', self.uri)
for url in self.urls:
try:
- if parser.process(url):
+ if parser.process(url, raise_on_error):
error = True
except IOError, exc:
if raise_on_error:
@@ -219,7 +225,7 @@
self.sourceuris.pop(str(uri), None)
return self._cw.entity_from_eid(eid, etype)
- def process(self, url):
+ def process(self, url, partialcommit=True):
"""main callback: process the url"""
raise NotImplementedError
@@ -237,3 +243,59 @@
def notify_updated(self, entity):
return self.stats['updated'].add(entity.eid)
+
+
+class DataFeedXMLParser(DataFeedParser):
+
+ def process(self, url, raise_on_error=False, partialcommit=True):
+ """IDataFeedParser main entry point"""
+ error = False
+ for args in self.parse(url):
+ try:
+ self.process_item(*args)
+ if partialcommit:
+ # commit+set_pool instead of commit(reset_pool=False) to let
+ # other a chance to get our pool
+ self._cw.commit()
+ self._cw.set_pool()
+ except ValidationError, exc:
+ if raise_on_error:
+ raise
+ if partialcommit:
+ self.source.error('Skipping %s because of validation error %s' % (args, exc))
+ self._cw.rollback()
+ self._cw.set_pool()
+ error = True
+ else:
+ raise
+ return error
+
+ def parse(self, url):
+ if url.startswith('http'):
+ from cubicweb.sobjects.parsers import HOST_MAPPING
+ for mappedurl in HOST_MAPPING:
+ if url.startswith(mappedurl):
+ url = url.replace(mappedurl, HOST_MAPPING[mappedurl], 1)
+ break
+ self.source.info('GET %s', url)
+ stream = _OPENER.open(url)
+ elif url.startswith('file://'):
+ stream = open(url[7:])
+ else:
+ stream = StringIO.StringIO(url)
+ return self.parse_etree(etree.parse(stream).getroot())
+
+ def parse_etree(self, document):
+ return [(document,)]
+
+ def process_item(self, *args):
+ raise NotImplementedError
+
+# use a cookie enabled opener to use session cookie if any
+_OPENER = urllib2.build_opener()
+try:
+ from logilab.common import urllib2ext
+ _OPENER.add_handler(urllib2ext.HTTPGssapiAuthHandler())
+except ImportError: # python-kerberos not available
+ pass
+_OPENER.add_handler(urllib2.HTTPCookieProcessor(CookieJar()))
--- a/server/sources/pyrorql.py Thu May 19 10:35:20 2011 +0200
+++ b/server/sources/pyrorql.py Thu May 19 10:36:26 2011 +0200
@@ -437,7 +437,7 @@
cu = session.pool[self.uri]
cu.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations), kwargs)
self._query_cache.clear()
- entity.clear_all_caches()
+ entity.cw_clear_all_caches()
def delete_entity(self, session, entity):
"""delete an entity from the source"""
@@ -453,8 +453,8 @@
{'x': self.eid2extid(subject, session),
'y': self.eid2extid(object, session)})
self._query_cache.clear()
- session.entity_from_eid(subject).clear_all_caches()
- session.entity_from_eid(object).clear_all_caches()
+ session.entity_from_eid(subject).cw_clear_all_caches()
+ session.entity_from_eid(object).cw_clear_all_caches()
def delete_relation(self, session, subject, rtype, object):
"""delete a relation from the source"""
@@ -463,8 +463,8 @@
{'x': self.eid2extid(subject, session),
'y': self.eid2extid(object, session)})
self._query_cache.clear()
- session.entity_from_eid(subject).clear_all_caches()
- session.entity_from_eid(object).clear_all_caches()
+ session.entity_from_eid(subject).cw_clear_all_caches()
+ session.entity_from_eid(object).cw_clear_all_caches()
class RQL2RQL(object):
--- a/server/sources/rql2sql.py Thu May 19 10:35:20 2011 +0200
+++ b/server/sources/rql2sql.py Thu May 19 10:36:26 2011 +0200
@@ -1357,6 +1357,8 @@
operator = ' LIKE '
else:
operator = ' %s ' % operator
+ elif operator == 'REGEXP':
+ return ' %s' % self.dbhelper.sql_regexp_match_expression(rhs.accept(self))
elif (operator == '=' and isinstance(rhs, Constant)
and rhs.eval(self._args) is None):
if lhs is None:
@@ -1407,6 +1409,8 @@
if constant.type is None:
return 'NULL'
value = constant.value
+ if constant.type == 'etype':
+ return value
if constant.type == 'Int' and isinstance(constant.parent, SortTerm):
return value
if constant.type in ('Date', 'Datetime'):
--- a/server/test/unittest_datafeed.py Thu May 19 10:35:20 2011 +0200
+++ b/server/test/unittest_datafeed.py Thu May 19 10:36:26 2011 +0200
@@ -39,7 +39,7 @@
class AParser(datafeed.DataFeedParser):
__regid__ = 'testparser'
- def process(self, url):
+ def process(self, url, raise_on_error=False):
entity = self.extid2entity('http://www.cubicweb.org/', 'Card',
item={'title': u'cubicweb.org',
'content': u'the cw web site'})
--- a/server/test/unittest_hook.py Thu May 19 10:35:20 2011 +0200
+++ b/server/test/unittest_hook.py Thu May 19 10:36:26 2011 +0200
@@ -23,7 +23,7 @@
from logilab.common.testlib import TestCase, unittest_main, mock_object
-from cubicweb.devtools import TestServerConfiguration
+from cubicweb.devtools import TestServerConfiguration, fake
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.server import hook
from cubicweb.hooks import integrity, syncschema
@@ -124,10 +124,8 @@
def test_call_hook(self):
self.o.register(AddAnyHook)
dis = set()
- cw = mock_object(vreg=self.vreg,
- set_read_security=lambda *a,**k: None,
- set_write_security=lambda *a,**k: None,
- is_hook_activated=lambda x, cls: cls.category not in dis)
+ cw = fake.FakeSession()
+ cw.is_hook_activated = lambda cls: cls.category not in dis
self.assertRaises(HookCalled,
self.o.call_hooks, 'before_add_entity', cw)
dis.add('cat1')
--- a/server/test/unittest_ldapuser.py Thu May 19 10:35:20 2011 +0200
+++ b/server/test/unittest_ldapuser.py Thu May 19 10:36:26 2011 +0200
@@ -239,7 +239,7 @@
iworkflowable.fire_transition('deactivate')
try:
cnx.commit()
- adim.clear_all_caches()
+ adim.cw_clear_all_caches()
self.assertEqual(adim.in_state[0].name, 'deactivated')
trinfo = iworkflowable.latest_trinfo()
self.assertEqual(trinfo.owned_by[0].login, SYT)
--- a/server/test/unittest_querier.py Thu May 19 10:35:20 2011 +0200
+++ b/server/test/unittest_querier.py Thu May 19 10:36:26 2011 +0200
@@ -311,6 +311,14 @@
seid = self.execute('State X WHERE X name "deactivated"')[0][0]
rset = self.execute('Any U,L,S GROUPBY U,L,S WHERE X in_state S, U login L, S eid %s' % seid)
+ def test_select_groupby_funccall(self):
+ rset = self.execute('Any YEAR(CD), COUNT(X) GROUPBY YEAR(CD) WHERE X is CWUser, X creation_date CD')
+ self.assertListEqual(rset.rows, [[date.today().year, 2]])
+
+ def test_select_groupby_colnumber(self):
+ rset = self.execute('Any YEAR(CD), COUNT(X) GROUPBY 1 WHERE X is CWUser, X creation_date CD')
+ self.assertListEqual(rset.rows, [[date.today().year, 2]])
+
def test_select_complex_orderby(self):
rset1 = self.execute('Any N ORDERBY N WHERE X name N')
self.assertEqual(sorted(rset1.rows), rset1.rows)
@@ -443,6 +451,15 @@
self.assertEqual(rset.rows[0][0], result)
self.assertEqual(rset.description, [('Int',)])
+ def test_regexp_based_pattern_matching(self):
+ peid1 = self.execute("INSERT Personne X: X nom 'bidule'")[0][0]
+ peid2 = self.execute("INSERT Personne X: X nom 'cidule'")[0][0]
+ rset = self.execute('Any X WHERE X is Personne, X nom REGEXP "^b"')
+ self.assertEqual(len(rset.rows), 1, rset.rows)
+ self.assertEqual(rset.rows[0][0], peid1)
+ rset = self.execute('Any X WHERE X is Personne, X nom REGEXP "idu"')
+ self.assertEqual(len(rset.rows), 2, rset.rows)
+
def test_select_aggregat_count(self):
rset = self.execute('Any COUNT(X)')
self.assertEqual(len(rset.rows), 1)
--- a/server/test/unittest_repository.py Thu May 19 10:35:20 2011 +0200
+++ b/server/test/unittest_repository.py Thu May 19 10:36:26 2011 +0200
@@ -24,6 +24,7 @@
import sys
import threading
import time
+import logging
from copy import deepcopy
from datetime import datetime
@@ -704,13 +705,18 @@
class PerformanceTest(CubicWebTC):
- def setup_database(self):
- import logging
+ def setUp(self):
+ super(PerformanceTest, self).setUp()
logger = logging.getLogger('cubicweb.session')
#logger.handlers = [logging.StreamHandler(sys.stdout)]
logger.setLevel(logging.INFO)
self.info = logger.info
+ def tearDown(self):
+ super(PerformanceTest, self).tearDown()
+ logger = logging.getLogger('cubicweb.session')
+ logger.setLevel(logging.CRITICAL)
+
def test_composite_deletion(self):
req = self.request()
personnes = []
--- a/server/test/unittest_rql2sql.py Thu May 19 10:35:20 2011 +0200
+++ b/server/test/unittest_rql2sql.py Thu May 19 10:36:26 2011 +0200
@@ -1340,6 +1340,18 @@
'''SELECT SUBSTR(_P.cw_nom, 1, 1)
FROM cw_Personne AS _P''')
+ def test_cast(self):
+ self._check("Any CAST(String, P) WHERE P is Personne",
+ '''SELECT CAST(_P.cw_eid AS text)
+FROM cw_Personne AS _P''')
+
+ def test_regexp(self):
+ self._check("Any X WHERE X login REGEXP '[0-9].*'",
+ '''SELECT _X.cw_eid
+FROM cw_CWUser AS _X
+WHERE _X.cw_login ~ [0-9].*
+''')
+
def test_parser_parse(self):
for t in self._parse(PARSER):
yield t
@@ -1639,6 +1651,9 @@
for t in self._parse(HAS_TEXT_LG_INDEXER):
yield t
+ def test_regexp(self):
+ self.skipTest('regexp-based pattern matching not implemented in sqlserver')
+
def test_or_having_fake_terms(self):
self._check('Any X WHERE X is CWUser, X creation_date D HAVING YEAR(D) = "2010" OR D = NULL',
'''SELECT _X.cw_eid
@@ -1735,6 +1750,10 @@
for t in self._parse(WITH_LIMIT):# + ADVANCED_WITH_LIMIT_OR_ORDERBY):
yield t
+ def test_cast(self):
+ self._check("Any CAST(String, P) WHERE P is Personne",
+ '''SELECT CAST(_P.cw_eid AS nvarchar(max))
+FROM cw_Personne AS _P''')
class SqliteSQLGeneratorTC(PostgresSQLGeneratorTC):
@@ -1748,6 +1767,14 @@
'''SELECT MONTH(_P.cw_creation_date)
FROM cw_Personne AS _P''')
+ def test_regexp(self):
+ self._check("Any X WHERE X login REGEXP '[0-9].*'",
+ '''SELECT _X.cw_eid
+FROM cw_CWUser AS _X
+WHERE _X.cw_login REGEXP [0-9].*
+''')
+
+
def test_union(self):
for t in self._parse((
('(Any N ORDERBY 1 WHERE X name N, X is State)'
@@ -1888,6 +1915,18 @@
'''SELECT EXTRACT(MONTH from _P.cw_creation_date)
FROM cw_Personne AS _P''')
+ def test_cast(self):
+ self._check("Any CAST(String, P) WHERE P is Personne",
+ '''SELECT CAST(_P.cw_eid AS mediumtext)
+FROM cw_Personne AS _P''')
+
+ def test_regexp(self):
+ self._check("Any X WHERE X login REGEXP '[0-9].*'",
+ '''SELECT _X.cw_eid
+FROM cw_CWUser AS _X
+WHERE _X.cw_login REGEXP [0-9].*
+''')
+
def test_from_clause_needed(self):
queries = [("Any 1 WHERE EXISTS(T is CWGroup, T name 'managers')",
'''SELECT 1
--- a/server/test/unittest_session.py Thu May 19 10:35:20 2011 +0200
+++ b/server/test/unittest_session.py Thu May 19 10:36:26 2011 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -15,13 +15,12 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""
+from __future__ import with_statement
-"""
from logilab.common.testlib import TestCase, unittest_main, mock_object
from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.server.session import _make_description
+from cubicweb.server.session import _make_description, hooks_control
class Variable:
def __init__(self, name):
@@ -46,11 +45,38 @@
self.assertEqual(_make_description((Function('max', 'A'), Variable('B')), {}, solution),
['Int','CWUser'])
+
class InternalSessionTC(CubicWebTC):
def test_dbapi_query(self):
session = self.repo.internal_session()
self.assertFalse(session.running_dbapi_query)
session.close()
+
+class SessionTC(CubicWebTC):
+
+ def test_hooks_control(self):
+ session = self.session
+ self.assertEqual(session.hooks_mode, session.HOOKS_ALLOW_ALL)
+ self.assertEqual(session.disabled_hook_categories, set())
+ self.assertEqual(session.enabled_hook_categories, set())
+ self.assertEqual(len(session._tx_data), 1)
+ with hooks_control(session, session.HOOKS_DENY_ALL, 'metadata'):
+ self.assertEqual(session.hooks_mode, session.HOOKS_DENY_ALL)
+ self.assertEqual(session.disabled_hook_categories, set())
+ self.assertEqual(session.enabled_hook_categories, set(('metadata',)))
+ session.commit()
+ self.assertEqual(session.hooks_mode, session.HOOKS_DENY_ALL)
+ self.assertEqual(session.disabled_hook_categories, set())
+ self.assertEqual(session.enabled_hook_categories, set(('metadata',)))
+ session.rollback()
+ self.assertEqual(session.hooks_mode, session.HOOKS_DENY_ALL)
+ self.assertEqual(session.disabled_hook_categories, set())
+ self.assertEqual(session.enabled_hook_categories, set(('metadata',)))
+ # leaving context manager with no transaction running should reset the
+ # transaction local storage (and associated pool)
+ self.assertEqual(session._tx_data, {})
+ self.assertEqual(session.pool, None)
+
if __name__ == '__main__':
unittest_main()
--- a/server/test/unittest_undo.py Thu May 19 10:35:20 2011 +0200
+++ b/server/test/unittest_undo.py Thu May 19 10:36:26 2011 +0200
@@ -150,8 +150,8 @@
txuuid = self.commit()
actions = self.cnx.transaction_info(txuuid).actions_list()
self.assertEqual(len(actions), 1)
- toto.clear_all_caches()
- e.clear_all_caches()
+ toto.cw_clear_all_caches()
+ e.cw_clear_all_caches()
errors = self.cnx.undo_transaction(txuuid)
undotxuuid = self.commit()
self.assertEqual(undotxuuid, None) # undo not undoable
@@ -192,7 +192,7 @@
self.commit()
errors = self.cnx.undo_transaction(txuuid)
self.commit()
- p.clear_all_caches()
+ p.cw_clear_all_caches()
self.assertEqual(p.fiche[0].eid, c2.eid)
self.assertEqual(len(errors), 1)
self.assertEqual(errors[0],
--- a/sobjects/parsers.py Thu May 19 10:35:20 2011 +0200
+++ b/sobjects/parsers.py Thu May 19 10:36:26 2011 +0200
@@ -31,14 +31,9 @@
"""
-import urllib2
-import StringIO
import os.path as osp
-from cookielib import CookieJar
from datetime import datetime, timedelta
-from lxml import etree
-
from logilab.common.date import todate, totime
from logilab.common.textutils import splitstrip, text_to_dict
@@ -72,15 +67,6 @@
return time(seconds=int(ustr))
DEFAULT_CONVERTERS['Interval'] = convert_interval
-# use a cookie enabled opener to use session cookie if any
-_OPENER = urllib2.build_opener()
-try:
- from logilab.common import urllib2ext
- _OPENER.add_handler(urllib2ext.HTTPGssapiAuthHandler())
-except ImportError: # python-kerberos not available
- pass
-_OPENER.add_handler(urllib2.HTTPCookieProcessor(CookieJar()))
-
def extract_typed_attrs(eschema, stringdict, converters=DEFAULT_CONVERTERS):
typeddict = {}
for rschema in eschema.subject_relations():
@@ -138,7 +124,7 @@
raise ValidationError(eid, {rn('options', 'subject'): msg})
-class CWEntityXMLParser(datafeed.DataFeedParser):
+class CWEntityXMLParser(datafeed.DataFeedXMLParser):
"""datafeed parser for the 'xml' entity view"""
__regid__ = 'cw.entityxml'
@@ -147,6 +133,8 @@
'link-or-create': _check_linkattr_option,
'link': _check_linkattr_option,
}
+ parse_etree = staticmethod(_parse_entity_etree)
+
def __init__(self, *args, **kwargs):
super(CWEntityXMLParser, self).__init__(*args, **kwargs)
@@ -208,42 +196,8 @@
# import handling ##########################################################
- def process(self, url, partialcommit=True):
- """IDataFeedParser main entry point"""
- # XXX suppression support according to source configuration. If set, get
- # all cwuri of entities from this source, and compare with newly
- # imported ones
- error = False
- for item, rels in self.parse(url):
- cwuri = item['cwuri']
- try:
- self.process_item(item, rels)
- if partialcommit:
- # commit+set_pool instead of commit(reset_pool=False) to let
- # other a chance to get our pool
- self._cw.commit()
- self._cw.set_pool()
- except ValidationError, exc:
- if partialcommit:
- self.source.error('Skipping %s because of validation error %s' % (cwuri, exc))
- self._cw.rollback()
- self._cw.set_pool()
- error = True
- else:
- raise
- return error
-
- def parse(self, url):
- if not url.startswith('http'):
- stream = StringIO.StringIO(url)
- else:
- for mappedurl in HOST_MAPPING:
- if url.startswith(mappedurl):
- url = url.replace(mappedurl, HOST_MAPPING[mappedurl], 1)
- break
- self.source.info('GET %s', url)
- stream = _OPENER.open(url)
- return _parse_entity_etree(etree.parse(stream).getroot())
+ # XXX suppression support according to source configuration. If set, get all
+ # cwuri of entities from this source, and compare with newly imported ones
def process_item(self, item, rels):
entity = self.extid2entity(str(item.pop('cwuri')), item.pop('cwtype'),
--- a/test/unittest_entity.py Thu May 19 10:35:20 2011 +0200
+++ b/test/unittest_entity.py Thu May 19 10:36:26 2011 +0200
@@ -560,7 +560,7 @@
self.assertEqual(person.rest_path(), 'personne/doe')
# ambiguity test
person2 = req.create_entity('Personne', prenom=u'remi', nom=u'doe')
- person.clear_all_caches()
+ person.cw_clear_all_caches()
self.assertEqual(person.rest_path(), 'personne/eid/%s' % person.eid)
self.assertEqual(person2.rest_path(), 'personne/eid/%s' % person2.eid)
# unique attr with None value (wikiid in this case)
--- a/test/unittest_selectors.py Thu May 19 10:35:20 2011 +0200
+++ b/test/unittest_selectors.py Thu May 19 10:36:26 2011 +0200
@@ -102,6 +102,10 @@
self.assertIs(csel.search_selector(is_instance), sel)
csel = AndSelector(Selector(), sel)
self.assertIs(csel.search_selector(is_instance), sel)
+ self.assertIs(csel.search_selector((AndSelector, OrSelector)), csel)
+ self.assertIs(csel.search_selector((OrSelector, AndSelector)), csel)
+ self.assertIs(csel.search_selector((is_instance, score_entity)), sel)
+ self.assertIs(csel.search_selector((score_entity, is_instance)), sel)
def test_inplace_and(self):
selector = _1_()
@@ -193,7 +197,7 @@
class WorkflowSelectorTC(CubicWebTC):
def _commit(self):
self.commit()
- self.wf_entity.clear_all_caches()
+ self.wf_entity.cw_clear_all_caches()
def setup_database(self):
wf = self.shell().add_workflow("wf_test", 'StateFull', default=True)
--- a/test/unittest_utils.py Thu May 19 10:35:20 2011 +0200
+++ b/test/unittest_utils.py Thu May 19 10:36:26 2011 +0200
@@ -22,8 +22,8 @@
import datetime
from logilab.common.testlib import TestCase, unittest_main
-
-from cubicweb.utils import make_uid, UStringIO, SizeConstrainedList, RepeatList
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.utils import make_uid, UStringIO, SizeConstrainedList, RepeatList, HTMLHead
from cubicweb.entity import Entity
try:
@@ -155,6 +155,102 @@
def test_encoding_unknown_stuff(self):
self.assertEqual(self.encode(TestCase), 'null')
+class HTMLHeadTC(CubicWebTC):
+ def test_concat_urls(self):
+ base_url = u'http://test.fr/data/'
+ head = HTMLHead(base_url)
+ urls = [base_url + u'bob1.js',
+ base_url + u'bob2.js',
+ base_url + u'bob3.js']
+ result = head.concat_urls(urls)
+ expected = u'http://test.fr/data/??bob1.js,bob2.js,bob3.js'
+ self.assertEqual(result, expected)
+
+ def test_group_urls(self):
+ base_url = u'http://test.fr/data/'
+ head = HTMLHead(base_url)
+ urls_spec = [(base_url + u'bob0.js', None),
+ (base_url + u'bob1.js', None),
+ (u'http://ext.com/bob2.js', None),
+ (u'http://ext.com/bob3.js', None),
+ (base_url + u'bob4.css', 'all'),
+ (base_url + u'bob5.css', 'all'),
+ (base_url + u'bob6.css', 'print'),
+ (base_url + u'bob7.css', 'print'),
+ (base_url + u'bob8.css', ('all', u'[if IE 8]')),
+ (base_url + u'bob9.css', ('print', u'[if IE 8]'))
+ ]
+ result = head.group_urls(urls_spec)
+ expected = [(base_url + u'??bob0.js,bob1.js', None),
+ (u'http://ext.com/bob2.js', None),
+ (u'http://ext.com/bob3.js', None),
+ (base_url + u'??bob4.css,bob5.css', 'all'),
+ (base_url + u'??bob6.css,bob7.css', 'print'),
+ (base_url + u'bob8.css', ('all', u'[if IE 8]')),
+ (base_url + u'bob9.css', ('print', u'[if IE 8]'))
+ ]
+ self.assertEqual(list(result), expected)
+
+ def test_getvalue_with_concat(self):
+ base_url = u'http://test.fr/data/'
+ head = HTMLHead(base_url)
+ head.add_js(base_url + u'bob0.js')
+ head.add_js(base_url + u'bob1.js')
+ head.add_js(u'http://ext.com/bob2.js')
+ head.add_js(u'http://ext.com/bob3.js')
+ head.add_css(base_url + u'bob4.css')
+ head.add_css(base_url + u'bob5.css')
+ head.add_css(base_url + u'bob6.css', 'print')
+ head.add_css(base_url + u'bob7.css', 'print')
+ head.add_ie_css(base_url + u'bob8.css')
+ head.add_ie_css(base_url + u'bob9.css', 'print', u'[if lt IE 7]')
+ result = head.getvalue()
+ expected = u"""<head>
+<link rel="stylesheet" type="text/css" media="all" href="http://test.fr/data/??bob4.css,bob5.css"/>
+<link rel="stylesheet" type="text/css" media="print" href="http://test.fr/data/??bob6.css,bob7.css"/>
+<!--[if lt IE 8]>
+<link rel="stylesheet" type="text/css" media="all" href="http://test.fr/data/bob8.css"/>
+<!--[if lt IE 7]>
+<link rel="stylesheet" type="text/css" media="print" href="http://test.fr/data/bob9.css"/>
+<![endif]-->
+<script type="text/javascript" src="http://test.fr/data/??bob0.js,bob1.js"></script>
+<script type="text/javascript" src="http://ext.com/bob2.js"></script>
+<script type="text/javascript" src="http://ext.com/bob3.js"></script>
+</head>
+"""
+ self.assertEqual(result, expected)
+
+ def test_getvalue_without_concat(self):
+ base_url = u'http://test.fr/data/'
+ head = HTMLHead()
+ head.add_js(base_url + u'bob0.js')
+ head.add_js(base_url + u'bob1.js')
+ head.add_js(u'http://ext.com/bob2.js')
+ head.add_js(u'http://ext.com/bob3.js')
+ head.add_css(base_url + u'bob4.css')
+ head.add_css(base_url + u'bob5.css')
+ head.add_css(base_url + u'bob6.css', 'print')
+ head.add_css(base_url + u'bob7.css', 'print')
+ head.add_ie_css(base_url + u'bob8.css')
+ head.add_ie_css(base_url + u'bob9.css', 'print', u'[if lt IE 7]')
+ result = head.getvalue()
+ expected = u"""<head>
+<link rel="stylesheet" type="text/css" media="all" href="http://test.fr/data/bob4.css"/>
+<link rel="stylesheet" type="text/css" media="all" href="http://test.fr/data/bob5.css"/>
+<link rel="stylesheet" type="text/css" media="print" href="http://test.fr/data/bob6.css"/>
+<link rel="stylesheet" type="text/css" media="print" href="http://test.fr/data/bob7.css"/>
+<!--[if lt IE 8]>
+<link rel="stylesheet" type="text/css" media="all" href="http://test.fr/data/bob8.css"/>
+<!--[if lt IE 7]>
+<link rel="stylesheet" type="text/css" media="print" href="http://test.fr/data/bob9.css"/>
+<![endif]-->
+<script type="text/javascript" src="http://test.fr/data/bob0.js"></script>
+<script type="text/javascript" src="http://test.fr/data/bob1.js"></script>
+<script type="text/javascript" src="http://ext.com/bob2.js"></script>
+<script type="text/javascript" src="http://ext.com/bob3.js"></script>
+</head>
+"""
+ self.assertEqual(result, expected)
if __name__ == '__main__':
unittest_main()
--- a/toolsutils.py Thu May 19 10:35:20 2011 +0200
+++ b/toolsutils.py Thu May 19 10:36:26 2011 +0200
@@ -159,15 +159,11 @@
print '-> set permissions to 0600 for %s' % filepath
chmod(filepath, 0600)
-def read_config(config_file):
- """read the instance configuration from a file and return it as a
- dictionnary
-
- :type config_file: str
- :param config_file: path to the configuration file
-
- :rtype: dict
- :return: a dictionary with specified values associated to option names
+def read_config(config_file, raise_if_unreadable=False):
+ """read some simple configuration from `config_file` and return it as a
+ dictionary. If `raise_if_unreadable` is false (the default), an empty
+ dictionary will be returned if the file is inexistant or unreadable, else
+ :exc:`ExecutionError` will be raised.
"""
from logilab.common.fileutils import lines
config = current = {}
@@ -190,8 +186,12 @@
value = value.strip()
current[option] = value or None
except IOError, ex:
- warning('missing or non readable configuration file %s (%s)',
- config_file, ex)
+ if raise_if_unreadable:
+ raise ExecutionError('%s. Are you logged with the correct user '
+ 'to use this instance?' % ex)
+ else:
+ warning('missing or non readable configuration file %s (%s)',
+ config_file, ex)
return config
--- a/utils.py Thu May 19 10:35:20 2011 +0200
+++ b/utils.py Thu May 19 10:36:26 2011 +0200
@@ -240,7 +240,7 @@
xhtml_safe_script_opening = u'<script type="text/javascript"><!--//--><![CDATA[//><!--\n'
xhtml_safe_script_closing = u'\n//--><!]]></script>'
- def __init__(self):
+ def __init__(self, datadir_url=None):
super(HTMLHead, self).__init__()
self.jsvars = []
self.jsfiles = []
@@ -248,6 +248,7 @@
self.ie_cssfiles = []
self.post_inlined_scripts = []
self.pagedata_unload = False
+ self.datadir_url = datadir_url
def add_raw(self, rawheader):
@@ -284,7 +285,7 @@
if jsfile not in self.jsfiles:
self.jsfiles.append(jsfile)
- def add_css(self, cssfile, media):
+ def add_css(self, cssfile, media='all'):
"""adds `cssfile` to the list of javascripts used in the webpage
This function checks if the file has already been added
@@ -304,6 +305,45 @@
self.post_inlined_scripts.append(self.js_unload_code)
self.pagedata_unload = True
+ def concat_urls(self, urls):
+ """concatenates urls into one url usable by Apache mod_concat
+
+ This method returns the url without modifying it if there is only
+ one element in the list
+ :param urls: list of local urls/filenames to concatenate
+ """
+ if len(urls) == 1:
+ return urls[0]
+ len_prefix = len(self.datadir_url)
+ concated = u','.join(url[len_prefix:] for url in urls)
+ return (u'%s??%s' % (self.datadir_url, concated))
+
+ def group_urls(self, urls_spec):
+ """parses urls_spec in order to generate concatenated urls
+ for js and css includes
+
+ This method checks if the file is local and if it shares options
+ with direct neighbors
+ :param urls_spec: entire list of urls/filenames to inspect
+ """
+ concatable = []
+ prev_islocal = False
+ prev_key = None
+ for url, key in urls_spec:
+ islocal = url.startswith(self.datadir_url)
+ if concatable and (islocal != prev_islocal or key != prev_key):
+ yield (self.concat_urls(concatable), prev_key)
+ del concatable[:]
+ if not islocal:
+ yield (url, key)
+ else:
+ concatable.append(url)
+ prev_islocal = islocal
+ prev_key = key
+ if concatable:
+ yield (self.concat_urls(concatable), prev_key)
+
+
def getvalue(self, skiphead=False):
"""reimplement getvalue to provide a consistent (and somewhat browser
optimzed cf. http://stevesouders.com/cuzillion) order in external
@@ -321,18 +361,20 @@
w(vardecl + u'\n')
w(self.xhtml_safe_script_closing)
# 2/ css files
- for cssfile, media in self.cssfiles:
+ for cssfile, media in (self.group_urls(self.cssfiles) if self.datadir_url else self.cssfiles):
w(u'<link rel="stylesheet" type="text/css" media="%s" href="%s"/>\n' %
(media, xml_escape(cssfile)))
# 3/ ie css if necessary
if self.ie_cssfiles:
- for cssfile, media, iespec in self.ie_cssfiles:
+ ie_cssfiles = ((x, (y, z)) for x, y, z in self.ie_cssfiles)
+ for cssfile, (media, iespec) in (self.group_urls(ie_cssfiles) if self.datadir_url else ie_cssfiles):
w(u'<!--%s>\n' % iespec)
w(u'<link rel="stylesheet" type="text/css" media="%s" href="%s"/>\n' %
(media, xml_escape(cssfile)))
w(u'<![endif]--> \n')
# 4/ js files
- for jsfile in self.jsfiles:
+ jsfiles = ((x, None) for x in self.jsfiles)
+ for jsfile, media in self.group_urls(jsfiles) if self.datadir_url else jsfiles:
w(u'<script type="text/javascript" src="%s"></script>\n' %
xml_escape(jsfile))
# 5/ post inlined scripts (i.e. scripts depending on other JS files)
--- a/vregistry.py Thu May 19 10:35:20 2011 +0200
+++ b/vregistry.py Thu May 19 10:36:26 2011 +0200
@@ -184,7 +184,10 @@
raise :exc:`NoSelectableObject` if not object apply
"""
- return self._select_best(self[__oid], *args, **kwargs)
+ obj = self._select_best(self[__oid], *args, **kwargs)
+ if obj is None:
+ raise NoSelectableObject(args, kwargs, self[__oid] )
+ return obj
def select_or_none(self, __oid, *args, **kwargs):
"""return the most specific object among those with the given oid
@@ -202,16 +205,18 @@
context
"""
for appobjects in self.itervalues():
- try:
- yield self._select_best(appobjects, *args, **kwargs)
- except NoSelectableObject:
+ obj = self._select_best(appobjects, *args, **kwargs)
+ if obj is None:
continue
+ yield obj
def _select_best(self, appobjects, *args, **kwargs):
"""return an instance of the most specific object according
to parameters
- raise `NoSelectableObject` if not object apply
+ return None if not object apply (don't raise `NoSelectableObject` since
+ it's costly when searching appobjects using `possible_objects`
+ (e.g. searching for hooks).
"""
if len(args) > 1:
warn('[3.5] only the request param can not be named when calling select*',
@@ -224,7 +229,7 @@
elif appobjectscore > 0 and appobjectscore == score:
winners.append(appobject)
if winners is None:
- raise NoSelectableObject(args, kwargs, appobjects)
+ return None
if len(winners) > 1:
# log in production environement / test, error while debugging
msg = 'select ambiguity: %s\n(args: %s, kwargs: %s)'
--- a/web/component.py Thu May 19 10:35:20 2011 +0200
+++ b/web/component.py Thu May 19 10:36:26 2011 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -57,8 +57,6 @@
page_link_templ = u'<span class="slice"><a href="%s" title="%s">%s</a></span>'
selected_page_link_templ = u'<span class="selectedSlice"><a href="%s" title="%s">%s</a></span>'
previous_page_link_templ = next_page_link_templ = page_link_templ
- no_previous_page_link = u'<<'
- no_next_page_link = u'>>'
def __init__(self, req, rset, **kwargs):
super(NavigationComponent, self).__init__(req, rset=rset, **kwargs)
@@ -131,7 +129,37 @@
return self.selected_page_link_templ % (url, content, content)
return self.page_link_templ % (url, content, content)
- def previous_link(self, path, params, content='<<', title=_('previous_results')):
+ @property
+ def prev_icon_url(self):
+ return xml_escape(self._cw.data_url('go_prev.png'))
+
+ @property
+ def next_icon_url(self):
+ return xml_escape(self._cw.data_url('go_next.png'))
+
+ @property
+ def no_previous_page_link(self):
+ return (u'<img src="%s" alt="%s" class="prevnext_nogo"/>' %
+ (self.prev_icon_url, self._cw._('there is no previous page')))
+
+ @property
+ def no_next_page_link(self):
+ return (u'<img src="%s" alt="%s" class="prevnext_nogo"/>' %
+ (self.next_icon_url, self._cw._('there is no next page')))
+
+ @property
+ def no_content_prev_link(self):
+ return (u'<img src="%s" alt="%s" class="prevnext"/>' % (
+ (self.prev_icon_url, self._cw._('no content prev link'))))
+
+ @property
+ def no_content_next_link(self):
+ return (u'<img src="%s" alt="%s" class="prevnext"/>' %
+ (self.next_icon_url, self._cw._('no content next link')))
+
+ def previous_link(self, path, params, content=None, title=_('previous_results')):
+ if not content:
+ content = self.no_content_prev_link
start = self.starting_from
if not start :
return self.no_previous_page_link
@@ -140,7 +168,9 @@
url = xml_escape(self.page_url(path, params, start, stop))
return self.previous_page_link_templ % (url, title, content)
- def next_link(self, path, params, content='>>', title=_('next_results')):
+ def next_link(self, path, params, content=None, title=_('next_results')):
+ if not content:
+ content = self.no_content_next_link
start = self.starting_from + self.page_size
if start >= self.total:
return self.no_next_page_link
--- a/web/controller.py Thu May 19 10:35:20 2011 +0200
+++ b/web/controller.py Thu May 19 10:36:26 2011 +0200
@@ -165,7 +165,7 @@
elif self._edited_entity:
# clear caches in case some attribute participating to the rest path
# has been modified
- self._edited_entity.clear_all_caches()
+ self._edited_entity.cw_clear_all_caches()
path = self._edited_entity.rest_path()
else:
path = 'view'
--- a/web/data/cubicweb.ajax.js Thu May 19 10:35:20 2011 +0200
+++ b/web/data/cubicweb.ajax.js Thu May 19 10:36:26 2011 +0200
@@ -86,6 +86,41 @@
var JSON_BASE_URL = baseuri() + 'json?';
+/**
+ * returns true if `url` is a mod_concat-like url
+ * (e.g. http://..../data??resource1.js,resource2.js)
+ */
+function _modconcatLikeUrl(url) {
+ var base = baseuri();
+ if (!base.endswith('/')) {
+ base += '/';
+ }
+ var modconcat_rgx = new RegExp('(' + base + 'data/([a-z0-9]+/)?)\\?\\?(.+)');
+ return modconcat_rgx.exec(url);
+}
+
+/**
+ * decomposes a mod_concat-like url into its corresponding list of
+ * resources' urls
+ *
+ * >>> _listResources('http://foo.com/data/??a.js,b.js,c.js')
+ * ['http://foo.com/data/a.js', 'http://foo.com/data/b.js', 'http://foo.com/data/c.js']
+ */
+function _listResources(src) {
+ var resources = [];
+ var groups = _modconcatLikeUrl(src);
+ if (groups == null) {
+ resources.push(src);
+ } else {
+ var dataurl = groups[1];
+ $.each(cw.utils.lastOf(groups).split(','),
+ function() {
+ resources.push(dataurl + this);
+ });
+ }
+ return resources;
+}
+
//============= utility function handling remote calls responses. ==============//
function _loadAjaxHtmlHead($node, $head, tag, srcattr) {
var jqtagfilter = tag + '[' + srcattr + ']';
@@ -93,29 +128,32 @@
cw['loaded_'+srcattr] = [];
var loaded = cw['loaded_'+srcattr];
jQuery('head ' + jqtagfilter).each(function(i) {
- loaded.push(this.getAttribute(srcattr));
- });
+ // tab1.push.apply(tab1, tab2) <=> tab1 += tab2 (python-wise)
+ loaded.push.apply(loaded, _listResources(this.getAttribute(srcattr)));
+ });
} else {
var loaded = cw['loaded_'+srcattr];
}
$node.find(tag).each(function(i) {
- var url = this.getAttribute(srcattr);
+ var srcnode = this;
+ var url = srcnode.getAttribute(srcattr);
if (url) {
- if (jQuery.inArray(url, loaded) == -1) {
- // take care to <script> tags: jQuery append method script nodes
- // don't appears in the DOM (See comments on
- // http://api.jquery.com/append/), which cause undesired
- // duplicated load in our case. After trying to use bare DOM api
- // to avoid this, we switched to handle a list of already loaded
- // stuff ourselves, since bare DOM api gives bug with the
- // server-response event, since we loose control on when the
- // script is loaded (jQuery load it immediatly).
- loaded.push(url);
- jQuery(this).appendTo($head);
- }
- } else {
- jQuery(this).appendTo($head);
+ $.each(_listResources(url), function() {
+ var resource = '' + this; // implicit object->string cast
+ if ($.inArray(resource, loaded) == -1) {
+ // take care to <script> tags: jQuery append method script nodes
+ // don't appears in the DOM (See comments on
+ // http://api.jquery.com/append/), which cause undesired
+ // duplicated load in our case. After trying to use bare DOM api
+ // to avoid this, we switched to handle a list of already loaded
+ // stuff ourselves, since bare DOM api gives bug with the
+ // server-response event, since we loose control on when the
+ // script is loaded (jQuery load it immediatly).
+ loaded.push(resource);
+ }
+ });
}
+ jQuery(srcnode).appendTo($head);
});
$node.find(jqtagfilter).remove();
}
--- a/web/data/cubicweb.css Thu May 19 10:35:20 2011 +0200
+++ b/web/data/cubicweb.css Thu May 19 10:36:26 2011 +0200
@@ -120,6 +120,19 @@
border: none;
}
+
+img.prevnext {
+ width: 22px;
+ height: 22px;
+}
+
+img.prevnext_nogo {
+ width: 22px;
+ height: 22px;
+ filter:alpha(opacity=25); /* IE */
+ opacity:.25;
+}
+
fieldset {
border: none;
}
--- a/web/data/cubicweb.facets.css Thu May 19 10:35:20 2011 +0200
+++ b/web/data/cubicweb.facets.css Thu May 19 10:36:26 2011 +0200
@@ -109,11 +109,15 @@
div#facetLoading {
display: none;
position: fixed;
- padding-left: 20px;
+ background: #f2f2f2;
top: 400px;
width: 200px;
- height: 100px;
+ padding: 1em;
font-size: 120%;
font-weight: bold;
text-align: center;
}
+
+div.facetTitleSelected {
+ background: url("required.png") no-repeat right top;
+}
--- a/web/data/cubicweb.facets.js Thu May 19 10:35:20 2011 +0200
+++ b/web/data/cubicweb.facets.js Thu May 19 10:36:26 2011 +0200
@@ -238,6 +238,18 @@
});
}
+// change css class of facets that have a value selected
+function updateFacetTitles() {
+ $('.facet').each(function() {
+ var $divTitle = $(this).find('.facetTitle');
+ var facetSelected = $(this).find('.facetValueSelected');
+ if (facetSelected.length) {
+ $divTitle.addClass('facetTitleSelected');
+ } else {
+ $divTitle.removeClass('facetTitleSelected');
+ }
+ });
+}
// we need to differenciate cases where initFacetBoxEvents is called with one
// argument or without any argument. If we use `initFacetBoxEvents` as the
@@ -245,4 +257,34 @@
// his, so we use this small anonymous function instead.
jQuery(document).ready(function() {
initFacetBoxEvents();
+ jQuery(cw).bind('facets-content-loaded', onFacetContentLoaded);
+ jQuery(cw).bind('facets-content-loading', onFacetFiltering);
+ jQuery(cw).bind('facets-content-loading', updateFacetTitles);
});
+
+function showFacetLoading(parentid) {
+ var loadingWidth = 200; // px
+ var loadingHeight = 100; // px
+ var $msg = jQuery('#facetLoading');
+ var $parent = jQuery('#' + parentid);
+ var leftPos = $parent.offset().left + ($parent.width() - loadingWidth) / 2;
+ $parent.fadeTo('normal', 0.2);
+ $msg.css('left', leftPos).show();
+}
+
+function onFacetFiltering(event, divid /* ... */) {
+ showFacetLoading(divid);
+}
+
+function onFacetContentLoaded(event, divid, rql, vid, extraparams) {
+ jQuery('#facetLoading').hide();
+}
+
+jQuery(document).ready(function () {
+ if (jQuery('div.facetBody').length) {
+ var $loadingDiv = $(DIV({id:'facetLoading'},
+ facetLoadingMsg));
+ $loadingDiv.corner();
+ $('body').append($loadingDiv);
+ }
+});
--- a/web/data/cubicweb.js Thu May 19 10:35:20 2011 +0200
+++ b/web/data/cubicweb.js Thu May 19 10:36:26 2011 +0200
@@ -308,6 +308,17 @@
},
/**
+ * returns the last element of an array-like object or undefined if empty
+ */
+ lastOf: function(array) {
+ if (array.length) {
+ return array[array.length-1];
+ } else {
+ return undefined;
+ }
+ },
+
+ /**
* .. function:: difference(lst1, lst2)
*
* returns a list containing all elements in `lst1` that are not
--- a/web/data/cubicweb.old.css Thu May 19 10:35:20 2011 +0200
+++ b/web/data/cubicweb.old.css Thu May 19 10:36:26 2011 +0200
@@ -69,6 +69,18 @@
text-align: center;
}
+img.prevnext {
+ width: 22px;
+ height: 22px;
+}
+
+img.prevnext_nogo {
+ width: 22px;
+ height: 22px;
+ filter:alpha(opacity=25); /* IE */
+ opacity:.25;
+}
+
p {
margin: 0em 0px 0.2em;
padding-top: 2px;
@@ -613,7 +625,7 @@
span.selectedSlice a:visited,
span.selectedSlice a {
- color: #000;
+ background-color: #EBE8D9;
}
/* FIXME should be moved to cubes/folder */
Binary file web/data/go_next.png has changed
Binary file web/data/go_prev.png has changed
--- a/web/request.py Thu May 19 10:35:20 2011 +0200
+++ b/web/request.py Thu May 19 10:36:26 2011 +0200
@@ -92,7 +92,7 @@
self.uiprops = vreg.config.uiprops
self.datadir_url = vreg.config.datadir_url
# raw html headers that can be added from any view
- self.html_headers = HTMLHead()
+ self.html_headers = HTMLHead(self.datadir_url)
# form parameters
self.setup_params(form)
# dictionnary that may be used to store request data that has to be
@@ -256,7 +256,7 @@
"""used by AutomaticWebTest to clear html headers between tests on
the same resultset
"""
- self.html_headers = HTMLHead()
+ self.html_headers = HTMLHead(self.datadir_url)
return self
# web state helpers #######################################################
@@ -415,7 +415,8 @@
@cached # so it's writed only once
def fckeditor_config(self):
- self.add_js('fckeditor/fckeditor.js')
+ fckeditor_url = self.build_url('fckeditor/fckeditor.js')
+ self.add_js(fckeditor_url, localfile=False)
self.html_headers.define_var('fcklang', self.lang)
self.html_headers.define_var('fckconfigpath',
self.data_url('cubicweb.fckcwconfig.js'))
@@ -888,10 +889,20 @@
def _parse_accept_header(raw_header, value_parser=None, value_sort_key=None):
"""returns an ordered list accepted types
- returned value is a list of 2-tuple (value, score), ordered
- by score. Exact type of `value` will depend on what `value_parser`
- will reutrn. if `value_parser` is None, then the raw value, as found
- in the http header, is used.
+ :param value_parser: a function to parse a raw accept chunk. If None
+ is provided, the function defaults to identity. If a function is provided,
+ it must accept 2 parameters ``value`` and ``other_params``. ``value`` is
+ the value found before the first ';', `other_params` is a dictionary
+ built from all other chunks after this first ';'
+
+ :param value_sort_key: a key function to sort values found in the accept
+ header. This function will be passed a 3-tuple
+ (raw_value, parsed_value, score). If None is provided, the default
+ sort_key is 1./score
+
+ :return: a list of 3-tuple (raw_value, parsed_value, score),
+ ordered by score. ``parsed_value`` will be the return value of
+ ``value_parser(raw_value)``
"""
if value_sort_key is None:
value_sort_key = lambda infos: 1./infos[-1]
@@ -926,7 +937,7 @@
'text/html;level=1', `mimetypeinfo` will be ('text', '*', {'level': '1'})
"""
try:
- media_type, media_subtype = value.strip().split('/')
+ media_type, media_subtype = value.strip().split('/', 1)
except ValueError: # safety belt : '/' should always be present
media_type = value.strip()
media_subtype = '*'
--- a/web/test/unittest_views_basecontrollers.py Thu May 19 10:35:20 2011 +0200
+++ b/web/test/unittest_views_basecontrollers.py Thu May 19 10:36:26 2011 +0200
@@ -194,7 +194,7 @@
'use_email-object:'+emaileid: peid,
}
path, params = self.expect_redirect_publish(req, 'edit')
- email.clear_all_caches()
+ email.cw_clear_all_caches()
self.assertEqual(email.address, 'adim@logilab.fr')
--- a/web/views/facets.py Thu May 19 10:35:20 2011 +0200
+++ b/web/views/facets.py Thu May 19 10:36:26 2011 +0200
@@ -73,6 +73,7 @@
req = self._cw
req.add_js( self.needs_js )
req.add_css( self.needs_css)
+ req.html_headers.define_var('facetLoadingMsg', req._('facet-loading-msg'))
if self.roundcorners:
req.html_headers.add_onload('jQuery(".facet").corner("tl br 10px");')
rset, vid, divid, paginate = self._get_context()
--- a/web/views/navigation.py Thu May 19 10:35:20 2011 +0200
+++ b/web/views/navigation.py Thu May 19 10:36:26 2011 +0200
@@ -40,10 +40,10 @@
self.clean_params(params)
basepath = self._cw.relative_path(includeparams=False)
self.w(u'<div class="pagination">')
- self.w(u'%s ' % self.previous_link(basepath, params))
+ self.w(self.previous_link(basepath, params))
self.w(u'[ %s ]' %
u' | '.join(self.iter_page_links(basepath, params)))
- self.w(u' %s' % self.next_link(basepath, params))
+ self.w(u'  %s' % self.next_link(basepath, params))
self.w(u'</div>')
def index_display(self, start, stop):
@@ -74,12 +74,12 @@
basepath = self._cw.relative_path(includeparams=False)
w = self.w
w(u'<div class="pagination">')
- w(u'%s ' % self.previous_link(basepath, params))
+ w(self.previous_link(basepath, params))
w(u'<select onchange="javascript: document.location=this.options[this.selectedIndex].value">')
for option in self.iter_page_links(basepath, params):
w(option)
w(u'</select>')
- w(u' %s' % self.next_link(basepath, params))
+ w(u'  %s' % self.next_link(basepath, params))
w(u'</div>')
--- a/web/views/urlpublishing.py Thu May 19 10:35:20 2011 +0200
+++ b/web/views/urlpublishing.py Thu May 19 10:36:26 2011 +0200
@@ -260,9 +260,8 @@
else:
try:
action = actionsreg._select_best(actions, req, rset=rset)
+ if action is not None:
+ raise Redirect(action.url())
except RegistryException:
- continue
- else:
- # XXX avoid redirect
- raise Redirect(action.url())
+ pass # continue searching
raise PathDontMatch()
--- a/web/webconfig.py Thu May 19 10:35:20 2011 +0200
+++ b/web/webconfig.py Thu May 19 10:36:26 2011 +0200
@@ -300,19 +300,17 @@
if not (self.repairing or self.creating):
self.global_set_option('base-url', baseurl)
httpsurl = self['https-url']
+ if (self.debugmode or self.mode == 'test'):
+ datadir_path = 'data/'
+ else:
+ datadir_path = 'data/%s/' % self.instance_md5_version()
if httpsurl:
if httpsurl[-1] != '/':
httpsurl += '/'
if not self.repairing:
self.global_set_option('https-url', httpsurl)
- if self.debugmode:
- self.https_datadir_url = httpsurl + 'data/'
- else:
- self.https_datadir_url = httpsurl + 'data%s/' % self.instance_md5_version()
- if self.debugmode:
- self.datadir_url = baseurl + 'data/'
- else:
- self.datadir_url = baseurl + 'data%s/' % self.instance_md5_version()
+ self.https_datadir_url = httpsurl + datadir_path
+ self.datadir_url = baseurl + datadir_path
def _build_ui_properties(self):
# self.datadir_url[:-1] to remove trailing /
--- a/web/webctl.py Thu May 19 10:35:20 2011 +0200
+++ b/web/webctl.py Thu May 19 10:36:26 2011 +0200
@@ -21,9 +21,22 @@
__docformat__ = "restructuredtext en"
+import os, os.path as osp
+from shutil import copy
+
from logilab.common.shellutils import ASK
-from cubicweb.toolsutils import CommandHandler, underline_title
+from cubicweb import ExecutionError
+from cubicweb.cwctl import CWCTL
+from cubicweb.cwconfig import CubicWebConfiguration as cwcfg
+from cubicweb.toolsutils import Command, CommandHandler, underline_title
+
+
+try:
+ from os import symlink as linkdir
+except ImportError:
+ from shutil import copytree as linkdir
+
class WebCreateHandler(CommandHandler):
cmdname = 'create'
@@ -43,3 +56,57 @@
def postcreate(self, *args, **kwargs):
"""hooks called once instance's initialization has been completed"""
+
+
+class GenStaticDataDir(Command):
+ """Create a directory merging all data directory content from cubes and CW.
+ """
+ name = 'gen-static-datadir'
+ arguments = '<instance> [dirpath]'
+ min_args = 1
+ max_args = 2
+
+ options = ()
+
+ def run(self, args):
+ appid = args.pop(0)
+ config = cwcfg.config_for(appid)
+ if args:
+ dest = args[0]
+ else:
+ dest = osp.join(config.appdatahome, 'data')
+ if osp.exists(dest):
+ raise ExecutionError('Directory %s already exists. '
+ 'Remove it first.' % dest)
+ config.quick_start = True # notify this is not a regular start
+ # list all resources (no matter their order)
+ resources = set()
+ for datadir in self._datadirs(config):
+ for dirpath, dirnames, filenames in os.walk(datadir):
+ rel_dirpath = dirpath[len(datadir)+1:]
+ resources.update(osp.join(rel_dirpath, f) for f in filenames)
+ # locate resources and copy them to destination
+ for resource in resources:
+ dirname = osp.dirname(resource)
+ dest_resource = osp.join(dest, dirname)
+ if not osp.isdir(dest_resource):
+ os.makedirs(dest_resource)
+ resource_dir, resource_path = config.locate_resource(resource)
+ copy(osp.join(resource_dir, resource_path), dest_resource)
+ # handle md5 version subdirectory
+ linkdir(dest, osp.join(dest, config.instance_md5_version()))
+ print ('You can use apache rewrite rule below :\n'
+ 'RewriteRule ^/data/(.*) %s/$1 [L]' % dest)
+
+ def _datadirs(self, config):
+ repo = config.repository()
+ if config._cubes is None:
+ # web only config
+ config.init_cubes(repo.get_cubes())
+ for cube in repo.get_cubes():
+ cube_datadir = osp.join(cwcfg.cube_dir(cube), 'data')
+ if osp.isdir(cube_datadir):
+ yield cube_datadir
+ yield osp.join(config.shared_dir(), 'data')
+
+CWCTL.register(GenStaticDataDir)