--- a/appobject.py Wed Jun 02 16:13:28 2010 +0200
+++ b/appobject.py Wed Jun 02 16:15:05 2010 +0200
@@ -39,6 +39,92 @@
from logilab.common.decorators import classproperty
from logilab.common.logging_ext import set_log_methods
+from cubicweb.cwconfig import CubicWebConfiguration
+
+def class_regid(cls):
+ """returns a unique identifier for an appobject class"""
+ if 'id' in cls.__dict__:
+ warn('[3.6] %s.%s: id is deprecated, use __regid__'
+ % (cls.__module__, cls.__name__), DeprecationWarning)
+ cls.__regid__ = cls.id
+ if hasattr(cls, 'id') and not isinstance(cls.id, property):
+ return cls.id
+ return cls.__regid__
+
+# helpers for debugging selectors
+TRACED_OIDS = None
+
+def _trace_selector(cls, selector, args, ret):
+ # /!\ lltrace decorates pure function or __call__ method, this
+ # means argument order may be different
+ if isinstance(cls, Selector):
+ selname = str(cls)
+ vobj = args[0]
+ else:
+ selname = selector.__name__
+ vobj = cls
+ if TRACED_OIDS == 'all' or class_regid(vobj) in TRACED_OIDS:
+ #SELECTOR_LOGGER.warning('selector %s returned %s for %s', selname, ret, cls)
+ print '%s -> %s for %s(%s)' % (selname, ret, vobj, vobj.__regid__)
+
+def lltrace(selector):
+ """use this decorator on your selectors so the becomes traceable with
+ :class:`traced_selection`
+ """
+ # don't wrap selectors if not in development mode
+ if CubicWebConfiguration.mode == 'system': # XXX config.debug
+ return selector
+ def traced(cls, *args, **kwargs):
+ ret = selector(cls, *args, **kwargs)
+ if TRACED_OIDS is not None:
+ _trace_selector(cls, selector, args, ret)
+ return ret
+ traced.__name__ = selector.__name__
+ traced.__doc__ = selector.__doc__
+ return traced
+
+class traced_selection(object):
+ """
+ Typical usage is :
+
+ .. sourcecode:: python
+
+ >>> from cubicweb.selectors import traced_selection
+ >>> with traced_selection():
+ ... # some code in which you want to debug selectors
+ ... # for all objects
+
+ Don't forget the 'from __future__ import with_statement' at the module top-level
+ if you're using python prior to 2.6.
+
+ This will yield lines like this in the logs::
+
+ selector one_line_rset returned 0 for <class 'cubicweb.web.views.basecomponents.WFHistoryVComponent'>
+
+ You can also give to :class:`traced_selection` the identifiers of objects on
+ which you want to debug selection ('oid1' and 'oid2' in the example above).
+
+ .. sourcecode:: python
+
+ >>> with traced_selection( ('regid1', 'regid2') ):
+ ... # some code in which you want to debug selectors
+ ... # for objects with __regid__ 'regid1' and 'regid2'
+
+ A potentially usefull point to set up such a tracing function is
+ the `cubicweb.vregistry.Registry.select` method body.
+ """
+
+ def __init__(self, traced='all'):
+ self.traced = traced
+
+ def __enter__(self):
+ global TRACED_OIDS
+ TRACED_OIDS = self.traced
+
+ def __exit__(self, exctype, exc, traceback):
+ global TRACED_OIDS
+ TRACED_OIDS = None
+ return traceback is None
# selector base classes and operations ########################################
@@ -175,6 +261,7 @@
class AndSelector(MultiSelector):
"""and-chained selectors (formerly known as chainall)"""
+ @lltrace
def __call__(self, cls, *args, **kwargs):
score = 0
for selector in self.selectors:
@@ -187,6 +274,7 @@
class OrSelector(MultiSelector):
"""or-chained selectors (formerly known as chainfirst)"""
+ @lltrace
def __call__(self, cls, *args, **kwargs):
for selector in self.selectors:
partscore = selector(cls, *args, **kwargs)
@@ -199,6 +287,7 @@
def __init__(self, selector):
self.selector = selector
+ @lltrace
def __call__(self, cls, *args, **kwargs):
score = self.selector(cls, *args, **kwargs)
return int(not score)
--- a/cwconfig.py Wed Jun 02 16:13:28 2010 +0200
+++ b/cwconfig.py Wed Jun 02 16:15:05 2010 +0200
@@ -293,8 +293,6 @@
log_format = '%(asctime)s - (%(name)s) %(levelname)s: %(message)s'
# nor remove appobjects based on unused interface
cleanup_interface_sobjects = True
- # debug mode
- debugmode = False
if (CWDEV and _forced_mode != 'system'):
@@ -660,12 +658,14 @@
vregpath.append(path + '.py')
return vregpath
- def __init__(self):
+ def __init__(self, debugmode=False):
register_stored_procedures()
ConfigurationMixIn.__init__(self)
+ self.debugmode = debugmode
self.adjust_sys_path()
self.load_defaults()
- self.translations = {}
+ # will be properly initialized later by _gettext_init
+ self.translations = {'en': (unicode, lambda ctx, msgid: unicode(msgid) )}
# don't register ReStructured Text directives by simple import, avoid pb
# with eg sphinx.
# XXX should be done properly with a function from cw.uicfg
@@ -680,16 +680,14 @@
# overriden in CubicWebConfiguration
self.cls_adjust_sys_path()
- def init_log(self, logthreshold=None, debug=False,
- logfile=None, syslog=False):
+ def init_log(self, logthreshold=None, logfile=None, syslog=False):
"""init the log service"""
if logthreshold is None:
- if debug:
+ if self.debugmode:
logthreshold = 'DEBUG'
else:
logthreshold = self['log-threshold']
- self.debugmode = debug
- init_log(debug, syslog, logthreshold, logfile, self.log_format)
+ init_log(self.debugmode, syslog, logthreshold, logfile, self.log_format)
# configure simpleTal logger
logging.getLogger('simpleTAL').setLevel(logging.ERROR)
@@ -803,12 +801,12 @@
return mdir
@classmethod
- def config_for(cls, appid, config=None):
+ def config_for(cls, appid, config=None, debugmode=False):
"""return a configuration instance for the given instance identifier
"""
config = config or guess_configuration(cls.instance_home(appid))
configcls = configuration_cls(config)
- return configcls(appid)
+ return configcls(appid, debugmode)
@classmethod
def possible_configurations(cls, appid):
@@ -876,9 +874,9 @@
# instance methods used to get instance specific resources #############
- def __init__(self, appid):
+ def __init__(self, appid, debugmode=False):
self.appid = appid
- CubicWebNoAppConfiguration.__init__(self)
+ CubicWebNoAppConfiguration.__init__(self, debugmode)
self._cubes = None
self._site_loaded = set()
self.load_file_configuration(self.main_config_file())
@@ -991,14 +989,14 @@
super(CubicWebConfiguration, self).load_configuration()
if self.apphome and self.set_language:
# init gettext
- self._set_language()
+ self._gettext_init()
- def init_log(self, logthreshold=None, debug=False, force=False):
+ def init_log(self, logthreshold=None, force=False):
"""init the log service"""
if not force and hasattr(self, '_logging_initialized'):
return
self._logging_initialized = True
- CubicWebNoAppConfiguration.init_log(self, logthreshold, debug,
+ CubicWebNoAppConfiguration.init_log(self, logthreshold,
logfile=self.get('log-file'))
# read a config file if it exists
logconfig = join(self.apphome, 'logging.conf')
@@ -1019,7 +1017,7 @@
if lang != 'en':
yield lang
- def _set_language(self):
+ def _gettext_init(self):
"""set language for gettext"""
from gettext import translation
path = join(self.apphome, 'i18n')
--- a/cwctl.py Wed Jun 02 16:13:28 2010 +0200
+++ b/cwctl.py Wed Jun 02 16:15:05 2010 +0200
@@ -477,14 +477,13 @@
def start_instance(self, appid):
"""start the instance's server"""
- debug = self['debug']
force = self['force']
loglevel = self['loglevel']
- config = cwcfg.config_for(appid)
+ config = cwcfg.config_for(appid, debugmode=self['debug'])
if loglevel is not None:
loglevel = 'LOG_%s' % loglevel.upper()
config.global_set_option('log-threshold', loglevel)
- config.init_log(loglevel, debug=debug, force=True)
+ config.init_log(loglevel, force=True)
if self['profile']:
config.global_set_option('profile', self.config.profile)
helper = self.config_helper(config, cmdname='start')
@@ -493,7 +492,7 @@
msg = "%s seems to be running. Remove %s by hand if necessary or use \
the --force option."
raise ExecutionError(msg % (appid, pidf))
- helper.start_server(config, debug)
+ helper.start_server(config)
class StopInstanceCommand(InstanceCommand):
@@ -781,11 +780,15 @@
repository internals (session, etc...) so most migration commands won't be
available.
+ Arguments after bare "--" string will not be processed by the shell command
+ You can use it to pass extra arguments to your script and expect for
+ them in '__args__' afterwards.
+
<instance>
the identifier of the instance to connect.
"""
name = 'shell'
- arguments = '<instance> [batch command file]'
+ arguments = '<instance> [batch command file(s)] [-- <script arguments>]'
options = (
('system-only',
{'short': 'S', 'action' : 'store_true',
@@ -861,8 +864,11 @@
mih = config.migration_handler()
try:
if args:
- for arg in args:
- mih.cmd_process_script(arg)
+ # use cmdline parser to access left/right attributes only
+ # remember that usage requires instance appid as first argument
+ scripts, args = self.cmdline_parser.largs[1:], self.cmdline_parser.rargs
+ for script in scripts:
+ mih.cmd_process_script(script, scriptargs=args)
else:
mih.interactive_shell()
finally:
--- a/cwvreg.py Wed Jun 02 16:13:28 2010 +0200
+++ b/cwvreg.py Wed Jun 02 16:15:05 2010 +0200
@@ -82,7 +82,6 @@
.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_all
.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_and_replace
.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register
-.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_if_interface_found
.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.unregister
Examples:
@@ -193,6 +192,8 @@
__docformat__ = "restructuredtext en"
_ = unicode
+from warnings import warn
+
from logilab.common.decorators import cached, clear_cache
from logilab.common.deprecation import deprecated
from logilab.common.modutils import cleanup_sys_modules
@@ -212,23 +213,23 @@
def use_interfaces(obj):
"""return interfaces used by the given object by searching for implements
- selectors, with a bw compat fallback to accepts_interfaces attribute
+ selectors
"""
from cubicweb.selectors import implements
- try:
- # XXX deprecated
- return sorted(obj.accepts_interfaces)
- except AttributeError:
- try:
- impl = obj.__select__.search_selector(implements)
- if impl:
- return sorted(impl.expected_ifaces)
- except AttributeError:
- pass # old-style appobject classes with no accepts_interfaces
- except:
- print 'bad selector %s on %s' % (obj.__select__, obj)
- raise
- return ()
+ impl = obj.__select__.search_selector(implements)
+ if impl:
+ return sorted(impl.expected_ifaces)
+ return ()
+
+def require_appobject(obj):
+ """return interfaces used by the given object by searching for implements
+ selectors
+ """
+ from cubicweb.selectors import appobject_selectable
+ impl = obj.__select__.search_selector(appobject_selectable)
+ if impl:
+ return (impl.registry, impl.regids)
+ return None
class CWRegistry(Registry):
@@ -443,14 +444,13 @@
* contentnavigation XXX to merge with components? to kill?
"""
- def __init__(self, config, debug=None, initlog=True):
+ def __init__(self, config, initlog=True):
if initlog:
# first init log service
- config.init_log(debug=debug)
+ config.init_log()
super(CubicWebVRegistry, self).__init__(config)
self.schema = None
self.initialized = False
- self.reset()
# XXX give force_reload (or refactor [re]loading...)
if self.config.mode != 'test':
# don't clear rtags during test, this may cause breakage with
@@ -479,6 +479,7 @@
def reset(self):
super(CubicWebVRegistry, self).reset()
self._needs_iface = {}
+ self._needs_appobject = {}
# two special registries, propertydefs which care all the property
# definitions, and propertyvals which contains values for those
# properties
@@ -520,7 +521,6 @@
if not cube in cubes:
cpath = cfg.build_vregistry_cube_path([cfg.cube_dir(cube)])
cleanup_sys_modules(cpath)
- self.reset()
self.register_objects(path, force_reload)
CW_EVENT_MANAGER.emit('after-registry-reload')
@@ -539,6 +539,7 @@
for obj in objects:
obj.schema = schema
+ @deprecated('[3.9] use .register instead')
def register_if_interface_found(self, obj, ifaces, **kwargs):
"""register `obj` but remove it if no entity class implements one of
the given `ifaces` interfaces at the end of the registration process.
@@ -564,7 +565,15 @@
# XXX bw compat
ifaces = use_interfaces(obj)
if ifaces:
+ if not obj.__name__.endswith('Adapter') and \
+ any(iface for iface in ifaces if not isinstance(iface, basestring)):
+ warn('[3.9] %s: interfaces in implements selector are '
+ 'deprecated in favor of adapters / appobject_selectable '
+ 'selector' % obj.__name__, DeprecationWarning)
self._needs_iface[obj] = ifaces
+ depends_on = require_appobject(obj)
+ if depends_on is not None:
+ self._needs_appobject[obj] = depends_on
def register_objects(self, path, force_reload=False):
"""overriden to remove objects requiring a missing interface"""
@@ -581,13 +590,18 @@
# we may want to keep interface dependent objects (e.g.for i18n
# catalog generation)
if self.config.cleanup_interface_sobjects:
- # remove appobjects that don't support any available interface
+ # XXX deprecated with cw 3.9: remove appobjects that don't support
+ # any available interface
implemented_interfaces = set()
if 'Any' in self.get('etypes', ()):
for etype in self.schema.entities():
if etype.final:
continue
cls = self['etypes'].etype_class(etype)
+ if cls.__implements__:
+ warn('[3.9] %s: using __implements__/interfaces are '
+ 'deprecated in favor of adapters' % cls.__name__,
+ DeprecationWarning)
for iface in cls.__implements__:
implemented_interfaces.update(iface.__mro__)
implemented_interfaces.update(cls.__mro__)
@@ -601,9 +615,22 @@
self.debug('kicking appobject %s (no implemented '
'interface among %s)', obj, ifaces)
self.unregister(obj)
- # clear needs_iface so we don't try to remove some not-anymore-in
- # objects on automatic reloading
- self._needs_iface.clear()
+ # since 3.9: remove appobjects which depending on other, unexistant
+ # appobjects
+ for obj, (regname, regids) in self._needs_appobject.items():
+ try:
+ registry = self[regname]
+ except RegistryNotFound:
+ self.debug('kicking %s (no registry %s)', obj, regname)
+ self.unregister(obj)
+ continue
+ for regid in regids:
+ if registry.get(regid):
+ break
+ else:
+ self.debug('kicking %s (no %s object in registry %s)',
+ obj, ' or '.join(regids), regname)
+ self.unregister(obj)
super(CubicWebVRegistry, self).initialization_completed()
for rtag in RTAGS:
# don't check rtags if we don't want to cleanup_interface_sobjects
--- a/dataimport.py Wed Jun 02 16:13:28 2010 +0200
+++ b/dataimport.py Wed Jun 02 16:15:05 2010 +0200
@@ -584,7 +584,7 @@
kwargs[k] = getattr(v, 'eid', v)
entity, rels = self.metagen.base_etype_dicts(etype)
entity = copy(entity)
- entity._related_cache = {}
+ entity.cw_clear_relation_cache()
self.metagen.init_entity(entity)
entity.update(kwargs)
entity.edited_attributes = set(entity)
--- a/dbapi.py Wed Jun 02 16:13:28 2010 +0200
+++ b/dbapi.py Wed Jun 02 16:15:05 2010 +0200
@@ -254,9 +254,6 @@
self.session = None
self.cnx = self.user = _NeedAuthAccessMock()
- def base_url(self):
- return self.vreg.config['base-url']
-
def from_controller(self):
return 'view'
--- a/devtools/__init__.py Wed Jun 02 16:13:28 2010 +0200
+++ b/devtools/__init__.py Wed Jun 02 16:15:05 2010 +0200
@@ -181,10 +181,6 @@
def available_languages(self, *args):
return ('en', 'fr', 'de')
- def ext_resources_file(self):
- """return instance's external resources file"""
- return join(self.apphome, 'data', 'external_resources')
-
def pyro_enabled(self):
# but export PYRO_MULTITHREAD=0 or you get problems with sqlite and threads
return True
--- a/devtools/devctl.py Wed Jun 02 16:13:28 2010 +0200
+++ b/devtools/devctl.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,10 +15,10 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""additional cubicweb-ctl commands and command handlers for cubicweb and cubicweb's
-cubes development
+"""additional cubicweb-ctl commands and command handlers for cubicweb and
+cubicweb's cubes development
+"""
-"""
__docformat__ = "restructuredtext en"
# *ctl module should limit the number of import to be imported as quickly as
@@ -67,7 +67,7 @@
return None
def main_config_file(self):
return None
- def init_log(self, debug=None):
+ def init_log(self):
pass
def load_configuration(self):
pass
@@ -603,7 +603,7 @@
exclude = SKEL_EXCLUDE
if self['layout'] == 'simple':
exclude += ('sobjects.py*', 'precreate.py*', 'realdb_test*',
- 'cubes.*', 'external_resources*')
+ 'cubes.*', 'uiprops.py*')
copy_skeleton(skeldir, cubedir, context, exclude=exclude)
def _ask_for_dependencies(self):
--- a/devtools/fake.py Wed Jun 02 16:13:28 2010 +0200
+++ b/devtools/fake.py Wed Jun 02 16:15:05 2010 +0200
@@ -30,6 +30,7 @@
class FakeConfig(dict, BaseApptestConfiguration):
translations = {}
+ uiprops = {}
apphome = None
def __init__(self, appid='data', apphome=None, cubes=()):
self.appid = appid
@@ -39,6 +40,7 @@
self['uid'] = None
self['base-url'] = BASE_URL
self['rql-cache-size'] = 100
+ self.datadir_url = BASE_URL + 'data/'
def cubes(self, expand=False):
return self._cubes
@@ -66,10 +68,6 @@
def header_if_modified_since(self):
return None
- def base_url(self):
- """return the root url of the instance"""
- return BASE_URL
-
def relative_path(self, includeparams=True):
"""return the normalized path of the request (ie at least relative
to the instance's root, but some other normalization may be needed
--- a/devtools/fill.py Wed Jun 02 16:13:28 2010 +0200
+++ b/devtools/fill.py Wed Jun 02 16:15:05 2010 +0200
@@ -217,7 +217,7 @@
# XXX nothing to do here
def generate_Any_data_format(self, entity, index, **kwargs):
- # data_format attribute of Image/File has no vocabulary constraint, we
+ # data_format attribute of File has no vocabulary constraint, we
# need this method else stupid values will be set which make mtconverter
# raise exception
return u'application/octet-stream'
@@ -228,12 +228,6 @@
# raise exception
return u'text/plain'
- def generate_Image_data_format(self, entity, index, **kwargs):
- # data_format attribute of Image/File has no vocabulary constraint, we
- # need this method else stupid values will be set which make mtconverter
- # raise exception
- return u'image/png'
-
class autoextend(type):
def __new__(mcs, name, bases, classdict):
--- a/devtools/testlib.py Wed Jun 02 16:13:28 2010 +0200
+++ b/devtools/testlib.py Wed Jun 02 16:15:05 2010 +0200
@@ -306,7 +306,7 @@
req.execute('SET X in_group G WHERE X eid %%(x)s, G name IN(%s)'
% ','.join(repr(g) for g in groups),
{'x': user.eid})
- user.clear_related_cache('in_group', 'subject')
+ user.cw_clear_relation_cache('in_group', 'subject')
if commit:
req.cnx.commit()
return user
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/annexes/docstrings-conventions.rst Wed Jun 02 16:15:05 2010 +0200
@@ -0,0 +1,106 @@
+Javascript docstrings
+=====================
+
+Whereas in Python source code we only need to include a module docstrings
+using the directive `.. automodule:: mypythonmodule`, we will have to
+explicitely define Javascript modules and functions in the doctrings since
+there is no native directive to include Javascript files.
+
+Rest generation
+---------------
+
+`pyjsrest` is a small utility parsing Javascript doctrings and generating the
+corresponding Restructured file used by Sphinx to generate HTML documentation.
+This script will have the following structure::
+
+ ===========
+ filename.js
+ ===========
+ .. module:: filename.js
+
+We use the `.. module::` directive to register a javascript library
+as a Python module for Sphinx. This provides an entry in the module index.
+
+The contents of the docstring found in the javascript file will be added as is
+following the module declaration. No treatment will be done on the doctring.
+All the documentation structure will be in the docstrings and will comply
+with the following rules.
+
+Docstring structure
+-------------------
+
+Basically we document javascript with RestructuredText docstring
+following the same convention as documenting Python code.
+
+The doctring in Javascript files must be contained in standard
+Javascript comment signs, starting with `/**` and ending with `*/`,
+such as::
+
+ /**
+ * My comment starts here.
+ * This is the second line prefixed with a `*`.
+ * ...
+ * ...
+ * All the follwing line will be prefixed with a `*` followed by a space.
+ * ...
+ * ...
+ */
+
+
+Comments line prefixed by `//` will be ignored. They are reserved for source
+code comments dedicated to developers.
+
+
+Javscript functions docstring
+-----------------------------
+
+By default, the `function` directive describes a module-level function.
+
+`function` directive
+~~~~~~~~~~~~~~~~~~~~
+
+Its purpose is to define the function prototype such as::
+
+ .. function:: loadxhtml(url, data, reqtype, mode)
+
+If any namespace is used, we should add it in the prototype for now,
+until we define an appropriate directive.
+::
+ .. function:: jQuery.fn.loadxhtml(url, data, reqtype, mode)
+
+Function parameters
+~~~~~~~~~~~~~~~~~~~
+
+We will define function parameters as a bulleted list, where the
+parameter name will be backquoted and followed by its description.
+
+Example of a javascript function docstring::
+
+ .. function:: loadxhtml(url, data, reqtype, mode)
+
+ cubicweb loadxhtml plugin to make jquery handle xhtml response
+
+ fetches `url` and replaces this's content with the result
+
+ Its arguments are:
+
+ * `url`
+
+ * `mode`, how the replacement should be done (default is 'replace')
+ Possible values are :
+ - 'replace' to replace the node's content with the generated HTML
+ - 'swap' to replace the node itself with the generated HTML
+ - 'append' to append the generated HTML to the node's content
+
+
+Optional parameter specification
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Javascript functions handle arguments not listed in the function signature.
+In the javascript code, they will be flagged using `/* ... */`. In the docstring,
+we flag those optional arguments the same way we would define it in
+Python::
+
+ .. function:: asyncRemoteExec(fname, arg1=None, arg2=None)
+
+
--- a/doc/book/en/annexes/index.rst Wed Jun 02 16:13:28 2010 +0200
+++ b/doc/book/en/annexes/index.rst Wed Jun 02 16:15:05 2010 +0200
@@ -17,3 +17,5 @@
rql/index
mercurial
depends
+ javascript-api
+ docstrings-conventions
--- a/doc/book/en/devrepo/vreg.rst Wed Jun 02 16:13:28 2010 +0200
+++ b/doc/book/en/devrepo/vreg.rst Wed Jun 02 16:15:05 2010 +0200
@@ -37,6 +37,7 @@
.. autoclass:: cubicweb.appobject.yes
.. autoclass:: cubicweb.selectors.match_kwargs
.. autoclass:: cubicweb.selectors.appobject_selectable
+.. autoclass:: cubicweb.selectors.adaptable
Result set selectors
@@ -75,6 +76,7 @@
.. autoclass:: cubicweb.selectors.partial_has_related_entities
.. autoclass:: cubicweb.selectors.has_permission
.. autoclass:: cubicweb.selectors.has_add_permission
+.. autoclass:: cubicweb.selectors.has_mimetype
Logged user selectors
--- a/doc/book/en/devweb/js.rst Wed Jun 02 16:13:28 2010 +0200
+++ b/doc/book/en/devweb/js.rst Wed Jun 02 16:15:05 2010 +0200
@@ -350,3 +350,11 @@
There is also javascript support for massmailing, gmap (google maps),
fckcwconfig (fck editor), timeline, calendar, goa (CubicWeb over
AppEngine), flot (charts drawing), tabs and bookmarks.
+
+API
+~~~
+
+.. toctree::
+ :maxdepth: 1
+
+ js_api/index
--- a/doc/book/en/makefile Wed Jun 02 16:13:28 2010 +0200
+++ b/doc/book/en/makefile Wed Jun 02 16:15:05 2010 +0200
@@ -11,6 +11,10 @@
PAPER =
#BUILDDIR = build
BUILDDIR = ~/tmp/cwdoc
+CWDIR = ../../..
+JSDIR = ${CWDIR}/web/data
+JSTORST = ${CWDIR}/doc/tools/pyjsrest.py
+BUILDJS = devweb/js_api
# Internal variables for sphinx
PAPEROPT_a4 = -D latex_paper_size=a4
@@ -18,6 +22,7 @@
ALLSPHINXOPTS = -d ${BUILDDIR}/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
.PHONY: help clean html web pickle htmlhelp latex changes linkcheck
help:
@@ -36,6 +41,7 @@
rm -rf apidoc/
rm -f *.html
-rm -rf ${BUILDDIR}/*
+ -rm -rf ${BUILDJS}
all: ${TARGET} apidoc html
@@ -48,12 +54,16 @@
epydoc --html -o apidoc -n "cubicweb" --exclude=setup --exclude=__pkginfo__ ../../../
# run sphinx ###
-html:
+html: js
mkdir -p ${BUILDDIR}/html ${BUILDDIR}/doctrees
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) ${BUILDDIR}/html
@echo
@echo "Build finished. The HTML pages are in ${BUILDDIR}/html."
+js:
+ mkdir -p ${BUILDJS}
+ $(JSTORST) -p ${JSDIR} -o ${BUILDJS}
+
pickle:
mkdir -p ${BUILDDIR}/pickle ${BUILDDIR}/doctrees
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) ${BUILDDIR}/pickle
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/refactoring-the-css-with-uiprops.rst Wed Jun 02 16:15:05 2010 +0200
@@ -0,0 +1,73 @@
+=========================================
+Refactoring the CSSs with UI properties
+=========================================
+
+Overview
+=========
+
+Managing styles progressively became difficult in CubicWeb. The
+introduction of uiprops is an attempt to fix this problem.
+
+The goal is to make it possible to use variables in our CSSs.
+
+These variables are defined or computed in the uiprops.py python file
+and inserted in the CSS using the Python string interpolation syntax.
+
+A quick example, put in ``uiprops.py``::
+
+ defaultBgColor = '#eee'
+
+and in your css::
+
+ body { background-color: %(defaultBgColor)s; }
+
+
+The good practices are:
+
+- define a variable in uiprops to avoid repetitions in the CSS
+ (colors, borders, fonts, etc.)
+
+- define a variable in uiprops when you need to compute values
+ (compute a color palette, etc.)
+
+The algorithm implemented in CubicWeb is the following:
+
+- read uiprops file while walk up the chain of cube dependencies: if
+ cube myblog depends on cube comment, the variables defined in myblog
+ will have precedence over the ones in comment
+
+- replace the %(varname)s in all the CSSs of all the cubes
+
+Keep in mind that the browser will then interpret the CSSs and apply
+the standard cascading mechanism.
+
+FAQ
+====
+
+- How do I keep the old style?
+
+ Put ``STYLESHEET = [data('cubicweb.old.css')]`` in your uiprops.py
+ file and think about something else.
+
+- What are the changes in cubicweb.css?
+
+ Version 3.9.0 of cubicweb changed the following in the default html
+ markup and css:
+
+ =============== ==================================
+ old new
+ =============== ==================================
+ .navcol #navColumnLeft, #navColumnRight
+ #contentcol #contentColumn
+ .footer #footer
+ .logo #logo
+ .simpleMessage .loginMessage
+ .appMsg (styles are removed from css)
+ .searchMessage (styles are removed from css)
+ =============== ==================================
+
+ Introduction of the new cubicweb.reset.css based on Eric Meyer's
+ reset css.
+
+ Lots of margin, padding, etc.
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/tools/pyjsrest.py Wed Jun 02 16:15:05 2010 +0200
@@ -0,0 +1,110 @@
+#!/usr/bin/env python
+"""
+Parser for Javascript comments.
+"""
+from __future__ import with_statement
+
+import sys, os, getopt, re
+
+def clean_comment(match):
+ comment = match.group()
+ comment = strip_stars(comment)
+ return comment
+
+# Rest utilities
+def rest_title(title, level, level_markups=['=', '=', '-', '~', '+', '`']):
+ size = len(title)
+ if level == 0:
+ return '\n'.join((level_markups[level] * size, title, level_markups[0] * size)) + '\n'
+ return '\n'.join(('\n' + title, level_markups[level] * size)) + '\n'
+
+def get_doc_comments(text):
+ """
+ Return a list of all documentation comments in the file text. Each
+ comment is a pair, with the first element being the comment text and
+ the second element being the line after it, which may be needed to
+ guess function & arguments.
+
+ >>> get_doc_comments(read_file('examples/module.js'))[0][0][:40]
+ '/**\n * This is the module documentation.'
+ >>> get_doc_comments(read_file('examples/module.js'))[1][0][7:50]
+ 'This is documentation for the first method.'
+ >>> get_doc_comments(read_file('examples/module.js'))[1][1]
+ 'function the_first_function(arg1, arg2) '
+ >>> get_doc_comments(read_file('examples/module.js'))[2][0]
+ '/** This is the documentation for the second function. */'
+
+ """
+ return [clean_comment(match) for match in re.finditer('/\*\*.*?\*/',
+ text, re.DOTALL|re.MULTILINE)]
+
+RE_STARS = re.compile('^\s*?\* ?', re.MULTILINE)
+
+
+def strip_stars(doc_comment):
+ """
+ Strip leading stars from a doc comment.
+
+ >>> strip_stars('/** This is a comment. */')
+ 'This is a comment.'
+ >>> strip_stars('/**\n * This is a\n * multiline comment. */')
+ 'This is a\n multiline comment.'
+ >>> strip_stars('/** \n\t * This is a\n\t * multiline comment. \n*/')
+ 'This is a\n multiline comment.'
+
+ """
+ return RE_STARS.sub('', doc_comment[3:-2]).strip()
+
+def parse_js_files(args=sys.argv):
+ """
+ Main command-line invocation.
+ """
+ try:
+ opts, args = getopt.gnu_getopt(args[1:], 'p:o:h', [
+ 'jspath=', 'output=', 'help'])
+ opts = dict(opts)
+ except getopt.GetoptError:
+ usage()
+ sys.exit(2)
+
+ rst_dir = opts.get('--output') or opts.get('-o')
+ if rst_dir is None and len(args) != 1:
+ rst_dir = 'apidocs'
+ js_dir = opts.get('--jspath') or opts.get('-p')
+ if not os.path.exists(os.path.join(rst_dir)):
+ os.makedirs(os.path.join(rst_dir))
+
+ f_index = open(os.path.join(rst_dir, 'index.rst'), 'wb')
+ f_index.write('''
+.. toctree::
+ :maxdepth: 1
+
+'''
+)
+ for js_path, js_dirs, js_files in os.walk(js_dir):
+ rst_path = re.sub('%s%s*' % (js_dir, os.path.sep), '', js_path)
+ for js_file in js_files:
+ if not js_file.endswith('.js'):
+ continue
+ if not os.path.exists(os.path.join(rst_dir, rst_path)):
+ os.makedirs(os.path.join(rst_dir, rst_path))
+ rst_content = extract_rest(js_path, js_file)
+ filename = os.path.join(rst_path, js_file[:-3])
+ # add to index
+ f_index.write(' %s\n' % filename)
+ # save rst file
+ with open(os.path.join(rst_dir, filename) + '.rst', 'wb') as f_rst:
+ f_rst.write(rst_content)
+ f_index.close()
+
+def extract_rest(js_dir, js_file):
+ js_filepath = os.path.join(js_dir, js_file)
+ filecontent = open(js_filepath, 'U').read()
+ comments = get_doc_comments(filecontent)
+ rst = rest_title(js_file, 0)
+ rst += '.. module:: %s\n\n' % js_file
+ rst += '\n\n'.join(comments)
+ return rst
+
+if __name__ == '__main__':
+ parse_js_files()
--- a/entities/__init__.py Wed Jun 02 16:13:28 2010 +0200
+++ b/entities/__init__.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,9 +15,8 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""base application's entities class implementation: `AnyEntity`
+"""base application's entities class implementation: `AnyEntity`"""
-"""
__docformat__ = "restructuredtext en"
from warnings import warn
@@ -28,33 +27,13 @@
from cubicweb import Unauthorized, typed_eid
from cubicweb.entity import Entity
-from cubicweb.interfaces import IBreadCrumbs, IFeed
-
class AnyEntity(Entity):
"""an entity instance has e_schema automagically set on the class and
instances have access to their issuing cursor
"""
__regid__ = 'Any'
- __implements__ = (IBreadCrumbs, IFeed)
-
- fetch_attrs = ('modification_date',)
- @classmethod
- def fetch_order(cls, attr, var):
- """class method used to control sort order when multiple entities of
- this type are fetched
- """
- return cls.fetch_unrelated_order(attr, var)
-
- @classmethod
- def fetch_unrelated_order(cls, attr, var):
- """class method used to control sort order when multiple entities of
- this type are fetched to use in edition (eg propose them to create a
- new relation on an edited entity).
- """
- if attr == 'modification_date':
- return '%s DESC' % var
- return None
+ __implements__ = ()
# meta data api ###########################################################
@@ -63,7 +42,7 @@
for rschema, attrschema in self.e_schema.attribute_definitions():
if rschema.meta:
continue
- value = self.get_value(rschema.type)
+ value = self.cw_attr_value(rschema.type)
if value:
# make the value printable (dates, floats, bytes, etc.)
return self.printable_value(rschema.type, value, attrschema.type,
@@ -120,32 +99,6 @@
except (Unauthorized, IndexError):
return None
- def breadcrumbs(self, view=None, recurs=False):
- path = [self]
- if hasattr(self, 'parent'):
- parent = self.parent()
- if parent is not None:
- try:
- path = parent.breadcrumbs(view, True) + [self]
- except TypeError:
- warn("breadcrumbs method's now takes two arguments "
- "(view=None, recurs=False), please update",
- DeprecationWarning)
- path = parent.breadcrumbs(view) + [self]
- if not recurs:
- if view is None:
- if 'vtitle' in self._cw.form:
- # embeding for instance
- path.append( self._cw.form['vtitle'] )
- elif view.__regid__ != 'primary' and hasattr(view, 'title'):
- path.append( self._cw._(view.title) )
- return path
-
- ## IFeed interface ########################################################
-
- def rss_feed_url(self):
- return self.absolute_url(vid='rss')
-
# abstractions making the whole things (well, some at least) working ######
def sortvalue(self, rtype=None):
@@ -154,7 +107,7 @@
"""
if rtype is None:
return self.dc_title().lower()
- value = self.get_value(rtype)
+ value = self.cw_attr_value(rtype)
# do not restrict to `unicode` because Bytes will return a `str` value
if isinstance(value, basestring):
return self.printable_value(rtype, format='text/plain').lower()
@@ -189,35 +142,8 @@
self.__linkto[(rtype, role)] = linkedto
return linkedto
- # edit controller callbacks ###############################################
-
- def after_deletion_path(self):
- """return (path, parameters) which should be used as redirect
- information when this entity is being deleted
- """
- if hasattr(self, 'parent') and self.parent():
- return self.parent().rest_path(), {}
- return str(self.e_schema).lower(), {}
-
- def pre_web_edit(self):
- """callback called by the web editcontroller when an entity will be
- created/modified, to let a chance to do some entity specific stuff.
-
- Do nothing by default.
- """
- pass
-
# server side helpers #####################################################
- def notification_references(self, view):
- """used to control References field of email send on notification
- for this entity. `view` is the notification view.
-
- Should return a list of eids which can be used to generate message ids
- of previously sent email
- """
- return ()
-
# XXX: store a reference to the AnyEntity class since it is hijacked in goa
# configuration and we need the actual reference to avoid infinite loops
# in mro
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/entities/adapters.py Wed Jun 02 16:15:05 2010 +0200
@@ -0,0 +1,166 @@
+# copyright 2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+"""some basic entity adapter implementations, for interfaces used in the
+framework itself.
+"""
+
+__docformat__ = "restructuredtext en"
+
+from cubicweb.view import EntityAdapter, implements_adapter_compat
+from cubicweb.selectors import implements, relation_possible
+from cubicweb.interfaces import IDownloadable
+
+
+class IEmailableAdapter(EntityAdapter):
+ __regid__ = 'IEmailable'
+ __select__ = relation_possible('primary_email') | relation_possible('use_email')
+
+ def get_email(self):
+ if getattr(self.entity, 'primary_email', None):
+ return self.entity.primary_email[0].address
+ if getattr(self.entity, 'use_email', None):
+ return self.entity.use_email[0].address
+ return None
+
+ def allowed_massmail_keys(self):
+ """returns a set of allowed email substitution keys
+
+ The default is to return the entity's attribute list but you might
+ override this method to allow extra keys. For instance, a Person
+ class might want to return a `companyname` key.
+ """
+ return set(rschema.type
+ for rschema, attrtype in self.entity.e_schema.attribute_definitions()
+ if attrtype.type not in ('Password', 'Bytes'))
+
+ def as_email_context(self):
+ """returns the dictionary as used by the sendmail controller to
+ build email bodies.
+
+ NOTE: the dictionary keys should match the list returned by the
+ `allowed_massmail_keys` method.
+ """
+ return dict( (attr, getattr(self.entity, attr))
+ for attr in self.allowed_massmail_keys() )
+
+
+class INotifiableAdapter(EntityAdapter):
+ __regid__ = 'INotifiable'
+ __select__ = implements('Any')
+
+ @implements_adapter_compat('INotifiableAdapter')
+ def notification_references(self, view):
+ """used to control References field of email send on notification
+ for this entity. `view` is the notification view.
+
+ Should return a list of eids which can be used to generate message
+ identifiers of previously sent email(s)
+ """
+ itree = self.entity.cw_adapt_to('ITree')
+ if itree is not None:
+ return itree.path()[:-1]
+ return ()
+
+
+class IFTIndexableAdapter(EntityAdapter):
+ __regid__ = 'IFTIndexable'
+ __select__ = implements('Any')
+
+ def fti_containers(self, _done=None):
+ if _done is None:
+ _done = set()
+ entity = self.entity
+ _done.add(entity.eid)
+ containers = tuple(entity.e_schema.fulltext_containers())
+ if containers:
+ for rschema, target in containers:
+ if target == 'object':
+ targets = getattr(entity, rschema.type)
+ else:
+ targets = getattr(entity, 'reverse_%s' % rschema)
+ for entity in targets:
+ if entity.eid in _done:
+ continue
+ for container in entity.cw_adapt_to('IFTIndexable').fti_containers(_done):
+ yield container
+ yielded = True
+ else:
+ yield entity
+
+ def get_words(self):
+ """used by the full text indexer to get words to index
+
+ this method should only be used on the repository side since it depends
+ on the logilab.database package
+
+ :rtype: list
+ :return: the list of indexable word of this entity
+ """
+ from logilab.database.fti import tokenize
+ # take care to cases where we're modyfying the schema
+ entity = self.entity
+ pending = self._cw.transaction_data.setdefault('pendingrdefs', set())
+ words = []
+ for rschema in entity.e_schema.indexable_attributes():
+ if (entity.e_schema, rschema) in pending:
+ continue
+ try:
+ value = entity.printable_value(rschema, format='text/plain')
+ except TransformError:
+ continue
+ except:
+ self.exception("can't add value of %s to text index for entity %s",
+ rschema, entity.eid)
+ continue
+ if value:
+ words += tokenize(value)
+ for rschema, role in entity.e_schema.fulltext_relations():
+ if role == 'subject':
+ for entity_ in getattr(entity, rschema.type):
+ words += entity_.cw_adapt_to('IFTIndexable').get_words()
+ else: # if role == 'object':
+ for entity_ in getattr(entity, 'reverse_%s' % rschema.type):
+ words += entity_.cw_adapt_to('IFTIndexable').get_words()
+ return words
+
+
+class IDownloadableAdapter(EntityAdapter):
+ """interface for downloadable entities"""
+ __regid__ = 'IDownloadable'
+ __select__ = implements(IDownloadable) # XXX for bw compat, else should be abstract
+
+ @implements_adapter_compat('IDownloadable')
+ def download_url(self): # XXX not really part of this interface
+ """return an url to download entity's content"""
+ raise NotImplementedError
+ @implements_adapter_compat('IDownloadable')
+ def download_content_type(self):
+ """return MIME type of the downloadable content"""
+ raise NotImplementedError
+ @implements_adapter_compat('IDownloadable')
+ def download_encoding(self):
+ """return encoding of the downloadable content"""
+ raise NotImplementedError
+ @implements_adapter_compat('IDownloadable')
+ def download_file_name(self):
+ """return file name of the downloadable content"""
+ raise NotImplementedError
+ @implements_adapter_compat('IDownloadable')
+ def download_data(self):
+ """return actual data of the downloadable content"""
+ raise NotImplementedError
--- a/entities/authobjs.py Wed Jun 02 16:13:28 2010 +0200
+++ b/entities/authobjs.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,9 +15,8 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""entity classes user and group entities
+"""entity classes user and group entities"""
-"""
__docformat__ = "restructuredtext en"
from logilab.common.decorators import cached
--- a/entities/lib.py Wed Jun 02 16:13:28 2010 +0200
+++ b/entities/lib.py Wed Jun 02 16:15:05 2010 +0200
@@ -48,13 +48,13 @@
@property
def email_of(self):
- return self.reverse_use_email and self.reverse_use_email[0]
+ return self.reverse_use_email and self.reverse_use_email[0] or None
@property
def prefered(self):
return self.prefered_form and self.prefered_form[0] or self
- @deprecated('use .prefered')
+ @deprecated('[3.6] use .prefered')
def canonical_form(self):
return self.prefered_form and self.prefered_form[0] or self
@@ -89,14 +89,6 @@
return self.display_address()
return super(EmailAddress, self).printable_value(attr, value, attrtype, format)
- def after_deletion_path(self):
- """return (path, parameters) which should be used as redirect
- information when this entity is being deleted
- """
- if self.email_of:
- return self.email_of.rest_path(), {}
- return super(EmailAddress, self).after_deletion_path()
-
class Bookmark(AnyEntity):
"""customized class for Bookmark entities"""
@@ -133,12 +125,6 @@
except UnknownProperty:
return u''
- def after_deletion_path(self):
- """return (path, parameters) which should be used as redirect
- information when this entity is being deleted
- """
- return 'view', {}
-
class CWCache(AnyEntity):
"""Cache"""
--- a/entities/schemaobjs.py Wed Jun 02 16:13:28 2010 +0200
+++ b/entities/schemaobjs.py Wed Jun 02 16:15:05 2010 +0200
@@ -115,14 +115,6 @@
scard, self.relation_type[0].name, ocard,
self.to_entity[0].name)
- def after_deletion_path(self):
- """return (path, parameters) which should be used as redirect
- information when this entity is being deleted
- """
- if self.relation_type:
- return self.relation_type[0].rest_path(), {}
- return super(CWRelation, self).after_deletion_path()
-
@property
def rtype(self):
return self.relation_type[0]
@@ -139,6 +131,7 @@
rschema = self._cw.vreg.schema.rschema(self.rtype.name)
return rschema.rdefs[(self.stype.name, self.otype.name)]
+
class CWAttribute(CWRelation):
__regid__ = 'CWAttribute'
@@ -160,14 +153,6 @@
def dc_title(self):
return '%s(%s)' % (self.cstrtype[0].name, self.value or u'')
- def after_deletion_path(self):
- """return (path, parameters) which should be used as redirect
- information when this entity is being deleted
- """
- if self.reverse_constrained_by:
- return self.reverse_constrained_by[0].rest_path(), {}
- return super(CWConstraint, self).after_deletion_path()
-
@property
def type(self):
return self.cstrtype[0].name
@@ -201,14 +186,6 @@
def check_expression(self, *args, **kwargs):
return self._rqlexpr().check(*args, **kwargs)
- def after_deletion_path(self):
- """return (path, parameters) which should be used as redirect
- information when this entity is being deleted
- """
- if self.expression_of:
- return self.expression_of.rest_path(), {}
- return super(RQLExpression, self).after_deletion_path()
-
class CWPermission(AnyEntity):
__regid__ = 'CWPermission'
@@ -218,12 +195,3 @@
if self.label:
return '%s (%s)' % (self._cw._(self.name), self.label)
return self._cw._(self.name)
-
- def after_deletion_path(self):
- """return (path, parameters) which should be used as redirect
- information when this entity is being deleted
- """
- permissionof = getattr(self, 'reverse_require_permission', ())
- if len(permissionof) == 1:
- return permissionof[0].rest_path(), {}
- return super(CWPermission, self).after_deletion_path()
--- a/entities/test/unittest_base.py Wed Jun 02 16:13:28 2010 +0200
+++ b/entities/test/unittest_base.py Wed Jun 02 16:15:05 2010 +0200
@@ -27,7 +27,7 @@
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb import ValidationError
-from cubicweb.interfaces import IMileStone, IWorkflowable
+from cubicweb.interfaces import IMileStone, ICalendarable
from cubicweb.entities import AnyEntity
@@ -106,7 +106,7 @@
def test_allowed_massmail_keys(self):
e = self.execute('CWUser U WHERE U login "member"').get_entity(0, 0)
# Bytes/Password attributes should be omited
- self.assertEquals(e.allowed_massmail_keys(),
+ self.assertEquals(e.cw_adapt_to('IEmailable').allowed_massmail_keys(),
set(('surname', 'firstname', 'login', 'last_login_time',
'creation_date', 'modification_date', 'cwuri', 'eid'))
)
@@ -115,8 +115,9 @@
class InterfaceTC(CubicWebTC):
def test_nonregr_subclasses_and_mixins_interfaces(self):
+ from cubicweb.entities.wfobjs import WorkflowableMixIn
+ WorkflowableMixIn.__implements__ = (ICalendarable,)
CWUser = self.vreg['etypes'].etype_class('CWUser')
- self.failUnless(implements(CWUser, IWorkflowable))
class MyUser(CWUser):
__implements__ = (IMileStone,)
self.vreg._loadedmods[__name__] = {}
@@ -126,10 +127,10 @@
# a copy is done systematically
self.failUnless(issubclass(MyUser_, MyUser))
self.failUnless(implements(MyUser_, IMileStone))
- self.failUnless(implements(MyUser_, IWorkflowable))
+ self.failUnless(implements(MyUser_, ICalendarable))
# original class should not have beed modified, only the copy
self.failUnless(implements(MyUser, IMileStone))
- self.failIf(implements(MyUser, IWorkflowable))
+ self.failIf(implements(MyUser, ICalendarable))
class SpecializedEntityClassesTC(CubicWebTC):
--- a/entities/test/unittest_wfobjs.py Wed Jun 02 16:13:28 2010 +0200
+++ b/entities/test/unittest_wfobjs.py Wed Jun 02 16:15:05 2010 +0200
@@ -100,35 +100,38 @@
def test_workflow_base(self):
e = self.create_user('toto')
- self.assertEquals(e.state, 'activated')
- e.change_state('deactivated', u'deactivate 1')
+ iworkflowable = e.cw_adapt_to('IWorkflowable')
+ self.assertEquals(iworkflowable.state, 'activated')
+ iworkflowable.change_state('deactivated', u'deactivate 1')
self.commit()
- e.change_state('activated', u'activate 1')
+ iworkflowable.change_state('activated', u'activate 1')
self.commit()
- e.change_state('deactivated', u'deactivate 2')
+ iworkflowable.change_state('deactivated', u'deactivate 2')
self.commit()
- e.clear_related_cache('wf_info_for', 'object')
+ e.cw_clear_relation_cache('wf_info_for', 'object')
self.assertEquals([tr.comment for tr in e.reverse_wf_info_for],
['deactivate 1', 'activate 1', 'deactivate 2'])
- self.assertEquals(e.latest_trinfo().comment, 'deactivate 2')
+ self.assertEquals(iworkflowable.latest_trinfo().comment, 'deactivate 2')
def test_possible_transitions(self):
user = self.execute('CWUser X').get_entity(0, 0)
- trs = list(user.possible_transitions())
+ iworkflowable = user.cw_adapt_to('IWorkflowable')
+ trs = list(iworkflowable.possible_transitions())
self.assertEquals(len(trs), 1)
self.assertEquals(trs[0].name, u'deactivate')
self.assertEquals(trs[0].destination(None).name, u'deactivated')
# test a std user get no possible transition
cnx = self.login('member')
# fetch the entity using the new session
- trs = list(cnx.user().possible_transitions())
+ trs = list(cnx.user().cw_adapt_to('IWorkflowable').possible_transitions())
self.assertEquals(len(trs), 0)
def _test_manager_deactivate(self, user):
- user.clear_related_cache('in_state', 'subject')
+ iworkflowable = user.cw_adapt_to('IWorkflowable')
+ user.cw_clear_relation_cache('in_state', 'subject')
self.assertEquals(len(user.in_state), 1)
- self.assertEquals(user.state, 'deactivated')
- trinfo = user.latest_trinfo()
+ self.assertEquals(iworkflowable.state, 'deactivated')
+ trinfo = iworkflowable.latest_trinfo()
self.assertEquals(trinfo.previous_state.name, 'activated')
self.assertEquals(trinfo.new_state.name, 'deactivated')
self.assertEquals(trinfo.comment, 'deactivate user')
@@ -137,7 +140,8 @@
def test_change_state(self):
user = self.user()
- user.change_state('deactivated', comment=u'deactivate user')
+ iworkflowable = user.cw_adapt_to('IWorkflowable')
+ iworkflowable.change_state('deactivated', comment=u'deactivate user')
trinfo = self._test_manager_deactivate(user)
self.assertEquals(trinfo.transition, None)
@@ -154,33 +158,36 @@
def test_fire_transition(self):
user = self.user()
- user.fire_transition('deactivate', comment=u'deactivate user')
+ iworkflowable = user.cw_adapt_to('IWorkflowable')
+ iworkflowable.fire_transition('deactivate', comment=u'deactivate user')
user.clear_all_caches()
- self.assertEquals(user.state, 'deactivated')
+ self.assertEquals(iworkflowable.state, 'deactivated')
self._test_manager_deactivate(user)
trinfo = self._test_manager_deactivate(user)
self.assertEquals(trinfo.transition.name, 'deactivate')
def test_goback_transition(self):
- wf = self.session.user.current_workflow
+ wf = self.session.user.cw_adapt_to('IWorkflowable').current_workflow
asleep = wf.add_state('asleep')
- wf.add_transition('rest', (wf.state_by_name('activated'), wf.state_by_name('deactivated')),
- asleep)
+ wf.add_transition('rest', (wf.state_by_name('activated'),
+ wf.state_by_name('deactivated')),
+ asleep)
wf.add_transition('wake up', asleep)
user = self.create_user('stduser')
- user.fire_transition('rest')
+ iworkflowable = user.cw_adapt_to('IWorkflowable')
+ iworkflowable.fire_transition('rest')
self.commit()
- user.fire_transition('wake up')
+ iworkflowable.fire_transition('wake up')
self.commit()
- self.assertEquals(user.state, 'activated')
- user.fire_transition('deactivate')
+ self.assertEquals(iworkflowable.state, 'activated')
+ iworkflowable.fire_transition('deactivate')
self.commit()
- user.fire_transition('rest')
+ iworkflowable.fire_transition('rest')
self.commit()
- user.fire_transition('wake up')
+ iworkflowable.fire_transition('wake up')
self.commit()
user.clear_all_caches()
- self.assertEquals(user.state, 'deactivated')
+ self.assertEquals(iworkflowable.state, 'deactivated')
# XXX test managers can change state without matching transition
@@ -189,18 +196,18 @@
self.create_user('tutu')
cnx = self.login('tutu')
req = self.request()
- member = req.entity_from_eid(self.member.eid)
+ iworkflowable = req.entity_from_eid(self.member.eid).cw_adapt_to('IWorkflowable')
ex = self.assertRaises(ValidationError,
- member.fire_transition, 'deactivate')
+ iworkflowable.fire_transition, 'deactivate')
self.assertEquals(ex.errors, {'by_transition-subject': "transition may not be fired"})
cnx.close()
cnx = self.login('member')
req = self.request()
- member = req.entity_from_eid(self.member.eid)
- member.fire_transition('deactivate')
+ iworkflowable = req.entity_from_eid(self.member.eid).cw_adapt_to('IWorkflowable')
+ iworkflowable.fire_transition('deactivate')
cnx.commit()
ex = self.assertRaises(ValidationError,
- member.fire_transition, 'activate')
+ iworkflowable.fire_transition, 'activate')
self.assertEquals(ex.errors, {'by_transition-subject': "transition may not be fired"})
def test_fire_transition_owned_by(self):
@@ -250,43 +257,44 @@
[(swfstate2, state2), (swfstate3, state3)])
self.assertEquals(swftr1.destination(None).eid, swfstate1.eid)
# workflows built, begin test
- self.group = self.request().create_entity('CWGroup', name=u'grp1')
+ group = self.request().create_entity('CWGroup', name=u'grp1')
self.commit()
- self.assertEquals(self.group.current_state.eid, state1.eid)
- self.assertEquals(self.group.current_workflow.eid, mwf.eid)
- self.assertEquals(self.group.main_workflow.eid, mwf.eid)
- self.assertEquals(self.group.subworkflow_input_transition(), None)
- self.group.fire_transition('swftr1', u'go')
+ iworkflowable = group.cw_adapt_to('IWorkflowable')
+ self.assertEquals(iworkflowable.current_state.eid, state1.eid)
+ self.assertEquals(iworkflowable.current_workflow.eid, mwf.eid)
+ self.assertEquals(iworkflowable.main_workflow.eid, mwf.eid)
+ self.assertEquals(iworkflowable.subworkflow_input_transition(), None)
+ iworkflowable.fire_transition('swftr1', u'go')
self.commit()
- self.group.clear_all_caches()
- self.assertEquals(self.group.current_state.eid, swfstate1.eid)
- self.assertEquals(self.group.current_workflow.eid, swf.eid)
- self.assertEquals(self.group.main_workflow.eid, mwf.eid)
- self.assertEquals(self.group.subworkflow_input_transition().eid, swftr1.eid)
- self.group.fire_transition('tr1', u'go')
+ group.clear_all_caches()
+ self.assertEquals(iworkflowable.current_state.eid, swfstate1.eid)
+ self.assertEquals(iworkflowable.current_workflow.eid, swf.eid)
+ self.assertEquals(iworkflowable.main_workflow.eid, mwf.eid)
+ self.assertEquals(iworkflowable.subworkflow_input_transition().eid, swftr1.eid)
+ iworkflowable.fire_transition('tr1', u'go')
self.commit()
- self.group.clear_all_caches()
- self.assertEquals(self.group.current_state.eid, state2.eid)
- self.assertEquals(self.group.current_workflow.eid, mwf.eid)
- self.assertEquals(self.group.main_workflow.eid, mwf.eid)
- self.assertEquals(self.group.subworkflow_input_transition(), None)
+ group.clear_all_caches()
+ self.assertEquals(iworkflowable.current_state.eid, state2.eid)
+ self.assertEquals(iworkflowable.current_workflow.eid, mwf.eid)
+ self.assertEquals(iworkflowable.main_workflow.eid, mwf.eid)
+ self.assertEquals(iworkflowable.subworkflow_input_transition(), None)
# force back to swfstate1 is impossible since we can't any more find
# subworkflow input transition
ex = self.assertRaises(ValidationError,
- self.group.change_state, swfstate1, u'gadget')
+ iworkflowable.change_state, swfstate1, u'gadget')
self.assertEquals(ex.errors, {'to_state-subject': "state doesn't belong to entity's workflow"})
self.rollback()
# force back to state1
- self.group.change_state('state1', u'gadget')
- self.group.fire_transition('swftr1', u'au')
- self.group.clear_all_caches()
- self.group.fire_transition('tr2', u'chapeau')
+ iworkflowable.change_state('state1', u'gadget')
+ iworkflowable.fire_transition('swftr1', u'au')
+ group.clear_all_caches()
+ iworkflowable.fire_transition('tr2', u'chapeau')
self.commit()
- self.group.clear_all_caches()
- self.assertEquals(self.group.current_state.eid, state3.eid)
- self.assertEquals(self.group.current_workflow.eid, mwf.eid)
- self.assertEquals(self.group.main_workflow.eid, mwf.eid)
- self.assertListEquals(parse_hist(self.group.workflow_history),
+ group.clear_all_caches()
+ self.assertEquals(iworkflowable.current_state.eid, state3.eid)
+ self.assertEquals(iworkflowable.current_workflow.eid, mwf.eid)
+ self.assertEquals(iworkflowable.main_workflow.eid, mwf.eid)
+ self.assertListEquals(parse_hist(iworkflowable.workflow_history),
[('state1', 'swfstate1', 'swftr1', 'go'),
('swfstate1', 'swfstate2', 'tr1', 'go'),
('swfstate2', 'state2', 'swftr1', 'exiting from subworkflow subworkflow'),
@@ -337,8 +345,9 @@
self.commit()
group = self.request().create_entity('CWGroup', name=u'grp1')
self.commit()
+ iworkflowable = group.cw_adapt_to('IWorkflowable')
for trans in ('identify', 'release', 'close'):
- group.fire_transition(trans)
+ iworkflowable.fire_transition(trans)
self.commit()
@@ -362,6 +371,7 @@
self.commit()
group = self.request().create_entity('CWGroup', name=u'grp1')
self.commit()
+ iworkflowable = group.cw_adapt_to('IWorkflowable')
for trans, nextstate in (('identify', 'xsigning'),
('xabort', 'created'),
('identify', 'xsigning'),
@@ -369,10 +379,10 @@
('release', 'xsigning'),
('xabort', 'identified')
):
- group.fire_transition(trans)
+ iworkflowable.fire_transition(trans)
self.commit()
group.clear_all_caches()
- self.assertEquals(group.state, nextstate)
+ self.assertEquals(iworkflowable.state, nextstate)
class CustomWorkflowTC(CubicWebTC):
@@ -389,35 +399,38 @@
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.assertEquals(self.member.state, 'activated')# no change before commit
+ iworkflowable = self.member.cw_adapt_to('IWorkflowable')
+ self.assertEquals(iworkflowable.state, 'activated')# no change before commit
self.commit()
self.member.clear_all_caches()
- self.assertEquals(self.member.current_workflow.eid, wf.eid)
- self.assertEquals(self.member.state, 'asleep')
- self.assertEquals(self.member.workflow_history, ())
+ self.assertEquals(iworkflowable.current_workflow.eid, wf.eid)
+ self.assertEquals(iworkflowable.state, 'asleep')
+ self.assertEquals(iworkflowable.workflow_history, ())
def test_custom_wf_replace_state_keep_history(self):
"""member in inital state with some history, state is redirected and
state change is recorded to history
"""
- self.member.fire_transition('deactivate')
- self.member.fire_transition('activate')
+ iworkflowable = self.member.cw_adapt_to('IWorkflowable')
+ iworkflowable.fire_transition('deactivate')
+ iworkflowable.fire_transition('activate')
wf = add_wf(self, 'CWUser')
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.commit()
self.member.clear_all_caches()
- self.assertEquals(self.member.current_workflow.eid, wf.eid)
- self.assertEquals(self.member.state, 'asleep')
- self.assertEquals(parse_hist(self.member.workflow_history),
+ self.assertEquals(iworkflowable.current_workflow.eid, wf.eid)
+ self.assertEquals(iworkflowable.state, 'asleep')
+ self.assertEquals(parse_hist(iworkflowable.workflow_history),
[('activated', 'deactivated', 'deactivate', None),
('deactivated', 'activated', 'activate', None),
('activated', 'asleep', None, 'workflow changed to "CWUser"')])
def test_custom_wf_no_initial_state(self):
"""try to set a custom workflow which has no initial state"""
- self.member.fire_transition('deactivate')
+ iworkflowable = self.member.cw_adapt_to('IWorkflowable')
+ iworkflowable.fire_transition('deactivate')
wf = add_wf(self, 'CWUser')
wf.add_state('asleep')
self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
@@ -438,7 +451,8 @@
"""member in some state shared by the new workflow, nothing has to be
done
"""
- self.member.fire_transition('deactivate')
+ iworkflowable = self.member.cw_adapt_to('IWorkflowable')
+ iworkflowable.fire_transition('deactivate')
wf = add_wf(self, 'CWUser')
wf.add_state('asleep', initial=True)
self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
@@ -447,12 +461,12 @@
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.assertEquals(self.member.state, 'asleep')# no change before commit
+ self.assertEquals(iworkflowable.state, 'asleep')# no change before commit
self.commit()
self.member.clear_all_caches()
- self.assertEquals(self.member.current_workflow.name, "default user workflow")
- self.assertEquals(self.member.state, 'activated')
- self.assertEquals(parse_hist(self.member.workflow_history),
+ self.assertEquals(iworkflowable.current_workflow.name, "default user workflow")
+ self.assertEquals(iworkflowable.state, 'activated')
+ self.assertEquals(parse_hist(iworkflowable.workflow_history),
[('activated', 'deactivated', 'deactivate', None),
('deactivated', 'asleep', None, 'workflow changed to "CWUser"'),
('asleep', 'activated', None, 'workflow changed to "default user workflow"'),])
@@ -473,28 +487,29 @@
def test_auto_transition_fired(self):
wf = self.setup_custom_wf()
user = self.create_user('member')
+ iworkflowable = user.cw_adapt_to('IWorkflowable')
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()
- self.assertEquals(user.state, 'asleep')
- self.assertEquals([t.name for t in user.possible_transitions()],
+ self.assertEquals(iworkflowable.state, 'asleep')
+ self.assertEquals([t.name for t in iworkflowable.possible_transitions()],
['rest'])
- user.fire_transition('rest')
+ iworkflowable.fire_transition('rest')
self.commit()
user.clear_all_caches()
- self.assertEquals(user.state, 'asleep')
- self.assertEquals([t.name for t in user.possible_transitions()],
+ self.assertEquals(iworkflowable.state, 'asleep')
+ self.assertEquals([t.name for t in iworkflowable.possible_transitions()],
['rest'])
- self.assertEquals(parse_hist(user.workflow_history),
+ self.assertEquals(parse_hist(iworkflowable.workflow_history),
[('asleep', 'asleep', 'rest', None)])
user.set_attributes(surname=u'toto') # fulfill condition
self.commit()
- user.fire_transition('rest')
+ iworkflowable.fire_transition('rest')
self.commit()
user.clear_all_caches()
- self.assertEquals(user.state, 'dead')
- self.assertEquals(parse_hist(user.workflow_history),
+ self.assertEquals(iworkflowable.state, 'dead')
+ self.assertEquals(parse_hist(iworkflowable.workflow_history),
[('asleep', 'asleep', 'rest', None),
('asleep', 'asleep', 'rest', None),
('asleep', 'dead', 'sick', None),])
@@ -505,7 +520,8 @@
self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
{'wf': wf.eid, 'x': user.eid})
self.commit()
- self.assertEquals(user.state, 'dead')
+ iworkflowable = user.cw_adapt_to('IWorkflowable')
+ self.assertEquals(iworkflowable.state, 'dead')
def test_auto_transition_initial_state_fired(self):
wf = self.execute('Any WF WHERE ET default_workflow WF, '
@@ -517,14 +533,15 @@
self.commit()
user = self.create_user('member', surname=u'toto')
self.commit()
- self.assertEquals(user.state, 'dead')
+ iworkflowable = user.cw_adapt_to('IWorkflowable')
+ self.assertEquals(iworkflowable.state, 'dead')
class WorkflowHooksTC(CubicWebTC):
def setUp(self):
CubicWebTC.setUp(self)
- self.wf = self.session.user.current_workflow
+ self.wf = self.session.user.cw_adapt_to('IWorkflowable').current_workflow
self.session.set_pool()
self.s_activated = self.wf.state_by_name('activated').eid
self.s_deactivated = self.wf.state_by_name('deactivated').eid
@@ -572,8 +589,9 @@
def test_transition_checking1(self):
cnx = self.login('stduser')
user = cnx.user(self.session)
+ iworkflowable = user.cw_adapt_to('IWorkflowable')
ex = self.assertRaises(ValidationError,
- user.fire_transition, 'activate')
+ iworkflowable.fire_transition, 'activate')
self.assertEquals(self._cleanup_msg(ex.errors['by_transition-subject']),
u"transition isn't allowed from")
cnx.close()
@@ -581,8 +599,9 @@
def test_transition_checking2(self):
cnx = self.login('stduser')
user = cnx.user(self.session)
+ iworkflowable = user.cw_adapt_to('IWorkflowable')
ex = self.assertRaises(ValidationError,
- user.fire_transition, 'dummy')
+ iworkflowable.fire_transition, 'dummy')
self.assertEquals(self._cleanup_msg(ex.errors['by_transition-subject']),
u"transition isn't allowed from")
cnx.close()
@@ -591,15 +610,16 @@
cnx = self.login('stduser')
session = self.session
user = cnx.user(session)
- user.fire_transition('deactivate')
+ iworkflowable = user.cw_adapt_to('IWorkflowable')
+ iworkflowable.fire_transition('deactivate')
cnx.commit()
session.set_pool()
ex = self.assertRaises(ValidationError,
- user.fire_transition, 'deactivate')
+ iworkflowable.fire_transition, 'deactivate')
self.assertEquals(self._cleanup_msg(ex.errors['by_transition-subject']),
u"transition isn't allowed from")
# get back now
- user.fire_transition('activate')
+ iworkflowable.fire_transition('activate')
cnx.commit()
cnx.close()
--- a/entities/wfobjs.py Wed Jun 02 16:13:28 2010 +0200
+++ b/entities/wfobjs.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,9 +15,13 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""workflow definition and history related entities
+"""workflow handling:
+* entity types defining workflow (Workflow, State, Transition...)
+* workflow history (TrInfo)
+* adapter for workflowable entities (IWorkflowableAdapter)
"""
+
__docformat__ = "restructuredtext en"
from warnings import warn
@@ -27,7 +31,8 @@
from logilab.common.compat import any
from cubicweb.entities import AnyEntity, fetch_config
-from cubicweb.interfaces import IWorkflowable
+from cubicweb.view import EntityAdapter
+from cubicweb.selectors import relation_possible
from cubicweb.mixins import MI_REL_TRIGGERS
class WorkflowException(Exception): pass
@@ -47,15 +52,6 @@
return any(et for et in self.reverse_default_workflow
if et.name == etype)
- # XXX define parent() instead? what if workflow of multiple types?
- def after_deletion_path(self):
- """return (path, parameters) which should be used as redirect
- information when this entity is being deleted
- """
- if self.workflow_of:
- return self.workflow_of[0].rest_path(), {'vid': 'workflow'}
- return super(Workflow, self).after_deletion_path()
-
def iter_workflows(self, _done=None):
"""return an iterator on actual workflows, eg this workflow and its
subworkflows
@@ -177,7 +173,7 @@
{'os': todelstate.eid, 'ns': replacement.eid})
execute('SET X to_state NS WHERE X to_state OS, OS eid %(os)s, NS eid %(ns)s',
{'os': todelstate.eid, 'ns': replacement.eid})
- todelstate.delete()
+ todelstate.cw_delete()
class BaseTransition(AnyEntity):
@@ -226,14 +222,6 @@
return False
return True
- def after_deletion_path(self):
- """return (path, parameters) which should be used as redirect
- information when this entity is being deleted
- """
- if self.transition_of:
- return self.transition_of[0].rest_path(), {}
- return super(BaseTransition, self).after_deletion_path()
-
def set_permissions(self, requiredgroups=(), conditions=(), reset=True):
"""set or add (if `reset` is False) groups and conditions for this
transition
@@ -277,7 +265,7 @@
try:
return self.destination_state[0]
except IndexError:
- return entity.latest_trinfo().previous_state
+ return entity.cw_adapt_to('IWorkflowable').latest_trinfo().previous_state
def potential_destinations(self):
try:
@@ -288,9 +276,6 @@
for previousstate in tr.reverse_allowed_transition:
yield previousstate
- def parent(self):
- return self.workflow
-
class WorkflowTransition(BaseTransition):
"""customized class for WorkflowTransition entities"""
@@ -331,7 +316,7 @@
return None
if tostateeid is None:
# go back to state from which we've entered the subworkflow
- return entity.subworkflow_input_trinfo().previous_state
+ return entity.cw_adapt_to('IWorkflowable').subworkflow_input_trinfo().previous_state
return self._cw.entity_from_eid(tostateeid)
@cached
@@ -358,9 +343,6 @@
def destination(self):
return self.destination_state and self.destination_state[0] or None
- def parent(self):
- return self.reverse_subworkflow_exit[0]
-
class State(AnyEntity):
"""customized class for State entities"""
@@ -371,10 +353,7 @@
@property
def workflow(self):
# take care, may be missing in multi-sources configuration
- return self.state_of and self.state_of[0]
-
- def parent(self):
- return self.workflow
+ return self.state_of and self.state_of[0] or None
class TrInfo(AnyEntity):
@@ -399,22 +378,99 @@
def transition(self):
return self.by_transition and self.by_transition[0] or None
- def parent(self):
- return self.for_entity
-
class WorkflowableMixIn(object):
"""base mixin providing workflow helper methods for workflowable entities.
This mixin will be automatically set on class supporting the 'in_state'
relation (which implies supporting 'wf_info_for' as well)
"""
- __implements__ = (IWorkflowable,)
+
+ @property
+ @deprecated('[3.5] use printable_state')
+ def displayable_state(self):
+ return self._cw._(self.state)
+ @property
+ @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').main_workflow")
+ def main_workflow(self):
+ return self.cw_adapt_to('IWorkflowable').main_workflow
+ @property
+ @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').current_workflow")
+ def current_workflow(self):
+ return self.cw_adapt_to('IWorkflowable').current_workflow
+ @property
+ @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').current_state")
+ def current_state(self):
+ return self.cw_adapt_to('IWorkflowable').current_state
+ @property
+ @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').state")
+ def state(self):
+ return self.cw_adapt_to('IWorkflowable').state
+ @property
+ @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').printable_state")
+ def printable_state(self):
+ return self.cw_adapt_to('IWorkflowable').printable_state
+ @property
+ @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').workflow_history")
+ def workflow_history(self):
+ return self.cw_adapt_to('IWorkflowable').workflow_history
+
+ @deprecated('[3.5] get transition from current workflow and use its may_be_fired method')
+ def can_pass_transition(self, trname):
+ """return the Transition instance if the current user can fire the
+ transition with the given name, else None
+ """
+ tr = self.current_workflow and self.current_workflow.transition_by_name(trname)
+ if tr and tr.may_be_fired(self.eid):
+ return tr
+ @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').cwetype_workflow()")
+ def cwetype_workflow(self):
+ return self.cw_adapt_to('IWorkflowable').main_workflow()
+ @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').latest_trinfo()")
+ def latest_trinfo(self):
+ return self.cw_adapt_to('IWorkflowable').latest_trinfo()
+ @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').possible_transitions()")
+ def possible_transitions(self, type='normal'):
+ return self.cw_adapt_to('IWorkflowable').possible_transitions(type)
+ @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').fire_transition()")
+ def fire_transition(self, tr, comment=None, commentformat=None):
+ return self.cw_adapt_to('IWorkflowable').fire_transition(tr, comment, commentformat)
+ @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').change_state()")
+ def change_state(self, statename, comment=None, commentformat=None, tr=None):
+ return self.cw_adapt_to('IWorkflowable').change_state(statename, comment, commentformat, tr)
+ @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').subworkflow_input_trinfo()")
+ def subworkflow_input_trinfo(self):
+ return self.cw_adapt_to('IWorkflowable').subworkflow_input_trinfo()
+ @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').subworkflow_input_transition()")
+ def subworkflow_input_transition(self):
+ return self.cw_adapt_to('IWorkflowable').subworkflow_input_transition()
+
+
+MI_REL_TRIGGERS[('in_state', 'subject')] = WorkflowableMixIn
+
+
+
+class IWorkflowableAdapter(WorkflowableMixIn, EntityAdapter):
+ """base adapter providing workflow helper methods for workflowable entities.
+ """
+ __regid__ = 'IWorkflowable'
+ __select__ = relation_possible('in_state')
+
+ @cached
+ def cwetype_workflow(self):
+ """return the default workflow for entities of this type"""
+ # XXX CWEType method
+ wfrset = self._cw.execute('Any WF WHERE ET default_workflow WF, '
+ 'ET name %(et)s', {'et': self.entity.__regid__})
+ if wfrset:
+ return wfrset.get_entity(0, 0)
+ self.warning("can't find any workflow for %s", self.entity.__regid__)
+ return None
@property
def main_workflow(self):
"""return current workflow applied to this entity"""
- if self.custom_workflow:
- return self.custom_workflow[0]
+ if self.entity.custom_workflow:
+ return self.entity.custom_workflow[0]
return self.cwetype_workflow()
@property
@@ -425,14 +481,14 @@
@property
def current_state(self):
"""return current state entity"""
- return self.in_state and self.in_state[0] or None
+ return self.entity.in_state and self.entity.in_state[0] or None
@property
def state(self):
"""return current state name"""
try:
- return self.in_state[0].name
- except IndexError:
+ return self.current_state.name
+ except AttributeError:
self.warning('entity %s has no state', self)
return None
@@ -449,26 +505,15 @@
"""return the workflow history for this entity (eg ordered list of
TrInfo entities)
"""
- return self.reverse_wf_info_for
+ return self.entity.reverse_wf_info_for
def latest_trinfo(self):
"""return the latest transition information for this entity"""
try:
- return self.reverse_wf_info_for[-1]
+ return self.workflow_history[-1]
except IndexError:
return None
- @cached
- def cwetype_workflow(self):
- """return the default workflow for entities of this type"""
- # XXX CWEType method
- wfrset = self._cw.execute('Any WF WHERE ET default_workflow WF, '
- 'ET name %(et)s', {'et': self.__regid__})
- if wfrset:
- return wfrset.get_entity(0, 0)
- self.warning("can't find any workflow for %s", self.__regid__)
- return None
-
def possible_transitions(self, type='normal'):
"""generates transition that MAY be fired for the given entity,
expected to be in this state
@@ -483,16 +528,44 @@
{'x': self.current_state.eid, 'type': type,
'wfeid': self.current_workflow.eid})
for tr in rset.entities():
- if tr.may_be_fired(self.eid):
+ if tr.may_be_fired(self.entity.eid):
yield tr
+ def subworkflow_input_trinfo(self):
+ """return the TrInfo which has be recorded when this entity went into
+ the current sub-workflow
+ """
+ if self.main_workflow.eid == self.current_workflow.eid:
+ return # doesn't make sense
+ subwfentries = []
+ for trinfo in self.workflow_history:
+ if (trinfo.transition and
+ trinfo.previous_state.workflow.eid != trinfo.new_state.workflow.eid):
+ # entering or leaving a subworkflow
+ if (subwfentries and
+ subwfentries[-1].new_state.workflow.eid == trinfo.previous_state.workflow.eid and
+ subwfentries[-1].previous_state.workflow.eid == trinfo.new_state.workflow.eid):
+ # leave
+ del subwfentries[-1]
+ else:
+ # enter
+ subwfentries.append(trinfo)
+ if not subwfentries:
+ return None
+ return subwfentries[-1]
+
+ def subworkflow_input_transition(self):
+ """return the transition which has went through the current sub-workflow
+ """
+ return getattr(self.subworkflow_input_trinfo(), 'transition', None)
+
def _add_trinfo(self, comment, commentformat, treid=None, tseid=None):
kwargs = {}
if comment is not None:
kwargs['comment'] = comment
if commentformat is not None:
kwargs['comment_format'] = commentformat
- kwargs['wf_info_for'] = self
+ kwargs['wf_info_for'] = self.entity
if treid is not None:
kwargs['by_transition'] = self._cw.entity_from_eid(treid)
if tseid is not None:
@@ -532,51 +605,3 @@
stateeid = state.eid
# XXX try to find matching transition?
return self._add_trinfo(comment, commentformat, tr and tr.eid, stateeid)
-
- def subworkflow_input_trinfo(self):
- """return the TrInfo which has be recorded when this entity went into
- the current sub-workflow
- """
- if self.main_workflow.eid == self.current_workflow.eid:
- return # doesn't make sense
- subwfentries = []
- for trinfo in self.workflow_history:
- if (trinfo.transition and
- trinfo.previous_state.workflow.eid != trinfo.new_state.workflow.eid):
- # entering or leaving a subworkflow
- if (subwfentries and
- subwfentries[-1].new_state.workflow.eid == trinfo.previous_state.workflow.eid and
- subwfentries[-1].previous_state.workflow.eid == trinfo.new_state.workflow.eid):
- # leave
- del subwfentries[-1]
- else:
- # enter
- subwfentries.append(trinfo)
- if not subwfentries:
- return None
- return subwfentries[-1]
-
- def subworkflow_input_transition(self):
- """return the transition which has went through the current sub-workflow
- """
- return getattr(self.subworkflow_input_trinfo(), 'transition', None)
-
- def clear_all_caches(self):
- super(WorkflowableMixIn, self).clear_all_caches()
- clear_cache(self, 'cwetype_workflow')
-
- @deprecated('[3.5] get transition from current workflow and use its may_be_fired method')
- def can_pass_transition(self, trname):
- """return the Transition instance if the current user can fire the
- transition with the given name, else None
- """
- tr = self.current_workflow and self.current_workflow.transition_by_name(trname)
- if tr and tr.may_be_fired(self.eid):
- return tr
-
- @property
- @deprecated('[3.5] use printable_state')
- def displayable_state(self):
- return self._cw._(self.state)
-
-MI_REL_TRIGGERS[('in_state', 'subject')] = WorkflowableMixIn
--- a/entity.py Wed Jun 02 16:13:28 2010 +0200
+++ b/entity.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,16 +15,15 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""Base class for entity objects manipulated in clients
+"""Base class for entity objects manipulated in clients"""
-"""
__docformat__ = "restructuredtext en"
from warnings import warn
from logilab.common import interface
-from logilab.common.compat import all
from logilab.common.decorators import cached
+from logilab.common.deprecation import deprecated
from logilab.mtconverter import TransformData, TransformError, xml_escape
from rql.utils import rqlvar_maker
@@ -107,10 +106,10 @@
if not interface.implements(cls, iface):
interface.extend(cls, iface)
if role == 'subject':
- setattr(cls, rschema.type, SubjectRelation(rschema))
+ attr = rschema.type
else:
attr = 'reverse_%s' % rschema.type
- setattr(cls, attr, ObjectRelation(rschema))
+ setattr(cls, attr, Relation(rschema, role))
if mixins:
# see etype class instantation in cwvreg.ETypeRegistry.etype_class method:
# due to class dumping, cls is the generated top level class with actual
@@ -125,6 +124,24 @@
cls.__bases__ = tuple(mixins)
cls.info('plugged %s mixins on %s', mixins, cls)
+ fetch_attrs = ('modification_date',)
+ @classmethod
+ def fetch_order(cls, attr, var):
+ """class method used to control sort order when multiple entities of
+ this type are fetched
+ """
+ return cls.fetch_unrelated_order(attr, var)
+
+ @classmethod
+ def fetch_unrelated_order(cls, attr, var):
+ """class method used to control sort order when multiple entities of
+ this type are fetched to use in edition (eg propose them to create a
+ new relation on an edited entity).
+ """
+ if attr == 'modification_date':
+ return '%s DESC' % var
+ return None
+
@classmethod
def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
settype=True, ordermethod='fetch_order'):
@@ -271,12 +288,12 @@
def __init__(self, req, rset=None, row=None, col=0):
AppObject.__init__(self, req, rset=rset, row=row, col=col)
dict.__init__(self)
- self._related_cache = {}
+ self._cw_related_cache = {}
if rset is not None:
self.eid = rset[row][col]
else:
self.eid = None
- self._is_saved = True
+ self._cw_is_saved = True
def __repr__(self):
return '<Entity %s %s %s at %s>' % (
@@ -328,7 +345,7 @@
if hasattr(self, 'edited_attributes') and \
attr not in self.edited_attributes:
self.edited_attributes.add(attr)
- self.skip_security_attributes.add(attr)
+ self._cw_skip_security_attributes.add(attr)
def __delitem__(self, attr):
"""override __delitem__ to update self.edited_attributes on cleanup of
@@ -358,7 +375,7 @@
if hasattr(self, 'edited_attributes') and \
attr not in self.edited_attributes:
self.edited_attributes.add(attr)
- self.skip_security_attributes.add(attr)
+ self._cw_skip_security_attributes.add(attr)
def pop(self, attr, default=_marker):
"""override pop to update self.edited_attributes on cleanup of
@@ -378,27 +395,24 @@
for attr, value in values.items():
self[attr] = value # use self.__setitem__ implementation
- def rql_set_value(self, attr, value):
- """call by rql execution plan when some attribute is modified
-
- don't use dict api in such case since we don't want attribute to be
- added to skip_security_attributes.
- """
- super(Entity, self).__setitem__(attr, value)
+ def cw_adapt_to(self, interface):
+ """return an adapter the entity to the given interface name.
- def pre_add_hook(self):
- """hook called by the repository before doing anything to add the entity
- (before_add entity hooks have not been called yet). This give the
- occasion to do weird stuff such as autocast (File -> Image for instance).
-
- This method must return the actual entity to be added.
+ return None if it can not be adapted.
"""
- return self
+ try:
+ cache = self._cw_adapters_cache
+ except AttributeError:
+ self._cw_adapters_cache = cache = {}
+ try:
+ return cache[interface]
+ except KeyError:
+ adapter = self._cw.vreg['adapters'].select_or_none(
+ interface, self._cw, entity=self)
+ cache[interface] = adapter
+ return adapter
- def set_eid(self, eid):
- self.eid = eid
-
- def has_eid(self):
+ def has_eid(self): # XXX cw_has_eid
"""return True if the entity has an attributed eid (False
meaning that the entity has to be created
"""
@@ -408,38 +422,34 @@
except (ValueError, TypeError):
return False
- def is_saved(self):
+ def cw_is_saved(self):
"""during entity creation, there is some time during which the entity
- has an eid attributed though it's not saved (eg during before_add_entity
- hooks). You can use this method to ensure the entity has an eid *and* is
- saved in its source.
+ has an eid attributed though it's not saved (eg during
+ 'before_add_entity' hooks). You can use this method to ensure the entity
+ has an eid *and* is saved in its source.
"""
- return self.has_eid() and self._is_saved
+ return self.has_eid() and self._cw_is_saved
@cached
- def metainformation(self):
+ def cw_metainformation(self):
res = dict(zip(('type', 'source', 'extid'), self._cw.describe(self.eid)))
res['source'] = self._cw.source_defs()[res['source']]
return res
- def clear_local_perm_cache(self, action):
- for rqlexpr in self.e_schema.get_rqlexprs(action):
- self._cw.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None)
-
- def check_perm(self, action):
+ def cw_check_perm(self, action):
self.e_schema.check_perm(self._cw, action, eid=self.eid)
- def has_perm(self, action):
+ def cw_has_perm(self, action):
return self.e_schema.has_perm(self._cw, action, eid=self.eid)
- def view(self, __vid, __registry='views', w=None, **kwargs):
+ def view(self, __vid, __registry='views', w=None, **kwargs): # XXX cw_view
"""shortcut to apply a view on this entity"""
view = self._cw.vreg[__registry].select(__vid, self._cw, rset=self.cw_rset,
row=self.cw_row, col=self.cw_col,
**kwargs)
return view.render(row=self.cw_row, col=self.cw_col, w=w, **kwargs)
- def absolute_url(self, *args, **kwargs):
+ def absolute_url(self, *args, **kwargs): # XXX cw_url
"""return an absolute url to view this entity"""
# use *args since we don't want first argument to be "anonymous" to
# avoid potential clash with kwargs
@@ -452,7 +462,7 @@
# the object for use in the relation is tricky
# XXX search_state is web specific
if getattr(self._cw, 'search_state', ('normal',))[0] == 'normal':
- kwargs['base_url'] = self.metainformation()['source'].get('base-url')
+ kwargs['base_url'] = self.cw_metainformation()['source'].get('base-url')
if method in (None, 'view'):
try:
kwargs['_restpath'] = self.rest_path(kwargs.get('base_url'))
@@ -464,7 +474,7 @@
kwargs['rql'] = 'Any X WHERE X eid %s' % self.eid
return self._cw.build_url(method, **kwargs)
- def rest_path(self, use_ext_eid=False):
+ def rest_path(self, use_ext_eid=False): # XXX cw_rest_path
"""returns a REST-like (relative) path for this entity"""
mainattr, needcheck = self._rest_attr_info()
etype = str(self.e_schema)
@@ -487,12 +497,12 @@
path += '/eid'
if mainattr == 'eid':
if use_ext_eid:
- value = self.metainformation()['extid']
+ value = self.cw_metainformation()['extid']
else:
value = self.eid
return '%s/%s' % (path, self._cw.url_quote(value))
- def attr_metadata(self, attr, metadata):
+ def cw_attr_metadata(self, attr, metadata):
"""return a metadata for an attribute (None if unspecified)"""
value = getattr(self, '%s_%s' % (attr, metadata), None)
if value is None and metadata == 'encoding':
@@ -500,7 +510,7 @@
return value
def printable_value(self, attr, value=_marker, attrtype=None,
- format='text/html', displaytime=True):
+ format='text/html', displaytime=True): # XXX cw_printable_value
"""return a displayable value (i.e. unicode string) which may contains
html tags
"""
@@ -519,16 +529,16 @@
# description...
if props.internationalizable:
value = self._cw._(value)
- attrformat = self.attr_metadata(attr, 'format')
+ attrformat = self.cw_attr_metadata(attr, 'format')
if attrformat:
- return self.mtc_transform(value, attrformat, format,
- self._cw.encoding)
+ return self._cw_mtc_transform(value, attrformat, format,
+ self._cw.encoding)
elif attrtype == 'Bytes':
- attrformat = self.attr_metadata(attr, 'format')
+ attrformat = self.cw_attr_metadata(attr, 'format')
if attrformat:
- encoding = self.attr_metadata(attr, 'encoding')
- return self.mtc_transform(value.getvalue(), attrformat, format,
- encoding)
+ encoding = self.cw_attr_metadata(attr, 'encoding')
+ return self._cw_mtc_transform(value.getvalue(), attrformat, format,
+ encoding)
return u''
value = printable_value(self._cw, attrtype, value, props,
displaytime=displaytime)
@@ -536,8 +546,8 @@
value = xml_escape(value)
return value
- def mtc_transform(self, data, format, target_format, encoding,
- _engine=ENGINE):
+ def _cw_mtc_transform(self, data, format, target_format, encoding,
+ _engine=ENGINE):
trdata = TransformData(data, format, encoding, appobject=self)
data = _engine.convert(trdata, target_format).decode()
if format == 'text/html':
@@ -546,7 +556,7 @@
# entity cloning ##########################################################
- def copy_relations(self, ceid):
+ def copy_relations(self, ceid): # XXX cw_copy_relations
"""copy relations of the object with the given eid on this
object (this method is called on the newly created copy, and
ceid designates the original entity).
@@ -575,7 +585,7 @@
rql = 'SET X %s V WHERE X eid %%(x)s, Y eid %%(y)s, Y %s V' % (
rschema.type, rschema.type)
execute(rql, {'x': self.eid, 'y': ceid})
- self.clear_related_cache(rschema.type, 'subject')
+ self.cw_clear_relation_cache(rschema.type, 'subject')
for rschema in self.e_schema.object_relations():
if rschema.meta:
continue
@@ -593,19 +603,19 @@
rql = 'SET V %s X WHERE X eid %%(x)s, Y eid %%(y)s, V %s Y' % (
rschema.type, rschema.type)
execute(rql, {'x': self.eid, 'y': ceid})
- self.clear_related_cache(rschema.type, 'object')
+ self.cw_clear_relation_cache(rschema.type, 'object')
# data fetching methods ###################################################
@cached
- def as_rset(self):
+ def as_rset(self): # XXX .cw_as_rset
"""returns a resultset containing `self` information"""
rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s',
{'x': self.eid}, [(self.__regid__,)])
rset.req = self._cw
return rset
- def to_complete_relations(self):
+ def _cw_to_complete_relations(self):
"""by default complete final relations to when calling .complete()"""
for rschema in self.e_schema.subject_relations():
if rschema.final:
@@ -622,7 +632,7 @@
all(matching_groups(e.get_groups('read')) for e in targets):
yield rschema, 'subject'
- def to_complete_attributes(self, skip_bytes=True, skip_pwd=True):
+ def _cw_to_complete_attributes(self, skip_bytes=True, skip_pwd=True):
for rschema, attrschema in self.e_schema.attribute_definitions():
# skip binary data by default
if skip_bytes and attrschema.type == 'Bytes':
@@ -639,7 +649,7 @@
yield attr
_cw_completed = False
- def complete(self, attributes=None, skip_bytes=True, skip_pwd=True):
+ def complete(self, attributes=None, skip_bytes=True, skip_pwd=True): # XXX cw_complete
"""complete this entity by adding missing attributes (i.e. query the
repository to fill the entity)
@@ -656,7 +666,7 @@
V = varmaker.next()
rql = ['WHERE %s eid %%(x)s' % V]
selected = []
- for attr in (attributes or self.to_complete_attributes(skip_bytes, skip_pwd)):
+ for attr in (attributes or self._cw_to_complete_attributes(skip_bytes, skip_pwd)):
# if attribute already in entity, nothing to do
if self.has_key(attr):
continue
@@ -668,9 +678,9 @@
lastattr = len(selected) + 1
if attributes is None:
# fetch additional relations (restricted to 0..1 relations)
- for rschema, role in self.to_complete_relations():
+ for rschema, role in self._cw_to_complete_relations():
rtype = rschema.type
- if self.relation_cached(rtype, role):
+ if self.cw_relation_cached(rtype, role):
continue
var = varmaker.next()
targettype = rschema.targets(self.e_schema, role)[0]
@@ -707,9 +717,9 @@
rrset.req = self._cw
else:
rrset = self._cw.eid_rset(value)
- self.set_related_cache(rtype, role, rrset)
+ self.cw_set_relation_cache(rtype, role, rrset)
- def get_value(self, name):
+ def cw_attr_value(self, name):
"""get value for the attribute relation <name>, query the repository
to get the value if necessary.
@@ -719,7 +729,7 @@
try:
value = self[name]
except KeyError:
- if not self.is_saved():
+ if not self.cw_is_saved():
return None
rql = "Any A WHERE X eid %%(x)s, X %s A" % name
try:
@@ -741,7 +751,7 @@
self[name] = value = None
return value
- def related(self, rtype, role='subject', limit=None, entities=False):
+ def related(self, rtype, role='subject', limit=None, entities=False): # XXX .cw_related
"""returns a resultset of related entities
:param role: is the role played by 'self' in the relation ('subject' or 'object')
@@ -749,16 +759,16 @@
:param entities: if True, the entites are returned; if False, a result set is returned
"""
try:
- return self.related_cache(rtype, role, entities, limit)
+ return self._cw_relation_cache(rtype, role, entities, limit)
except KeyError:
pass
assert self.has_eid()
- rql = self.related_rql(rtype, role)
+ rql = self.cw_related_rql(rtype, role)
rset = self._cw.execute(rql, {'x': self.eid})
- self.set_related_cache(rtype, role, rset)
+ self.cw_set_relation_cache(rtype, role, rset)
return self.related(rtype, role, limit, entities)
- def related_rql(self, rtype, role='subject', targettypes=None):
+ def cw_related_rql(self, rtype, role='subject', targettypes=None):
rschema = self._cw.vreg.schema[rtype]
if role == 'subject':
restriction = 'E eid %%(x)s, E %s X' % rtype
@@ -807,7 +817,7 @@
# generic vocabulary methods ##############################################
- def unrelated_rql(self, rtype, targettype, role, ordermethod=None,
+ def cw_unrelated_rql(self, rtype, targettype, role, ordermethod=None,
vocabconstraints=True):
"""build a rql to fetch `targettype` entities unrelated to this entity
using (rtype, role) relation.
@@ -869,12 +879,12 @@
return rql, args
def unrelated(self, rtype, targettype, role='subject', limit=None,
- ordermethod=None):
+ ordermethod=None): # XXX .cw_unrelated
"""return a result set of target type objects that may be related
by a given relation, with self as subject or object
"""
try:
- rql, args = self.unrelated_rql(rtype, targettype, role, ordermethod)
+ rql, args = self.cw_unrelated_rql(rtype, targettype, role, ordermethod)
except Unauthorized:
return self._cw.empty_rset()
if limit is not None:
@@ -882,18 +892,19 @@
rql = '%s LIMIT %s WHERE %s' % (before, limit, after)
return self._cw.execute(rql, args)
- # relations cache handling ################################################
+ # relations cache handling #################################################
- def relation_cached(self, rtype, role):
- """return true if the given relation is already cached on the instance
+ def cw_relation_cached(self, rtype, role):
+ """return None if the given relation isn't already cached on the
+ instance, else the content of the cache (a 2-uple (rset, entities)).
"""
- return self._related_cache.get('%s_%s' % (rtype, role))
+ return self._cw_related_cache.get('%s_%s' % (rtype, role))
- def related_cache(self, rtype, role, entities=True, limit=None):
+ def _cw_relation_cache(self, rtype, role, entities=True, limit=None):
"""return values for the given relation if it's cached on the instance,
else raise `KeyError`
"""
- res = self._related_cache['%s_%s' % (rtype, role)][entities]
+ res = self._cw_related_cache['%s_%s' % (rtype, role)][entities]
if limit is not None and limit < len(res):
if entities:
res = res[:limit]
@@ -901,10 +912,10 @@
res = res.limit(limit)
return res
- def set_related_cache(self, rtype, role, rset, col=0):
+ def cw_set_relation_cache(self, rtype, role, rset):
"""set cached values for the given relation"""
if rset:
- related = list(rset.entities(col))
+ related = list(rset.entities(0))
rschema = self._cw.vreg.schema.rschema(rtype)
if role == 'subject':
rcard = rschema.rdef(self.e_schema, related[0].e_schema).cardinality[1]
@@ -914,23 +925,24 @@
target = 'subject'
if rcard in '?1':
for rentity in related:
- rentity._related_cache['%s_%s' % (rtype, target)] = (
+ rentity._cw_related_cache['%s_%s' % (rtype, target)] = (
self.as_rset(), (self,))
else:
related = ()
- self._related_cache['%s_%s' % (rtype, role)] = (rset, related)
+ self._cw_related_cache['%s_%s' % (rtype, role)] = (rset, related)
- def clear_related_cache(self, rtype=None, role=None):
+ def cw_clear_relation_cache(self, rtype=None, role=None):
"""clear cached values for the given relation or the entire cache if
no relation is given
"""
if rtype is None:
- self._related_cache = {}
+ self._cw_related_cache = {}
+ self._cw_adapters_cache = {}
else:
assert role
- self._related_cache.pop('%s_%s' % (rtype, role), None)
+ self._cw_related_cache.pop('%s_%s' % (rtype, role), None)
- def clear_all_caches(self):
+ def clear_all_caches(self): # XXX cw_clear_all_caches
"""flush all caches on this entity. Further attributes/relations access
will triggers new database queries to get back values.
@@ -942,8 +954,7 @@
self._cw_completed = False
self.clear()
# clear relations cache
- for rschema, _, role in self.e_schema.relation_definitions():
- self.clear_related_cache(rschema.type, role)
+ self.cw_clear_relation_cache()
# rest path unique cache
try:
del self.__unique
@@ -952,10 +963,10 @@
# raw edition utilities ###################################################
- def set_attributes(self, **kwargs):
+ def set_attributes(self, **kwargs): # XXX cw_set_attributes
_check_cw_unsafe(kwargs)
assert kwargs
- assert self._is_saved, "should not call set_attributes while entity "\
+ assert self.cw_is_saved(), "should not call set_attributes while entity "\
"hasn't been saved yet"
relations = []
for key in kwargs:
@@ -970,7 +981,7 @@
# edited_attributes / skip_security_attributes machinery
self.update(kwargs)
- def set_relations(self, **kwargs):
+ def set_relations(self, **kwargs): # XXX cw_set_relations
"""add relations to the given object. To set a relation where this entity
is the object of the relation, use 'reverse_'<relation> as argument name.
@@ -994,28 +1005,42 @@
restr, ','.join(str(r.eid) for r in values)),
{'x': self.eid})
- def delete(self, **kwargs):
+ def cw_delete(self, **kwargs):
assert self.has_eid(), self.eid
self._cw.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema,
{'x': self.eid}, **kwargs)
# server side utilities ###################################################
+ def _cw_rql_set_value(self, attr, value):
+ """call by rql execution plan when some attribute is modified
+
+ don't use dict api in such case since we don't want attribute to be
+ added to skip_security_attributes.
+
+ This method is for internal use, you should not use it.
+ """
+ super(Entity, self).__setitem__(attr, value)
+
+ def _cw_clear_local_perm_cache(self, action):
+ for rqlexpr in self.e_schema.get_rqlexprs(action):
+ self._cw.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None)
+
@property
- def skip_security_attributes(self):
+ def _cw_skip_security_attributes(self):
try:
- return self._skip_security_attributes
+ return self.__cw_skip_security_attributes
except:
- self._skip_security_attributes = set()
- return self._skip_security_attributes
+ self.__cw_skip_security_attributes = set()
+ return self.__cw_skip_security_attributes
- def set_defaults(self):
+ def _cw_set_defaults(self):
"""set default values according to the schema"""
for attr, value in self.e_schema.defaults():
if not self.has_key(attr):
self[str(attr)] = value
- def check(self, creation=False):
+ def _cw_check(self, creation=False):
"""check this entity against its schema. Only final relation
are checked here, constraint on actual relations are checked in hooks
"""
@@ -1038,61 +1063,29 @@
self.e_schema.check(self, creation=creation, _=_,
relations=relations)
- def fti_containers(self, _done=None):
- if _done is None:
- _done = set()
- _done.add(self.eid)
- containers = tuple(self.e_schema.fulltext_containers())
- if containers:
- for rschema, target in containers:
- if target == 'object':
- targets = getattr(self, rschema.type)
- else:
- targets = getattr(self, 'reverse_%s' % rschema)
- for entity in targets:
- if entity.eid in _done:
- continue
- for container in entity.fti_containers(_done):
- yield container
- yielded = True
- else:
- yield self
+ @deprecated('[3.9] use entity.cw_attr_value(attr)')
+ def get_value(self, name):
+ return self.cw_attr_value(name)
- def get_words(self):
- """used by the full text indexer to get words to index
+ @deprecated('[3.9] use entity.cw_delete()')
+ def delete(self, **kwargs):
+ return self.cw_delete(**kwargs)
- this method should only be used on the repository side since it depends
- on the logilab.database package
+ @deprecated('[3.9] use entity.cw_attr_metadata(attr, metadata)')
+ def attr_metadata(self, attr, metadata):
+ return self.cw_attr_metadata(attr, metadata)
- :rtype: list
- :return: the list of indexable word of this entity
- """
- from logilab.database.fti import tokenize
- # take care to cases where we're modyfying the schema
- pending = self._cw.transaction_data.setdefault('pendingrdefs', set())
- words = []
- for rschema in self.e_schema.indexable_attributes():
- if (self.e_schema, rschema) in pending:
- continue
- try:
- value = self.printable_value(rschema, format='text/plain')
- except TransformError:
- continue
- except:
- self.exception("can't add value of %s to text index for entity %s",
- rschema, self.eid)
- continue
- if value:
- words += tokenize(value)
- for rschema, role in self.e_schema.fulltext_relations():
- if role == 'subject':
- for entity in getattr(self, rschema.type):
- words += entity.get_words()
- else: # if role == 'object':
- for entity in getattr(self, 'reverse_%s' % rschema.type):
- words += entity.get_words()
- return words
+ @deprecated('[3.9] use entity.cw_has_perm(action)')
+ def has_perm(self, action):
+ return self.cw_has_perm(action)
+ @deprecated('[3.9] use entity.cw_set_relation_cache(rtype, role, rset)')
+ def set_related_cache(self, rtype, role, rset):
+ self.cw_set_relation_cache(rtype, role, rset)
+
+ @deprecated('[3.9] use entity.cw_clear_relation_cache(rtype, role, rset)')
+ def clear_related_cache(self, rtype=None, role=None):
+ self.cw_clear_relation_cache(rtype, role)
# attribute and relation descriptors ##########################################
@@ -1106,18 +1099,18 @@
def __get__(self, eobj, eclass):
if eobj is None:
return self
- return eobj.get_value(self._attrname)
+ return eobj.cw_attr_value(self._attrname)
def __set__(self, eobj, value):
eobj[self._attrname] = value
+
class Relation(object):
"""descriptor that controls schema relation access"""
- _role = None # for pylint
- def __init__(self, rschema):
- self._rschema = rschema
+ def __init__(self, rschema, role):
self._rtype = rschema.type
+ self._role = role
def __get__(self, eobj, eclass):
if eobj is None:
@@ -1129,14 +1122,6 @@
raise NotImplementedError
-class SubjectRelation(Relation):
- """descriptor that controls schema relation access"""
- _role = 'subject'
-
-class ObjectRelation(Relation):
- """descriptor that controls schema relation access"""
- _role = 'object'
-
from logging import getLogger
from cubicweb import set_log_methods
set_log_methods(Entity, getLogger('cubicweb.entity'))
--- a/etwist/request.py Wed Jun 02 16:13:28 2010 +0200
+++ b/etwist/request.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,9 +15,8 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""Twisted request handler for CubicWeb
+"""Twisted request handler for CubicWeb"""
-"""
__docformat__ = "restructuredtext en"
from datetime import datetime
@@ -55,9 +54,9 @@
return self._twreq.method
def relative_path(self, includeparams=True):
- """return the normalized path of the request (ie at least relative
- to the instance's root, but some other normalization may be needed
- so that the returned path may be used to compare to generated urls
+ """return the normalized path of the request (ie at least relative to
+ the instance's root, but some other normalization may be needed so that
+ the returned path may be used to compare to generated urls
:param includeparams:
boolean indicating if GET form parameters should be kept in the path
@@ -68,8 +67,8 @@
return path
def get_header(self, header, default=None, raw=True):
- """return the value associated with the given input header,
- raise KeyError if the header is not set
+ """return the value associated with the given input header, raise
+ KeyError if the header is not set
"""
if raw:
return self._headers_in.getRawHeaders(header, [default])[0]
--- a/etwist/server.py Wed Jun 02 16:13:28 2010 +0200
+++ b/etwist/server.py Wed Jun 02 16:15:05 2010 +0200
@@ -99,12 +99,11 @@
class CubicWebRootResource(resource.Resource):
- def __init__(self, config, debug=None):
- self.debugmode = debug
+ def __init__(self, config):
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, debug=self.debugmode)
+ self.appli = CubicWebPublisher(config)
self.base_url = config['base-url']
self.https_url = config['https-url']
self.children = {}
@@ -156,6 +155,9 @@
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
@@ -165,7 +167,7 @@
if directory == 'static':
return File(self.config.static_directory)
if directory == 'fckeditor':
- return File(self.config.ext_resources['FCKEDITOR_PATH'])
+ 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
@@ -173,7 +175,7 @@
else:
cls = File
if path == 'fckeditor':
- return cls(self.config.ext_resources['FCKEDITOR_PATH'])
+ return cls(uiprops['FCKEDITOR_PATH'])
if path == directory: # recurse
return self
datadir = self.config.locate_resource(path)
@@ -187,7 +189,10 @@
def render(self, request):
"""Render a page from the root resource"""
# reload modified files in debug mode
- if self.debugmode:
+ if self.config.debugmode:
+ self.config.uiprops.reload_if_needed()
+ if self.https_url:
+ self.config.https_uiprops.reload_if_needed()
self.appli.vreg.reload_if_needed()
if self.config['profile']: # default profiler don't trace threads
return self.render_request(request)
@@ -382,15 +387,15 @@
LOGGER = getLogger('cubicweb.twisted')
set_log_methods(CubicWebRootResource, LOGGER)
-def run(config, debug):
+def run(config):
# create the site
- root_resource = CubicWebRootResource(config, debug)
+ root_resource = CubicWebRootResource(config)
website = server.Site(root_resource)
# serve it via standard HTTP on port set in the configuration
port = config['port'] or 8080
reactor.listenTCP(port, website)
logger = getLogger('cubicweb.twisted')
- if not debug:
+ if not config.debugmode:
if sys.platform == 'win32':
raise ConfigurationError("Under windows, you must use the service management "
"commands (e.g : 'net start my_instance)'")
--- a/etwist/twctl.py Wed Jun 02 16:13:28 2010 +0200
+++ b/etwist/twctl.py Wed Jun 02 16:15:05 2010 +0200
@@ -32,9 +32,9 @@
cmdname = 'start'
cfgname = 'twisted'
- def start_server(self, config, debug):
+ def start_server(self, config):
from cubicweb.etwist import server
- server.run(config, debug)
+ server.run(config)
class TWStopHandler(CommandHandler):
cmdname = 'stop'
--- a/goa/appobjects/components.py Wed Jun 02 16:13:28 2010 +0200
+++ b/goa/appobjects/components.py Wed Jun 02 16:15:05 2010 +0200
@@ -98,7 +98,7 @@
def sendmail(self, recipient, subject, body):
sender = '%s <%s>' % (
self.req.user.dc_title() or self.config['sender-name'],
- self.req.user.get_email() or self.config['sender-addr'])
+ self.req.user.cw_adapt_to('IEmailable').get_email() or self.config['sender-addr'])
mail.send_mail(sender=sender, to=recipient,
subject=subject, body=body)
--- a/goa/db.py Wed Jun 02 16:13:28 2010 +0200
+++ b/goa/db.py Wed Jun 02 16:15:05 2010 +0200
@@ -233,7 +233,7 @@
return self.req.datastore_get(self.eid)
except AttributeError: # self.req is not a server session
return Get(self.eid)
- self.set_defaults()
+ self._cw_set_defaults()
values = self._to_gae_dict(convert=False)
parent = key_name = _app = None
if self._gaeinitargs is not None:
@@ -343,7 +343,7 @@
self.req = req
dbmodel = self.to_gae_model()
key = Put(dbmodel)
- self.set_eid(str(key))
+ self.eid = str(key)
if self.req is not None and self.rset is None:
self.rset = rset_from_objs(self.req, dbmodel, ('eid',),
'Any X WHERE X eid %(x)s', {'x': self.eid})
@@ -409,7 +409,7 @@
def dynamic_properties(self):
raise NotImplementedError('use eschema')
- def is_saved(self):
+ def cw_is_saved(self):
return self.has_eid()
def parent(self):
--- a/goa/gaesource.py Wed Jun 02 16:13:28 2010 +0200
+++ b/goa/gaesource.py Wed Jun 02 16:15:05 2010 +0200
@@ -49,15 +49,15 @@
except KeyError:
pass
else:
- entity.clear_related_cache(rtype, role)
+ entity.cw_clear_relation_cache(rtype, role)
if gaesubject.kind() == 'CWUser':
for asession in session.repo._sessions.itervalues():
if asession.user.eid == subject:
- asession.user.clear_related_cache(rtype, 'subject')
+ asession.user.cw_clear_relation_cache(rtype, 'subject')
if gaeobject.kind() == 'CWUser':
for asession in session.repo._sessions.itervalues():
if asession.user.eid == object:
- asession.user.clear_related_cache(rtype, 'object')
+ asession.user.cw_clear_relation_cache(rtype, 'object')
def _mark_modified(session, gaeentity):
modified = session.transaction_data.setdefault('modifiedentities', {})
--- a/goa/skel/loader.py Wed Jun 02 16:13:28 2010 +0200
+++ b/goa/skel/loader.py Wed Jun 02 16:15:05 2010 +0200
@@ -30,7 +30,7 @@
# apply monkey patches first
goa.do_monkey_patch()
# get instance's configuration (will be loaded from app.conf file)
- GAEConfiguration.ext_resources['JAVASCRIPTS'].append('DATADIR/goa.js')
+ GAEConfiguration.uiprops['JAVASCRIPTS'].append('DATADIR/goa.js')
config = GAEConfiguration('toto', APPLROOT)
# create default groups
create_groups()
--- a/goa/skel/main.py Wed Jun 02 16:13:28 2010 +0200
+++ b/goa/skel/main.py Wed Jun 02 16:15:05 2010 +0200
@@ -31,7 +31,7 @@
# get instance's configuration (will be loaded from app.conf file)
from cubicweb.goa.goaconfig import GAEConfiguration
-GAEConfiguration.ext_resources['JAVASCRIPTS'].append('DATADIR/goa.js')
+GAEConfiguration.uiprops['JAVASCRIPTS'].append('DATADIR/goa.js')
config = GAEConfiguration('toto', APPLROOT)
# dynamic objects registry
--- a/goa/test/unittest_rql.py Wed Jun 02 16:13:28 2010 +0200
+++ b/goa/test/unittest_rql.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,9 +15,6 @@
#
# 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 cubicweb.goa.testlib import *
from cubicweb import Binary
@@ -612,7 +609,7 @@
def test_error_unknown_eid(self):
rset = self.req.execute('Any X WHERE X eid %(x)s', {'x': '1234'})
self.assertEquals(len(rset), 0)
- self.blog.delete()
+ self.blog.cw_delete()
rset = self.req.execute('Any X WHERE X eid %(x)s', {'x': self.blog.eid})
self.assertEquals(len(rset), 0)
--- a/goa/tools/laxctl.py Wed Jun 02 16:13:28 2010 +0200
+++ b/goa/tools/laxctl.py Wed Jun 02 16:15:05 2010 +0200
@@ -43,7 +43,7 @@
do_monkey_patch()
from cubicweb.goa.goavreg import GAEVregistry
from cubicweb.goa.goaconfig import GAEConfiguration
- #WebConfiguration.ext_resources['JAVASCRIPTS'].append('DATADIR/goa.js')
+ #WebConfiguration.uiprops['JAVASCRIPTS'].append('DATADIR/goa.js')
config = GAEConfiguration('toto', applroot)
vreg = GAEVregistry(config)
vreg.set_schema(config.load_schema())
--- a/hooks/bookmark.py Wed Jun 02 16:13:28 2010 +0200
+++ b/hooks/bookmark.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,9 +15,8 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""bookmark related hooks
+"""bookmark related hooks"""
-"""
__docformat__ = "restructuredtext en"
from cubicweb.server import hook
@@ -28,7 +27,7 @@
def precommit_event(self):
if not self.session.deleted_in_transaction(self.bookmark.eid):
if not self.bookmark.bookmarked_by:
- self.bookmark.delete()
+ self.bookmark.cw_delete()
class DelBookmarkedByHook(hook.Hook):
--- a/hooks/security.py Wed Jun 02 16:13:28 2010 +0200
+++ b/hooks/security.py Wed Jun 02 16:15:05 2010 +0200
@@ -29,9 +29,9 @@
def check_entity_attributes(session, entity, editedattrs=None):
eid = entity.eid
eschema = entity.e_schema
- # .skip_security_attributes is there to bypass security for attributes
+ # ._cw_skip_security_attributes is there to bypass security for attributes
# set by hooks by modifying the entity's dictionnary
- dontcheck = entity.skip_security_attributes
+ dontcheck = entity._cw_skip_security_attributes
if editedattrs is None:
try:
editedattrs = entity.edited_attributes
@@ -57,7 +57,7 @@
for values in session.transaction_data.pop('check_entity_perm_op'):
entity = session.entity_from_eid(values[0])
action = values[1]
- entity.check_perm(action)
+ entity.cw_check_perm(action)
check_entity_attributes(session, entity, values[2:])
def commit_event(self):
@@ -105,10 +105,10 @@
def __call__(self):
try:
# check user has permission right now, if not retry at commit time
- self.entity.check_perm('update')
+ self.entity.cw_check_perm('update')
check_entity_attributes(self._cw, self.entity)
except Unauthorized:
- self.entity.clear_local_perm_cache('update')
+ self.entity._cw_clear_local_perm_cache('update')
# save back editedattrs in case the entity is reedited later in the
# same transaction, which will lead to edited_attributes being
# overwritten
@@ -122,7 +122,7 @@
events = ('before_delete_entity',)
def __call__(self):
- self.entity.check_perm('delete')
+ self.entity.cw_check_perm('delete')
class BeforeAddRelationSecurityHook(SecurityHook):
--- a/hooks/syncschema.py Wed Jun 02 16:13:28 2010 +0200
+++ b/hooks/syncschema.py Wed Jun 02 16:15:05 2010 +0200
@@ -34,7 +34,8 @@
from cubicweb import ValidationError
from cubicweb.selectors import implements
-from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, CONSTRAINTS, display_name
+from cubicweb.schema import (META_RTYPES, VIRTUAL_RTYPES, CONSTRAINTS,
+ ETYPE_NAME_MAP, display_name)
from cubicweb.server import hook, schemaserial as ss
from cubicweb.server.sqlutils import SQL_PREFIX
@@ -815,9 +816,10 @@
if name in CORE_ETYPES:
raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
# delete every entities of this type
- self._cw.execute('DELETE %s X' % name)
+ if not name in ETYPE_NAME_MAP:
+ self._cw.execute('DELETE %s X' % name)
+ MemSchemaCWETypeDel(self._cw, name)
DropTable(self._cw, table=SQL_PREFIX + name)
- MemSchemaCWETypeDel(self._cw, name)
class AfterDelCWETypeHook(DelCWETypeHook):
@@ -982,7 +984,11 @@
def __call__(self):
session = self._cw
- rdef = session.vreg.schema.schema_by_eid(self.eidfrom)
+ try:
+ rdef = session.vreg.schema.schema_by_eid(self.eidfrom)
+ except KeyError:
+ self.critical('cant get schema rdef associated to %s', self.eidfrom)
+ return
subjschema, rschema, objschema = rdef.as_triple()
pendings = session.transaction_data.get('pendingeids', ())
pendingrdefs = session.transaction_data.setdefault('pendingrdefs', set())
@@ -1003,7 +1009,6 @@
# we have to update physical schema systematically for final and inlined
# relations, but only if it's the last instance for this relation type
# for other relations
-
if (rschema.final or rschema.inlined):
rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, '
'R eid %%(x)s, X from_entity E, E name %%(name)s'
@@ -1175,7 +1180,7 @@
still_fti = list(schema[etype].indexable_attributes())
for entity in rset.entities():
source.fti_unindex_entity(session, entity.eid)
- for container in entity.fti_containers():
+ for container in entity.cw_adapt_to('IFTIndexable').fti_containers():
if still_fti or container is not entity:
source.fti_unindex_entity(session, entity.eid)
source.fti_index_entity(session, container)
--- a/hooks/workflow.py Wed Jun 02 16:13:28 2010 +0200
+++ b/hooks/workflow.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,9 +15,8 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""Core hooks: workflow related hooks
+"""Core hooks: workflow related hooks"""
-"""
__docformat__ = "restructuredtext en"
from datetime import datetime
@@ -25,8 +24,7 @@
from yams.schema import role_name
from cubicweb import RepositoryError, ValidationError
-from cubicweb.interfaces import IWorkflowable
-from cubicweb.selectors import implements
+from cubicweb.selectors import implements, adaptable
from cubicweb.server import hook
@@ -51,11 +49,12 @@
def precommit_event(self):
session = self.session
entity = self.entity
+ iworkflowable = entity.cw_adapt_to('IWorkflowable')
# if there is an initial state and the entity's state is not set,
# use the initial state as a default state
if not (session.deleted_in_transaction(entity.eid) or entity.in_state) \
- and entity.current_workflow:
- state = entity.current_workflow.initial
+ and iworkflowable.current_workflow:
+ state = iworkflowable.current_workflow.initial
if state:
session.add_relation(entity.eid, 'in_state', state.eid)
_FireAutotransitionOp(session, entity=entity)
@@ -65,10 +64,11 @@
def precommit_event(self):
entity = self.entity
- autotrs = list(entity.possible_transitions('auto'))
+ iworkflowable = entity.cw_adapt_to('IWorkflowable')
+ autotrs = list(iworkflowable.possible_transitions('auto'))
if autotrs:
assert len(autotrs) == 1
- entity.fire_transition(autotrs[0])
+ iworkflowable.fire_transition(autotrs[0])
class _WorkflowChangedOp(hook.Operation):
@@ -82,29 +82,30 @@
if self.eid in pendingeids:
return
entity = session.entity_from_eid(self.eid)
+ iworkflowable = entity.cw_adapt_to('IWorkflowable')
# check custom workflow has not been rechanged to another one in the same
# transaction
- mainwf = entity.main_workflow
+ mainwf = iworkflowable.main_workflow
if mainwf.eid == self.wfeid:
deststate = mainwf.initial
if not deststate:
qname = role_name('custom_workflow', 'subject')
msg = session._('workflow has no initial state')
raise ValidationError(entity.eid, {qname: msg})
- if mainwf.state_by_eid(entity.current_state.eid):
+ if mainwf.state_by_eid(iworkflowable.current_state.eid):
# nothing to do
return
# if there are no history, simply go to new workflow's initial state
- if not entity.workflow_history:
- if entity.current_state.eid != deststate.eid:
+ if not iworkflowable.workflow_history:
+ if iworkflowable.current_state.eid != deststate.eid:
_change_state(session, entity.eid,
- entity.current_state.eid, deststate.eid)
+ iworkflowable.current_state.eid, deststate.eid)
_FireAutotransitionOp(session, entity=entity)
return
msg = session._('workflow changed to "%s"')
msg %= session._(mainwf.name)
session.transaction_data[(entity.eid, 'customwf')] = self.wfeid
- entity.change_state(deststate, msg, u'text/plain')
+ iworkflowable.change_state(deststate, msg, u'text/plain')
class _CheckTrExitPoint(hook.Operation):
@@ -125,9 +126,10 @@
def precommit_event(self):
session = self.session
forentity = self.forentity
+ iworkflowable = forentity.cw_adapt_to('IWorkflowable')
trinfo = self.trinfo
# we're in a subworkflow, check if we've reached an exit point
- wftr = forentity.subworkflow_input_transition()
+ wftr = iworkflowable.subworkflow_input_transition()
if wftr is None:
# inconsistency detected
qname = role_name('to_state', 'subject')
@@ -137,9 +139,9 @@
if tostate is not None:
# reached an exit point
msg = session._('exiting from subworkflow %s')
- msg %= session._(forentity.current_workflow.name)
+ msg %= session._(iworkflowable.current_workflow.name)
session.transaction_data[(forentity.eid, 'subwfentrytr')] = True
- forentity.change_state(tostate, msg, u'text/plain', tr=wftr)
+ iworkflowable.change_state(tostate, msg, u'text/plain', tr=wftr)
# hooks ########################################################################
@@ -151,7 +153,7 @@
class SetInitialStateHook(WorkflowHook):
__regid__ = 'wfsetinitial'
- __select__ = WorkflowHook.__select__ & implements(IWorkflowable)
+ __select__ = WorkflowHook.__select__ & adaptable('IWorkflowable')
events = ('after_add_entity',)
def __call__(self):
@@ -189,18 +191,19 @@
msg = session._('mandatory relation')
raise ValidationError(entity.eid, {qname: msg})
forentity = session.entity_from_eid(foreid)
+ iworkflowable = forentity.cw_adapt_to('IWorkflowable')
# then check it has a workflow set, unless we're in the process of changing
# entity's workflow
if session.transaction_data.get((forentity.eid, 'customwf')):
wfeid = session.transaction_data[(forentity.eid, 'customwf')]
wf = session.entity_from_eid(wfeid)
else:
- wf = forentity.current_workflow
+ wf = iworkflowable.current_workflow
if wf is None:
msg = session._('related entity has no workflow set')
raise ValidationError(entity.eid, {None: msg})
# then check it has a state set
- fromstate = forentity.current_state
+ fromstate = iworkflowable.current_state
if fromstate is None:
msg = session._('related entity has no state')
raise ValidationError(entity.eid, {None: msg})
@@ -278,8 +281,9 @@
_change_state(self._cw, trinfo['wf_info_for'],
trinfo['from_state'], trinfo['to_state'])
forentity = self._cw.entity_from_eid(trinfo['wf_info_for'])
- assert forentity.current_state.eid == trinfo['to_state']
- if forentity.main_workflow.eid != forentity.current_workflow.eid:
+ iworkflowable = forentity.cw_adapt_to('IWorkflowable')
+ assert iworkflowable.current_state.eid == trinfo['to_state']
+ if iworkflowable.main_workflow.eid != iworkflowable.current_workflow.eid:
_SubWorkflowExitOp(self._cw, forentity=forentity, trinfo=trinfo)
@@ -297,7 +301,8 @@
# state changed through TrInfo insertion, so we already know it's ok
return
entity = session.entity_from_eid(self.eidfrom)
- mainwf = entity.main_workflow
+ iworkflowable = entity.cw_adapt_to('IWorkflowable')
+ mainwf = iworkflowable.main_workflow
if mainwf is None:
msg = session._('entity has no workflow set')
raise ValidationError(entity.eid, {None: msg})
@@ -309,7 +314,7 @@
msg = session._("state doesn't belong to entity's workflow. You may "
"want to set a custom workflow for this entity first.")
raise ValidationError(self.eidfrom, {qname: msg})
- if entity.current_workflow and wf.eid != entity.current_workflow.eid:
+ if iworkflowable.current_workflow and wf.eid != iworkflowable.current_workflow.eid:
qname = role_name('in_state', 'subject')
msg = session._("state doesn't belong to entity's current workflow")
raise ValidationError(self.eidfrom, {qname: msg})
@@ -359,7 +364,7 @@
def __call__(self):
entity = self._cw.entity_from_eid(self.eidfrom)
- typewf = entity.cwetype_workflow()
+ typewf = entity.cw_adapt_to('IWorkflowable').cwetype_workflow()
if typewf is not None:
_WorkflowChangedOp(self._cw, eid=self.eidfrom, wfeid=typewf.eid)
--- a/i18n/fr.po Wed Jun 02 16:13:28 2010 +0200
+++ b/i18n/fr.po Wed Jun 02 16:15:05 2010 +0200
@@ -534,7 +534,7 @@
msgstr "Nouvelle transition workflow"
msgid "No result matching query"
-msgstr "aucun résultat"
+msgstr "Aucun résultat ne correspond à la requête"
msgid "Non exhaustive list of views that may apply to entities of this type"
msgstr "Liste non exhausite des vues s'appliquant à ce type d'entité"
--- a/interfaces.py Wed Jun 02 16:13:28 2010 +0200
+++ b/interfaces.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,68 +15,24 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""
-Standard interfaces.
+"""Standard interfaces. Deprecated in favor of adapters.
.. note::
- The `implements` selector matches not only entity classes but also
- their interfaces. Writing __select__ = implements('IGeocodable') is
- a perfectly fine thing to do.
+ The `implements` selector used to match not only entity classes but also their
+ interfaces. This will disappear in a future version. You should define an
+ adapter for that interface and use `adaptable('MyIFace')` selector on appobjects
+ that require that interface.
+
"""
__docformat__ = "restructuredtext en"
from logilab.common.interface import Interface
-class IEmailable(Interface):
- """interface for emailable entities"""
- def get_email(self):
- """return email address"""
-
- @classmethod
- def allowed_massmail_keys(cls):
- """returns a set of allowed email substitution keys
-
- The default is to return the entity's attribute list but an
- entity class might override this method to allow extra keys.
- For instance, the Person class might want to return a `companyname`
- key.
- """
-
- def as_email_context(self):
- """returns the dictionary as used by the sendmail controller to
- build email bodies.
-
- NOTE: the dictionary keys should match the list returned by the
- `allowed_massmail_keys` method.
- """
-
-
-class IWorkflowable(Interface):
- """interface for entities dealing with a specific workflow"""
- # XXX to be completed, see cw.entities.wfobjs.WorkflowableMixIn
-
- @property
- def state(self):
- """return current state name"""
-
- def change_state(self, stateeid, trcomment=None, trcommentformat=None):
- """change the entity's state to the state of the given name in entity's
- workflow
- """
-
- def latest_trinfo(self):
- """return the latest transition information for this entity
- """
-
-
+# XXX deprecates in favor of IProgressAdapter
class IProgress(Interface):
- """something that has a cost, a state and a progression
-
- Take a look at cubicweb.mixins.ProgressMixIn for some
- default implementations
- """
+ """something that has a cost, a state and a progression"""
@property
def cost(self):
@@ -112,7 +68,7 @@
def progress(self):
"""returns the % progress of the task item"""
-
+# XXX deprecates in favor of IMileStoneAdapter
class IMileStone(IProgress):
"""represents an ITask's item"""
@@ -135,7 +91,132 @@
def contractors(self):
"""returns the list of persons supposed to work on this task"""
+# XXX deprecates in favor of IEmbedableAdapter
+class IEmbedable(Interface):
+ """interface for embedable entities"""
+ def embeded_url(self):
+ """embed action interface"""
+
+# XXX deprecates in favor of ICalendarAdapter
+class ICalendarViews(Interface):
+ """calendar views interface"""
+ def matching_dates(self, begin, end):
+ """
+ :param begin: day considered as begin of the range (`DateTime`)
+ :param end: day considered as end of the range (`DateTime`)
+
+ :return:
+ a list of dates (`DateTime`) in the range [`begin`, `end`] on which
+ this entity apply
+ """
+
+# XXX deprecates in favor of ICalendarableAdapter
+class ICalendarable(Interface):
+ """interface for items that do have a begin date 'start' and an end date 'stop'
+ """
+
+ @property
+ def start(self):
+ """return start date"""
+
+ @property
+ def stop(self):
+ """return stop state"""
+
+# XXX deprecates in favor of ICalendarableAdapter
+class ITimetableViews(Interface):
+ """timetable views interface"""
+ def timetable_date(self):
+ """XXX explain
+
+ :return: date (`DateTime`)
+ """
+
+# XXX deprecates in favor of IGeocodableAdapter
+class IGeocodable(Interface):
+ """interface required by geocoding views such as gmap-view"""
+
+ @property
+ def latitude(self):
+ """returns the latitude of the entity"""
+
+ @property
+ def longitude(self):
+ """returns the longitude of the entity"""
+
+ def marker_icon(self):
+ """returns the icon that should be used as the marker"""
+
+# XXX deprecates in favor of ISIOCItemAdapter
+class ISiocItem(Interface):
+ """interface for entities which may be represented as an ISIOC item"""
+
+ def isioc_content(self):
+ """return item's content"""
+
+ def isioc_container(self):
+ """return container entity"""
+
+ def isioc_type(self):
+ """return container type (post, BlogPost, MailMessage)"""
+
+ def isioc_replies(self):
+ """return replies items"""
+
+ def isioc_topics(self):
+ """return topics items"""
+
+# XXX deprecates in favor of ISIOCContainerAdapter
+class ISiocContainer(Interface):
+ """interface for entities which may be represented as an ISIOC container"""
+
+ def isioc_type(self):
+ """return container type (forum, Weblog, MailingList)"""
+
+ def isioc_items(self):
+ """return contained items"""
+
+# XXX deprecates in favor of IEmailableAdapter
+class IFeed(Interface):
+ """interface for entities with rss flux"""
+
+ def rss_feed_url(self):
+ """"""
+
+# XXX deprecates in favor of IDownloadableAdapter
+class IDownloadable(Interface):
+ """interface for downloadable entities"""
+
+ def download_url(self): # XXX not really part of this interface
+ """return an url to download entity's content"""
+ def download_content_type(self):
+ """return MIME type of the downloadable content"""
+ def download_encoding(self):
+ """return encoding of the downloadable content"""
+ def download_file_name(self):
+ """return file name of the downloadable content"""
+ def download_data(self):
+ """return actual data of the downloadable content"""
+
+# XXX deprecates in favor of IPrevNextAdapter
+class IPrevNext(Interface):
+ """interface for entities which can be linked to a previous and/or next
+ entity
+ """
+
+ def next_entity(self):
+ """return the 'next' entity"""
+ def previous_entity(self):
+ """return the 'previous' entity"""
+
+# XXX deprecates in favor of IBreadCrumbsAdapter
+class IBreadCrumbs(Interface):
+
+ def breadcrumbs(self, view, recurs=False):
+ pass
+
+# XXX deprecates in favor of ITreeAdapter
class ITree(Interface):
def parent(self):
@@ -159,141 +240,3 @@
def root(self):
"""returns the root object"""
-
-## web specific interfaces ####################################################
-
-
-class IPrevNext(Interface):
- """interface for entities which can be linked to a previous and/or next
- entity
- """
-
- def next_entity(self):
- """return the 'next' entity"""
- def previous_entity(self):
- """return the 'previous' entity"""
-
-
-class IBreadCrumbs(Interface):
- """interface for entities which can be "located" on some path"""
-
- # XXX fix recurs !
- def breadcrumbs(self, view, recurs=False):
- """return a list containing some:
-
- * tuple (url, label)
- * entity
- * simple label string
-
- defining path from a root to the current view
-
- the main view is given as argument so breadcrumbs may vary according
- to displayed view (may be None). When recursing on a parent entity,
- the `recurs` argument should be set to True.
- """
-
-
-class IDownloadable(Interface):
- """interface for downloadable entities"""
-
- def download_url(self): # XXX not really part of this interface
- """return an url to download entity's content"""
- def download_content_type(self):
- """return MIME type of the downloadable content"""
- def download_encoding(self):
- """return encoding of the downloadable content"""
- def download_file_name(self):
- """return file name of the downloadable content"""
- def download_data(self):
- """return actual data of the downloadable content"""
-
-
-class IEmbedable(Interface):
- """interface for embedable entities"""
-
- def embeded_url(self):
- """embed action interface"""
-
-class ICalendarable(Interface):
- """interface for items that do have a begin date 'start' and an end date 'stop'
- """
-
- @property
- def start(self):
- """return start date"""
-
- @property
- def stop(self):
- """return stop state"""
-
-class ICalendarViews(Interface):
- """calendar views interface"""
- def matching_dates(self, begin, end):
- """
- :param begin: day considered as begin of the range (`DateTime`)
- :param end: day considered as end of the range (`DateTime`)
-
- :return:
- a list of dates (`DateTime`) in the range [`begin`, `end`] on which
- this entity apply
- """
-
-class ITimetableViews(Interface):
- """timetable views interface"""
- def timetable_date(self):
- """XXX explain
-
- :return: date (`DateTime`)
- """
-
-class IGeocodable(Interface):
- """interface required by geocoding views such as gmap-view"""
-
- @property
- def latitude(self):
- """returns the latitude of the entity"""
-
- @property
- def longitude(self):
- """returns the longitude of the entity"""
-
- def marker_icon(self):
- """returns the icon that should be used as the marker
- (returns None for default)
- """
-
-class IFeed(Interface):
- """interface for entities with rss flux"""
-
- def rss_feed_url(self):
- """return an url which layout sub-entities item
- """
-
-class ISiocItem(Interface):
- """interface for entities (which are item
- in sioc specification) with sioc views"""
-
- def isioc_content(self):
- """return content entity"""
-
- def isioc_container(self):
- """return container entity"""
-
- def isioc_type(self):
- """return container type (post, BlogPost, MailMessage)"""
-
- def isioc_replies(self):
- """return replies items"""
-
- def isioc_topics(self):
- """return topics items"""
-
-class ISiocContainer(Interface):
- """interface for entities (which are container
- in sioc specification) with sioc views"""
-
- def isioc_type(self):
- """return container type (forum, Weblog, MailingList)"""
-
- def isioc_items(self):
- """return contained items"""
--- a/mail.py Wed Jun 02 16:13:28 2010 +0200
+++ b/mail.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,9 +15,8 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""Common utilies to format / semd emails.
+"""Common utilies to format / semd emails."""
-"""
__docformat__ = "restructuredtext en"
from base64 import b64encode, b64decode
@@ -185,7 +184,7 @@
# previous email
if not self.msgid_timestamp:
refs = [self.construct_message_id(eid)
- for eid in entity.notification_references(self)]
+ for eid in entity.cw_adapt_to('INotifiable').notification_references(self)]
else:
refs = ()
msgid = self.construct_message_id(entity.eid)
@@ -199,7 +198,7 @@
if isinstance(something, Entity):
# hi-jack self._cw to get a session for the returned user
self._cw = self._cw.hijack_user(something)
- emailaddr = something.get_email()
+ emailaddr = something.cw_adapt_to('IEmailable').get_email()
else:
emailaddr, lang = something
self._cw.set_language(lang)
@@ -247,7 +246,8 @@
# email generation helpers #################################################
def construct_message_id(self, eid):
- return construct_message_id(self._cw.vreg.config.appid, eid, self.msgid_timestamp)
+ return construct_message_id(self._cw.vreg.config.appid, eid,
+ self.msgid_timestamp)
def format_field(self, attr, value):
return ':%(attr)s: %(value)s' % {'attr': attr, 'value': value}
--- a/migration.py Wed Jun 02 16:13:28 2010 +0200
+++ b/migration.py Wed Jun 02 16:15:05 2010 +0200
@@ -111,7 +111,7 @@
self.config = config
if config:
# no config on shell to a remote instance
- self.config.init_log(logthreshold=logging.ERROR, debug=True)
+ self.config.init_log(logthreshold=logging.ERROR)
# 0: no confirmation, 1: only main commands confirmed, 2 ask for everything
self.verbosity = verbosity
self.need_wrap = True
@@ -281,14 +281,25 @@
return context
def cmd_process_script(self, migrscript, funcname=None, *args, **kwargs):
- """execute a migration script
- in interactive mode, display the migration script path, ask for
- confirmation and execute it if confirmed
+ """execute a migration script in interactive mode
+
+ Display the migration script path, ask for confirmation and execute it
+ if confirmed
+
+ Context environment can have these variables defined:
+ - __name__ : will be determine by funcname parameter
+ - __file__ : is the name of the script if it exists
+ - __args__ : script arguments coming from command-line
+
+ :param migrscript: name of the script
+ :param funcname: defines __name__ inside the shell (or use __main__)
+ :params args: optional arguments for funcname
+ :keyword scriptargs: optional arguments of the script
"""
migrscript = os.path.normpath(migrscript)
if migrscript.endswith('.py'):
script_mode = 'python'
- elif migrscript.endswith('.txt') or migrscript.endswith('.rst'):
+ elif migrscript.endswith(('.txt', '.rst')):
script_mode = 'doctest'
else:
raise Exception('This is not a valid cubicweb shell input')
@@ -300,7 +311,8 @@
pyname = '__main__'
else:
pyname = splitext(basename(migrscript))[0]
- scriptlocals.update({'__file__': migrscript, '__name__': pyname})
+ scriptlocals.update({'__file__': migrscript, '__name__': pyname,
+ '__args__': kwargs.pop("scriptargs", [])})
execfile(migrscript, scriptlocals)
if funcname is not None:
try:
--- a/mixins.py Wed Jun 02 16:13:28 2010 +0200
+++ b/mixins.py Wed Jun 02 16:15:05 2010 +0200
@@ -21,9 +21,10 @@
from itertools import chain
from logilab.common.decorators import cached
+from logilab.common.deprecation import deprecated, class_deprecated
from cubicweb.selectors import implements
-from cubicweb.interfaces import IEmailable, ITree
+from cubicweb.interfaces import ITree
class TreeMixIn(object):
@@ -33,6 +34,9 @@
tree_attribute, parent_target and children_target class attribute to
benefit from this default implementation
"""
+ __metaclass__ = class_deprecated
+ __deprecation_warning__ = '[3.9] TreeMixIn is deprecated, use/override ITreeAdapter instead'
+
tree_attribute = None
# XXX misnamed
parent_target = 'subject'
@@ -117,16 +121,6 @@
return chain([self], _uptoroot(self))
return _uptoroot(self)
- def notification_references(self, view):
- """used to control References field of email send on notification
- for this entity. `view` is the notification view.
-
- Should return a list of eids which can be used to generate message ids
- of previously sent email
- """
- return self.path()[:-1]
-
-
## ITree interface ########################################################
def parent(self):
"""return the parent entity if any, else None (e.g. if we are on the
@@ -151,7 +145,7 @@
entities=entities)
def children_rql(self):
- return self.related_rql(self.tree_attribute, self.children_target)
+ return self.cw_related_rql(self.tree_attribute, self.children_target)
def is_leaf(self):
return len(self.children()) == 0
@@ -171,8 +165,7 @@
NOTE: The default implementation is based on the
primary_email / use_email scheme
"""
- __implements__ = (IEmailable,)
-
+ @deprecated("[3.9] use entity.cw_adapt_to('IEmailable').get_email()")
def get_email(self):
if getattr(self, 'primary_email', None):
return self.primary_email[0].address
@@ -180,28 +173,6 @@
return self.use_email[0].address
return None
- @classmethod
- def allowed_massmail_keys(cls):
- """returns a set of allowed email substitution keys
-
- The default is to return the entity's attribute list but an
- entity class might override this method to allow extra keys.
- For instance, the Person class might want to return a `companyname`
- key.
- """
- return set(rschema.type
- for rschema, attrtype in cls.e_schema.attribute_definitions()
- if attrtype.type not in ('Password', 'Bytes'))
-
- def as_email_context(self):
- """returns the dictionary as used by the sendmail controller to
- build email bodies.
-
- NOTE: the dictionary keys should match the list returned by the
- `allowed_massmail_keys` method.
- """
- return dict( (attr, getattr(self, attr)) for attr in self.allowed_massmail_keys() )
-
"""pluggable mixins system: plug classes registered in MI_REL_TRIGGERS on entity
classes which have the relation described by the dict's key.
@@ -215,7 +186,7 @@
}
-
+# XXX move to cubicweb.web.views.treeview once we delete usage from this file
def _done_init(done, view, row, col):
"""handle an infinite recursion safety belt"""
if done is None:
@@ -233,9 +204,12 @@
class TreeViewMixIn(object):
"""a recursive tree view"""
+ __metaclass__ = class_deprecated
+ __deprecation_warning__ = '[3.9] TreeViewMixIn is deprecated, use/override BaseTreeView instead'
+
__regid__ = 'tree'
+ __select__ = implements(ITree)
item_vid = 'treeitem'
- __select__ = implements(ITree)
def call(self, done=None, **kwargs):
if done is None:
@@ -262,6 +236,8 @@
class TreePathMixIn(object):
"""a recursive path view"""
+ __metaclass__ = class_deprecated
+ __deprecation_warning__ = '[3.9] TreePathMixIn is deprecated, use/override TreePathView instead'
__regid__ = 'path'
item_vid = 'oneline'
separator = u' > '
@@ -286,6 +262,8 @@
class ProgressMixIn(object):
"""provide a default implementations for IProgress interface methods"""
+ __metaclass__ = class_deprecated
+ __deprecation_warning__ = '[3.9] ProgressMixIn is deprecated, use/override IProgressAdapter instead'
@property
def cost(self):
--- a/mttransforms.py Wed Jun 02 16:13:28 2010 +0200
+++ b/mttransforms.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,9 +15,8 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""mime type transformation engine for cubicweb, based on mtconverter
+"""mime type transformation engine for cubicweb, based on mtconverter"""
-"""
__docformat__ = "restructuredtext en"
from logilab import mtconverter
--- a/req.py Wed Jun 02 16:13:28 2010 +0200
+++ b/req.py Wed Jun 02 16:15:05 2010 +0200
@@ -279,7 +279,7 @@
user = self.user
userinfo['login'] = user.login
userinfo['name'] = user.name()
- userinfo['email'] = user.get_email()
+ userinfo['email'] = user.cw_adapt_to('IEmailable').get_email()
return userinfo
def is_internal_session(self):
@@ -371,11 +371,11 @@
raise ValueError(self._('can\'t parse %(value)r (expected %(format)s)')
% {'value': value, 'format': format})
- # abstract methods to override according to the web front-end #############
-
def base_url(self):
"""return the root url of the instance"""
- raise NotImplementedError
+ return self.vreg.config['base-url']
+
+ # abstract methods to override according to the web front-end #############
def describe(self, eid):
"""return a tuple (type, sourceuri, extid) for the entity with id <eid>"""
--- a/rset.py Wed Jun 02 16:13:28 2010 +0200
+++ b/rset.py Wed Jun 02 16:15:05 2010 +0200
@@ -453,7 +453,7 @@
etype = self.description[row][col]
entity = self.req.vreg['etypes'].etype_class(etype)(req, rset=self,
row=row, col=col)
- entity.set_eid(eid)
+ entity.eid = eid
# cache entity
req.set_entity_cache(entity)
eschema = entity.e_schema
@@ -494,7 +494,7 @@
rrset.req = req
else:
rrset = self._build_entity(row, outerselidx).as_rset()
- entity.set_related_cache(attr, role, rrset)
+ entity.cw_set_relation_cache(attr, role, rrset)
return entity
@cached
--- a/schema.py Wed Jun 02 16:13:28 2010 +0200
+++ b/schema.py Wed Jun 02 16:15:05 2010 +0200
@@ -568,7 +568,14 @@
rdef.name = rdef.name.lower()
rdef.subject = bw_normalize_etype(rdef.subject)
rdef.object = bw_normalize_etype(rdef.object)
- rdefs = super(CubicWebSchema, self).add_relation_def(rdef)
+ try:
+ rdefs = super(CubicWebSchema, self).add_relation_def(rdef)
+ except BadSchemaDefinition:
+ reversed_etype_map = dict( (v, k) for k, v in ETYPE_NAME_MAP.iteritems() )
+ if rdef.subject in reversed_etype_map or rdef.object in reversed_etype_map:
+ self.warning('huuuu')
+ return
+ raise
if rdefs:
try:
self._eid_index[rdef.eid] = rdefs
--- a/selectors.py Wed Jun 02 16:13:28 2010 +0200
+++ b/selectors.py Wed Jun 02 16:15:05 2010 +0200
@@ -169,7 +169,7 @@
or below the :func:`objectify_selector` decorator of your selector function so it gets
traceable when :class:`traced_selection` is activated (see :ref:`DebuggingSelectors`).
-.. autofunction:: cubicweb.selectors.lltrace
+.. autofunction:: cubicweb.appobject.lltrace
.. note::
Selectors __call__ should *always* return a positive integer, and shall never
@@ -183,10 +183,10 @@
Once in a while, one needs to understand why a view (or any application object)
is, or is not selected appropriately. Looking at which selectors fired (or did
-not) is the way. The :class:`cubicweb.selectors.traced_selection` context
+not) is the way. The :class:`cubicweb.appobject.traced_selection` context
manager to help with that, *if you're running your instance in debug mode*.
-.. autoclass:: cubicweb.selectors.traced_selection
+.. autoclass:: cubicweb.appobject.traced_selection
.. |cubicweb| replace:: *CubicWeb*
@@ -204,87 +204,10 @@
from cubicweb import Unauthorized, NoSelectableObject, NotAnEntity, role
# even if not used, let yes here so it's importable through this module
-from cubicweb.appobject import Selector, objectify_selector, yes
-from cubicweb.vregistry import class_regid
-from cubicweb.cwconfig import CubicWebConfiguration
+from cubicweb.appobject import Selector, objectify_selector, lltrace, yes
from cubicweb.schema import split_expression
-# helpers for debugging selectors
-SELECTOR_LOGGER = logging.getLogger('cubicweb.selectors')
-TRACED_OIDS = None
-
-def _trace_selector(cls, selector, args, ret):
- # /!\ lltrace decorates pure function or __call__ method, this
- # means argument order may be different
- if isinstance(cls, Selector):
- selname = str(cls)
- vobj = args[0]
- else:
- selname = selector.__name__
- vobj = cls
- if TRACED_OIDS == 'all' or class_regid(vobj) in TRACED_OIDS:
- #SELECTOR_LOGGER.warning('selector %s returned %s for %s', selname, ret, cls)
- print '%s -> %s for %s(%s)' % (selname, ret, vobj, vobj.__regid__)
-
-def lltrace(selector):
- """use this decorator on your selectors so the becomes traceable with
- :class:`traced_selection`
- """
- # don't wrap selectors if not in development mode
- if CubicWebConfiguration.mode == 'system': # XXX config.debug
- return selector
- def traced(cls, *args, **kwargs):
- ret = selector(cls, *args, **kwargs)
- if TRACED_OIDS is not None:
- _trace_selector(cls, selector, args, ret)
- return ret
- traced.__name__ = selector.__name__
- traced.__doc__ = selector.__doc__
- return traced
-
-class traced_selection(object):
- """
- Typical usage is :
-
- .. sourcecode:: python
-
- >>> from cubicweb.selectors import traced_selection
- >>> with traced_selection():
- ... # some code in which you want to debug selectors
- ... # for all objects
-
- Don't forget the 'from __future__ import with_statement' at the module top-level
- if you're using python prior to 2.6.
-
- This will yield lines like this in the logs::
-
- selector one_line_rset returned 0 for <class 'cubicweb.web.views.basecomponents.WFHistoryVComponent'>
-
- You can also give to :class:`traced_selection` the identifiers of objects on
- which you want to debug selection ('oid1' and 'oid2' in the example above).
-
- .. sourcecode:: python
-
- >>> with traced_selection( ('regid1', 'regid2') ):
- ... # some code in which you want to debug selectors
- ... # for objects with __regid__ 'regid1' and 'regid2'
-
- A potentially usefull point to set up such a tracing function is
- the `cubicweb.vregistry.Registry.select` method body.
- """
-
- def __init__(self, traced='all'):
- self.traced = traced
-
- def __enter__(self):
- global TRACED_OIDS
- TRACED_OIDS = self.traced
-
- def __exit__(self, exctype, exc, traceback):
- global TRACED_OIDS
- TRACED_OIDS = None
- return traceback is None
-
+from cubicweb.appobject import traced_selection # XXX for bw compat
def score_interface(etypesreg, cls_or_inst, cls, iface):
"""Return XXX if the give object (maybe an instance or class) implements
@@ -301,6 +224,7 @@
if iface is basecls:
return index + 3
return 0
+ # XXX iface in implements deprecated in 3.9
if implements_iface(cls_or_inst, iface):
# implenting an interface takes precedence other special Any interface
return 2
@@ -374,14 +298,17 @@
self.accept_none = accept_none
@lltrace
- def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
+ def __call__(self, cls, req, rset=None, row=None, col=0, accept_none=None,
+ **kwargs):
if kwargs.get('entity'):
return self.score_class(kwargs['entity'].__class__, req)
if not rset:
return 0
score = 0
if row is None:
- if not self.accept_none:
+ if accept_none is None:
+ accept_none = self.accept_none
+ if not accept_none:
if any(rset[i][col] is None for i in xrange(len(rset))):
return 0
for etype in rset.column_types(col):
@@ -441,7 +368,8 @@
"""
@lltrace
- def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
+ def __call__(self, cls, req, rset=None, row=None, col=0, accept_none=None,
+ **kwargs):
if not rset and not kwargs.get('entity'):
return 0
score = 0
@@ -449,9 +377,11 @@
score = self.score_entity(kwargs['entity'])
elif row is None:
col = col or 0
+ if accept_none is None:
+ accept_none = self.accept_none
for row, rowvalue in enumerate(rset.rows):
if rowvalue[col] is None: # outer join
- if not self.accept_none:
+ if not accept_none:
return 0
continue
escore = self.score(req, rset, row, col)
@@ -527,19 +457,42 @@
* `registry`, a registry name
- * `regid`, an object identifier in this registry
+ * `regids`, object identifiers in this registry, one of them should be
+ selectable.
"""
- def __init__(self, registry, regid):
+ selectable_score = 1
+ def __init__(self, registry, *regids):
self.registry = registry
- self.regid = regid
+ self.regids = regids
+
+ @lltrace
+ def __call__(self, cls, req, **kwargs):
+ for regid in self.regids:
+ try:
+ req.vreg[self.registry].select(regid, req, **kwargs)
+ return self.selectable_score
+ except NoSelectableObject:
+ return 0
+
+
+class adaptable(appobject_selectable):
+ """Return 1 if another appobject is selectable using the same input context.
+
+ Initializer arguments:
+
+ * `regids`, adapter identifiers (e.g. interface names) to which the context
+ (usually entities) should be adaptable. One of them should be selectable
+ when multiple identifiers are given.
+ """
+ # implementing an interface takes precedence other special Any interface,
+ # hence return 2 (implements('Any') score is 1)
+ selectable_score = 2
+ def __init__(self, *regids):
+ super(adaptable, self).__init__('adapters', *regids)
def __call__(self, cls, req, **kwargs):
- try:
- req.vreg[self.registry].select(self.regid, req, **kwargs)
- return 1
- except NoSelectableObject:
- return 0
-
+ kwargs.setdefault('accept_none', False)
+ return super(adaptable, self).__call__(cls, req, **kwargs)
# rset selectors ##############################################################
@@ -594,7 +547,7 @@
class multi_lines_rset(Selector):
- """If `nb`is specified, return 1 if the result set has exactly `nb` row of
+ """If `nb` is specified, return 1 if the result set has exactly `nb` row of
result. Else (`nb` is None), return 1 if the result set contains *at least*
two rows.
"""
@@ -608,11 +561,11 @@
@lltrace
def __call__(self, cls, req, rset=None, **kwargs):
- return rset is not None and self.match_expected(rset.rowcount)
+ return int(rset is not None and self.match_expected(rset.rowcount))
class multi_columns_rset(multi_lines_rset):
- """If `nb`is specified, return 1 if the result set has exactly `nb` column
+ """If `nb` is specified, return 1 if the result set has exactly `nb` column
per row. Else (`nb` is None), return 1 if the result set contains *at least*
two columns per row. Return 0 for empty result set.
"""
@@ -738,7 +691,12 @@
.. note:: when interface is an entity class, the score will reflect class
proximity so the most specific object will be selected.
+
+ .. note:: with cubicweb >= 3.9, you should use adapters instead of
+ interface, so no interface should be given to this selector. Use
+ :class:`adaptable` instead.
"""
+
def score_class(self, eclass, req):
return self.score_interfaces(req, eclass, eclass)
@@ -765,6 +723,26 @@
self.score_entity = intscore
+class has_mimetype(EntitySelector):
+ """Return 1 if the entity adapt to IDownloadable and has the given MIME type.
+
+ You can give 'image/' to match any image for instance, or 'image/png' to match
+ only PNG images.
+ """
+ def __init__(self, mimetype, once_is_enough=False):
+ super(has_mimetype, self).__init__(once_is_enough)
+ self.mimetype = mimetype
+
+ def score_entity(self, entity):
+ idownloadable = entity.cw_adapt_to('IDownloadable')
+ if idownloadable is None:
+ return 0
+ mt = idownloadable.download_content_type()
+ if not (mt and mt.startswith(self.mimetype)):
+ return 0
+ return 1
+
+
class relation_possible(EntitySelector):
"""Return 1 for entity that supports the relation, provided that the
request's user may do some `action` on it (see below).
@@ -999,7 +977,7 @@
return self.score(req, rset, row, col)
def score_entity(self, entity):
- if entity.has_perm(self.action):
+ if entity.cw_has_perm(self.action):
return 1
return 0
@@ -1290,21 +1268,26 @@
class is_in_state(score_entity):
"""return 1 if entity is in one of the states given as argument list
- you should use this instead of your own score_entity x: x.state == 'bla'
- selector to avoid some gotchas:
+ you should use this instead of your own :class:`score_entity` selector to
+ avoid some gotchas:
* possible views gives a fake entity with no state
- * you must use the latest tr info, not entity.state for repository side
+ * you must use the latest tr info, not entity.in_state for repository side
checking of the current state
"""
def __init__(self, *states):
def score(entity, states=set(states)):
+ trinfo = entity.cw_adapt_to('IWorkflowable').latest_trinfo()
try:
- return entity.latest_trinfo().new_state.name in states
+ return trinfo.new_state.name in states
except AttributeError:
return None
super(is_in_state, self).__init__(score)
+@objectify_selector
+def debug_mode(cls, req, rset=None, **kwargs):
+ """Return 1 if running in debug mode"""
+ return req.vreg.config.debugmode and 1 or 0
## deprecated stuff ############################################################
--- a/server/migractions.py Wed Jun 02 16:13:28 2010 +0200
+++ b/server/migractions.py Wed Jun 02 16:15:05 2010 +0200
@@ -51,7 +51,8 @@
from yams.schema2sql import eschema2sql, rschema2sql
from cubicweb import AuthenticationError
-from cubicweb.schema import (META_RTYPES, VIRTUAL_RTYPES,
+from cubicweb.schema import (ETYPE_NAME_MAP, META_RTYPES, VIRTUAL_RTYPES,
+ PURE_VIRTUAL_RTYPES,
CubicWebRelationSchema, order_eschemas)
from cubicweb.dbapi import get_repository, repo_connect
from cubicweb.migration import MigrationHelper, yes
@@ -851,9 +852,23 @@
`oldname` is a string giving the name of the existing entity type
`newname` is a string giving the name of the renamed entity type
"""
- self.rqlexec('SET ET name %(newname)s WHERE ET is CWEType, ET name %(oldname)s',
- {'newname' : unicode(newname), 'oldname' : oldname},
- ask_confirm=False)
+ schema = self.repo.schema
+ if newname in schema:
+ assert oldname in ETYPE_NAME_MAP, \
+ '%s should be mappend to %s in ETYPE_NAME_MAP' % (oldname, newname)
+ attrs = ','.join([SQL_PREFIX + rschema.type
+ for rschema in schema[newname].subject_relations()
+ if (rschema.final or rschema.inlined)
+ and not rschema in PURE_VIRTUAL_RTYPES])
+ self.sqlexec('INSERT INTO %s%s(%s) SELECT %s FROM %s%s' % (
+ SQL_PREFIX, newname, attrs, attrs, SQL_PREFIX, oldname))
+ # use rql to propagate deletion. XXX we may miss some stuff since
+ # only the bootstrap schema is set.
+ self.rqlexec('DELETE CWEType ET WHERE ET name %(n)s', {'n': oldname})
+ else:
+ self.rqlexec('SET ET name %(newname)s WHERE ET is CWEType, ET name %(oldname)s',
+ {'newname' : unicode(newname), 'oldname' : oldname},
+ ask_confirm=False)
if commit:
self.commit()
@@ -1157,10 +1172,10 @@
if commit:
self.commit()
- @deprecated('[3.5] use entity.fire_transition("transition") or entity.change_state("state")',
- stacklevel=3)
+ @deprecated('[3.5] use iworkflowable.fire_transition("transition") or '
+ 'iworkflowable.change_state("state")', stacklevel=3)
def cmd_set_state(self, eid, statename, commit=False):
- self._cw.entity_from_eid(eid).change_state(statename)
+ self._cw.entity_from_eid(eid).cw_adapt_to('IWorkflowable').change_state(statename)
if commit:
self.commit()
--- a/server/repository.py Wed Jun 02 16:13:28 2010 +0200
+++ b/server/repository.py Wed Jun 02 16:15:05 2010 +0200
@@ -104,10 +104,10 @@
XXX protect pyro access
"""
- def __init__(self, config, vreg=None, debug=False):
+ def __init__(self, config, vreg=None):
self.config = config
if vreg is None:
- vreg = cwvreg.CubicWebVRegistry(config, debug)
+ vreg = cwvreg.CubicWebVRegistry(config)
self.vreg = vreg
self.pyro_registered = False
self.info('starting repository from %s', self.config.apphome)
@@ -152,13 +152,6 @@
if not isinstance(session.user, InternalManager):
session.user.__class__ = usercls
- def _bootstrap_hook_registry(self):
- """called during bootstrap since we need the metadata hooks"""
- hooksdirectory = join(CW_SOFTWARE_ROOT, 'hooks')
- self.vreg.init_registration([hooksdirectory])
- self.vreg.load_file(join(hooksdirectory, 'metadata.py'),
- 'cubicweb.hooks.metadata')
-
def open_connections_pools(self):
config = self.config
self._available_pools = Queue.Queue()
@@ -184,7 +177,9 @@
for modname in ('__init__', 'authobjs', 'wfobjs'):
self.vreg.load_file(join(etdirectory, '%s.py' % modname),
'cubicweb.entities.%s' % modname)
- self._bootstrap_hook_registry()
+ hooksdirectory = join(CW_SOFTWARE_ROOT, 'hooks')
+ self.vreg.load_file(join(hooksdirectory, 'metadata.py'),
+ 'cubicweb.hooks.metadata')
elif config.read_instance_schema:
# normal start: load the instance schema from the database
self.fill_schema()
@@ -233,8 +228,7 @@
if resetvreg:
if self.config._cubes is None:
self.config.init_cubes(self.get_cubes())
- # full reload of all appobjects
- self.vreg.reset()
+ # trigger full reload of all appobjects
self.vreg.set_schema(schema)
else:
self.vreg._set_schema(schema)
@@ -390,7 +384,7 @@
raise AuthenticationError('authentication failed with all sources')
cwuser = self._build_user(session, eid)
if self.config.consider_user_state and \
- not cwuser.state in cwuser.AUTHENTICABLE_STATES:
+ not cwuser.cw_adapt_to('IWorkflowable').state in cwuser.AUTHENTICABLE_STATES:
raise AuthenticationError('user is not in authenticable state')
return cwuser
@@ -570,7 +564,7 @@
session.close()
session = Session(user, self, cnxprops)
user._cw = user.cw_rset.req = session
- user.clear_related_cache()
+ user.cw_clear_relation_cache()
self._sessions[session.id] = session
self.info('opened %s', session)
self.hm.call_hooks('session_open', session)
@@ -1026,17 +1020,12 @@
the entity instance
"""
# init edited_attributes before calling before_add_entity hooks
- entity._is_saved = False # entity has an eid but is not yet saved
- entity.edited_attributes = set(entity)
- entity_ = entity.pre_add_hook()
- # XXX kill that transmutation feature !
- if not entity_ is entity:
- entity.__class__ = entity_.__class__
- entity.__dict__.update(entity_.__dict__)
+ entity._cw_is_saved = False # entity has an eid but is not yet saved
+ entity.edited_attributes = set(entity) # XXX cw_edited_attributes
eschema = entity.e_schema
source = self.locate_etype_source(entity.__regid__)
# allocate an eid to the entity before calling hooks
- entity.set_eid(self.system_source.create_eid(session))
+ entity.eid = self.system_source.create_eid(session)
# set caches asap
extid = self.init_entity_caches(session, entity, source)
if server.DEBUG & server.DBG_REPO:
@@ -1051,12 +1040,12 @@
rschema = eschema.subjrels[attr]
if not rschema.final: # inlined relation
relations.append((attr, entity[attr]))
- entity.set_defaults()
+ entity._cw_set_defaults()
if session.is_hook_category_activated('integrity'):
- entity.check(creation=True)
+ entity._cw_check(creation=True)
source.add_entity(session, entity)
self.add_info(session, entity, source, extid, complete=False)
- entity._is_saved = True # entity has an eid and is saved
+ entity._cw_is_saved = True # entity has an eid and is saved
# prefill entity relation caches
for rschema in eschema.subject_relations():
rtype = str(rschema)
@@ -1065,12 +1054,13 @@
if rschema.final:
entity.setdefault(rtype, None)
else:
- entity.set_related_cache(rtype, 'subject', session.empty_rset())
+ entity.cw_set_relation_cache(rtype, 'subject',
+ session.empty_rset())
for rschema in eschema.object_relations():
rtype = str(rschema)
if rtype in schema.VIRTUAL_RTYPES:
continue
- entity.set_related_cache(rtype, 'object', session.empty_rset())
+ entity.cw_set_relation_cache(rtype, 'object', session.empty_rset())
# set inline relation cache before call to after_add_entity
for attr, value in relations:
session.update_rel_cache_add(entity.eid, attr, value)
@@ -1099,7 +1089,7 @@
entity.edited_attributes = edited_attributes
try:
if session.is_hook_category_activated('integrity'):
- entity.check()
+ entity._cw_check()
only_inline_rels, need_fti_update = True, False
relations = []
source = self.source_from_eid(entity.eid, session)
@@ -1137,7 +1127,7 @@
hm.call_hooks('after_update_entity', session, entity=entity)
for attr, value, prevvalue in relations:
# if the relation is already cached, update existant cache
- relcache = entity.relation_cached(attr, 'subject')
+ relcache = entity.cw_relation_cached(attr, 'subject')
if prevvalue is not None:
hm.call_hooks('after_delete_relation', session,
eidfrom=entity.eid, rtype=attr, eidto=prevvalue)
@@ -1147,8 +1137,8 @@
if relcache is not None:
session.update_rel_cache_add(entity.eid, attr, value)
else:
- entity.set_related_cache(attr, 'subject',
- session.eid_rset(value))
+ entity.cw_set_relation_cache(attr, 'subject',
+ session.eid_rset(value))
hm.call_hooks('after_add_relation', session,
eidfrom=entity.eid, rtype=attr, eidto=value)
finally:
--- a/server/schemaserial.py Wed Jun 02 16:13:28 2010 +0200
+++ b/server/schemaserial.py Wed Jun 02 16:15:05 2010 +0200
@@ -27,7 +27,9 @@
from yams import schema as schemamod, buildobjs as ybo
-from cubicweb.schema import CONSTRAINTS, ETYPE_NAME_MAP, VIRTUAL_RTYPES
+from cubicweb import CW_SOFTWARE_ROOT
+from cubicweb.schema import (CONSTRAINTS, ETYPE_NAME_MAP,
+ VIRTUAL_RTYPES, PURE_VIRTUAL_RTYPES)
from cubicweb.server import sqlutils
def group_mapping(cursor, interactive=True):
@@ -100,17 +102,28 @@
sidx[eid] = eschema
continue
if etype in ETYPE_NAME_MAP:
+ needcopy = False
netype = ETYPE_NAME_MAP[etype]
# can't use write rql queries at this point, use raw sql
- session.system_sql('UPDATE %(p)sCWEType SET %(p)sname=%%(n)s WHERE %(p)seid=%%(x)s'
- % {'p': sqlutils.SQL_PREFIX},
- {'x': eid, 'n': netype})
- session.system_sql('UPDATE entities SET type=%(n)s WHERE type=%(x)s',
- {'x': etype, 'n': netype})
+ sqlexec = session.system_sql
+ if sqlexec('SELECT 1 FROM %(p)sCWEType WHERE %(p)sname=%%(n)s'
+ % {'p': sqlutils.SQL_PREFIX}, {'n': netype}).fetchone():
+ # the new type already exists, we should merge
+ assert etype.lower() != netype.lower()
+ needcopy = True
+ else:
+ # the new type doesn't exist, we should rename
+ sqlexec('UPDATE %(p)sCWEType SET %(p)sname=%%(n)s WHERE %(p)seid=%%(x)s'
+ % {'p': sqlutils.SQL_PREFIX}, {'x': eid, 'n': netype})
+ if etype.lower() != netype.lower():
+ sqlexec('ALTER TABLE %s%s RENAME TO %s%s' % (
+ sqlutils.SQL_PREFIX, etype, sqlutils.SQL_PREFIX, netype))
+ sqlexec('UPDATE entities SET type=%(n)s WHERE type=%(x)s',
+ {'x': etype, 'n': netype})
session.commit(False)
try:
- session.system_sql('UPDATE deleted_entities SET type=%(n)s WHERE type=%(x)s',
- {'x': etype, 'n': netype})
+ sqlexec('UPDATE deleted_entities SET type=%(n)s WHERE type=%(x)s',
+ {'x': etype, 'n': netype})
except:
pass
tocleanup = [eid]
@@ -118,6 +131,12 @@
if etype == eidetype)
repo.clear_caches(tocleanup)
session.commit(False)
+ if needcopy:
+ from logilab.common.testlib import mock_object
+ sidx[eid] = mock_object(type=netype)
+ # copy / CWEType entity removal expected to be done through
+ # rename_entity_type in a migration script
+ continue
etype = netype
etype = ybo.EntityType(name=etype, description=desc, eid=eid)
eschema = schema.add_entity_type(etype)
--- a/server/serverctl.py Wed Jun 02 16:13:28 2010 +0200
+++ b/server/serverctl.py Wed Jun 02 16:15:05 2010 +0200
@@ -249,9 +249,9 @@
cmdname = 'start'
cfgname = 'repository'
- def start_server(self, ctlconf, debug):
+ def start_server(self, ctlconf):
command = ['cubicweb-ctl start-repository ']
- if debug:
+ if ctlconf.debugmode:
command.append('--debug')
command.append(self.config.appid)
os.system(' '.join(command))
--- a/server/session.py Wed Jun 02 16:13:28 2010 +0200
+++ b/server/session.py Wed Jun 02 16:15:05 2010 +0200
@@ -242,7 +242,7 @@
entity = self.entity_cache(eid)
except KeyError:
return
- rcache = entity.relation_cached(rtype, role)
+ rcache = entity.cw_relation_cached(rtype, role)
if rcache is not None:
rset, entities = rcache
rset = rset.copy()
@@ -258,14 +258,15 @@
targetentity.cw_col = 0
rset.rowcount += 1
entities.append(targetentity)
- entity._related_cache['%s_%s' % (rtype, role)] = (rset, tuple(entities))
+ entity._cw_related_cache['%s_%s' % (rtype, role)] = (
+ rset, tuple(entities))
def _update_entity_rel_cache_del(self, eid, rtype, role, targeteid):
try:
entity = self.entity_cache(eid)
except KeyError:
return
- rcache = entity.relation_cached(rtype, role)
+ rcache = entity.cw_relation_cached(rtype, role)
if rcache is not None:
rset, entities = rcache
for idx, row in enumerate(rset.rows):
@@ -284,7 +285,8 @@
del rset.description[idx]
del entities[idx]
rset.rowcount -= 1
- entity._related_cache['%s_%s' % (rtype, role)] = (rset, tuple(entities))
+ entity._cw_related_cache['%s_%s' % (rtype, role)] = (
+ rset, tuple(entities))
# resource accessors ######################################################
@@ -302,16 +304,15 @@
def set_language(self, language):
"""i18n configuration for translation"""
- vreg = self.vreg
language = language or self.user.property_value('ui.language')
try:
- gettext, pgettext = vreg.config.translations[language]
+ gettext, pgettext = self.vreg.config.translations[language]
self._ = self.__ = gettext
self.pgettext = pgettext
except KeyError:
- language = vreg.property_value('ui.language')
+ language = self.vreg.property_value('ui.language')
try:
- gettext, pgettext = vreg.config.translations[language]
+ gettext, pgettext = self.vreg.config.translations[language]
self._ = self.__ = gettext
self.pgettext = pgettext
except KeyError:
@@ -626,16 +627,6 @@
else:
del self.transaction_data['ecache'][eid]
- def base_url(self):
- url = self.repo.config['base-url']
- if not url:
- try:
- url = self.repo.config.default_base_url()
- except AttributeError: # default_base_url() might not be available
- self.warning('missing base-url definition in server config')
- url = u''
- return url
-
def from_controller(self):
"""return the id (string) of the controller issuing the request (no
sense here, always return 'view')
--- a/server/sources/__init__.py Wed Jun 02 16:13:28 2010 +0200
+++ b/server/sources/__init__.py Wed Jun 02 16:15:05 2010 +0200
@@ -341,7 +341,7 @@
entity.
"""
entity = self.repo.vreg['etypes'].etype_class(etype)(session)
- entity.set_eid(eid)
+ entity.eid = eid
return entity
def after_entity_insertion(self, session, lid, entity):
--- a/server/sources/native.py Wed Jun 02 16:13:28 2010 +0200
+++ b/server/sources/native.py Wed Jun 02 16:15:05 2010 +0200
@@ -1015,10 +1015,10 @@
entity[rtype] = unicode(value, session.encoding, 'replace')
else:
entity[rtype] = value
- entity.set_eid(eid)
+ entity.eid = eid
session.repo.init_entity_caches(session, entity, self)
entity.edited_attributes = set(entity)
- entity.check()
+ entity._cw_check()
self.repo.hm.call_hooks('before_add_entity', session, entity=entity)
# restore the entity
action.changes['cw_eid'] = eid
@@ -1085,7 +1085,7 @@
return [session._(
"Can't undo creation of entity %(eid)s of type %(etype)s, type "
"no more supported" % {'eid': eid, 'etype': etype})]
- entity.set_eid(eid)
+ entity.eid = eid
# for proper eid/type cache update
hook.set_operation(session, 'pendingeids', eid,
CleanupDeletedEidsCacheOp)
@@ -1173,7 +1173,8 @@
try:
# use cursor_index_object, not cursor_reindex_object since
# unindexing done in the FTIndexEntityOp
- self.dbhelper.cursor_index_object(entity.eid, entity,
+ self.dbhelper.cursor_index_object(entity.eid,
+ entity.cw_adapt_to('IFTIndexable'),
session.pool['system'])
except Exception: # let KeyboardInterrupt / SystemExit propagate
self.exception('error while reindexing %s', entity)
@@ -1198,7 +1199,8 @@
# processed
return
done.add(eid)
- for container in session.entity_from_eid(eid).fti_containers():
+ iftindexable = session.entity_from_eid(eid).cw_adapt_to('IFTIndexable')
+ for container in iftindexable.fti_containers():
source.fti_unindex_entity(session, container.eid)
source.fti_index_entity(session, container)
--- a/server/sources/storages.py Wed Jun 02 16:13:28 2010 +0200
+++ b/server/sources/storages.py Wed Jun 02 16:15:05 2010 +0200
@@ -151,7 +151,7 @@
# PIL processing that use filename extension to detect content-type, as
# well as providing more understandable file names on the fs.
basename = [str(entity.eid), attr]
- name = entity.attr_metadata(attr, 'name')
+ name = entity.cw_attr_metadata(attr, 'name')
if name is not None:
basename.append(name.encode(self.fsencoding))
fspath = uniquify_path(self.default_directory, '_'.join(basename))
--- a/server/ssplanner.py Wed Jun 02 16:13:28 2010 +0200
+++ b/server/ssplanner.py Wed Jun 02 16:15:05 2010 +0200
@@ -487,7 +487,7 @@
value = row[index]
index += 1
if rorder == InsertRelationsStep.FINAL:
- edef.rql_set_value(rtype, value)
+ edef._cw_rql_set_value(rtype, value)
elif rorder == InsertRelationsStep.RELATION:
self.plan.add_relation_def( (edef, rtype, value) )
edef.querier_pending_relations[(rtype, 'subject')] = value
@@ -584,7 +584,7 @@
edef = edefs[eid]
except KeyError:
edefs[eid] = edef = session.entity_from_eid(eid)
- edef.rql_set_value(str(rschema), rhsval)
+ edef._cw_rql_set_value(str(rschema), rhsval)
else:
repo.glob_add_relation(session, lhsval, str(rschema), rhsval)
result[i] = newrow
--- a/server/test/data/migratedapp/schema.py Wed Jun 02 16:13:28 2010 +0200
+++ b/server/test/data/migratedapp/schema.py Wed Jun 02 16:15:05 2010 +0200
@@ -69,7 +69,7 @@
mydate = Date(default='TODAY')
shortpara = String(maxsize=64)
ecrit_par = SubjectRelation('Personne', constraints=[RQLConstraint('S concerne A, O concerne A')])
- attachment = SubjectRelation(('File', 'Image'))
+ attachment = SubjectRelation('File')
class Text(Para):
__specializes_schema__ = True
--- a/server/test/data/schema.py Wed Jun 02 16:13:28 2010 +0200
+++ b/server/test/data/schema.py Wed Jun 02 16:15:05 2010 +0200
@@ -94,7 +94,7 @@
})
migrated_from = SubjectRelation('Note')
- attachment = SubjectRelation(('File', 'Image'))
+ attachment = SubjectRelation('File')
inline1 = SubjectRelation('Affaire', inlined=True, cardinality='?*')
todo_by = SubjectRelation('CWUser')
--- a/server/test/unittest_ldapuser.py Wed Jun 02 16:13:28 2010 +0200
+++ b/server/test/unittest_ldapuser.py Wed Jun 02 16:15:05 2010 +0200
@@ -178,12 +178,13 @@
cnx = self.login(SYT, password='dummypassword')
cu = cnx.cursor()
adim = cu.execute('CWUser X WHERE X login %(login)s', {'login': ADIM}).get_entity(0, 0)
- adim.fire_transition('deactivate')
+ iworkflowable = adim.cw_adapt_to('IWorkflowable')
+ iworkflowable.fire_transition('deactivate')
try:
cnx.commit()
adim.clear_all_caches()
self.assertEquals(adim.in_state[0].name, 'deactivated')
- trinfo = adim.latest_trinfo()
+ trinfo = iworkflowable.latest_trinfo()
self.assertEquals(trinfo.owned_by[0].login, SYT)
# select from_state to skip the user's creation TrInfo
rset = self.sexecute('Any U ORDERBY D DESC WHERE WF wf_info_for X,'
@@ -195,7 +196,7 @@
# restore db state
self.restore_connection()
adim = self.sexecute('CWUser X WHERE X login %(login)s', {'login': ADIM}).get_entity(0, 0)
- adim.fire_transition('activate')
+ adim.cw_adapt_to('IWorkflowable').fire_transition('activate')
self.sexecute('DELETE X in_group G WHERE X login %(syt)s, G name "managers"', {'syt': SYT})
def test_same_column_names(self):
--- a/server/test/unittest_migractions.py Wed Jun 02 16:13:28 2010 +0200
+++ b/server/test/unittest_migractions.py Wed Jun 02 16:15:05 2010 +0200
@@ -425,7 +425,7 @@
self.failIf(self.config.cube_dir('email') in self.config.cubes_path())
self.failIf('file' in self.config.cubes())
self.failIf(self.config.cube_dir('file') in self.config.cubes_path())
- for ertype in ('Email', 'EmailThread', 'EmailPart', 'File', 'Image',
+ for ertype in ('Email', 'EmailThread', 'EmailPart', 'File',
'sender', 'in_thread', 'reply_to', 'data_format'):
self.failIf(ertype in schema, ertype)
self.assertEquals(sorted(schema['see_also'].rdefs.keys()),
@@ -448,7 +448,7 @@
self.failUnless(self.config.cube_dir('email') in self.config.cubes_path())
self.failUnless('file' in self.config.cubes())
self.failUnless(self.config.cube_dir('file') in self.config.cubes_path())
- for ertype in ('Email', 'EmailThread', 'EmailPart', 'File', 'Image',
+ for ertype in ('Email', 'EmailThread', 'EmailPart', 'File',
'sender', 'in_thread', 'reply_to', 'data_format'):
self.failUnless(ertype in schema, ertype)
self.assertEquals(sorted(schema['see_also'].rdefs.keys()),
--- a/server/test/unittest_msplanner.py Wed Jun 02 16:13:28 2010 +0200
+++ b/server/test/unittest_msplanner.py Wed Jun 02 16:15:05 2010 +0200
@@ -60,7 +60,7 @@
{'X': 'Card'}, {'X': 'Comment'}, {'X': 'Division'},
{'X': 'Email'}, {'X': 'EmailAddress'}, {'X': 'EmailPart'},
{'X': 'EmailThread'}, {'X': 'ExternalUri'}, {'X': 'File'},
- {'X': 'Folder'}, {'X': 'Image'}, {'X': 'Note'},
+ {'X': 'Folder'}, {'X': 'Note'},
{'X': 'Personne'}, {'X': 'RQLExpression'}, {'X': 'Societe'},
{'X': 'State'}, {'X': 'SubDivision'}, {'X': 'SubWorkflowExitPoint'},
{'X': 'Tag'}, {'X': 'TrInfo'}, {'X': 'Transition'},
@@ -784,10 +784,10 @@
[{'X': 'Basket'}]),
('Any X WHERE X has_text "bla", EXISTS(X owned_by 5), X is CWUser',
[{'X': 'CWUser'}]),
- ('Any X WHERE X has_text "bla", X is IN(Card, Comment, Division, Email, EmailThread, File, Folder, Image, Note, Personne, Societe, SubDivision, Tag)',
+ ('Any X WHERE X has_text "bla", X is IN(Card, Comment, Division, Email, EmailThread, File, Folder, Note, Personne, Societe, SubDivision, Tag)',
[{'X': 'Card'}, {'X': 'Comment'},
{'X': 'Division'}, {'X': 'Email'}, {'X': 'EmailThread'},
- {'X': 'File'}, {'X': 'Folder'}, {'X': 'Image'},
+ {'X': 'File'}, {'X': 'Folder'},
{'X': 'Note'}, {'X': 'Personne'}, {'X': 'Societe'},
{'X': 'SubDivision'}, {'X': 'Tag'}]),],
None, None, [self.system], {}, []),
@@ -810,10 +810,10 @@
[{'X': 'Basket'}]),
('Any X WHERE X has_text "bla", EXISTS(X owned_by 5), X is CWUser',
[{'X': 'CWUser'}]),
- ('Any X WHERE X has_text "bla", X is IN(Card, Comment, Division, Email, EmailThread, File, Folder, Image, Note, Personne, Societe, SubDivision, Tag)',
+ ('Any X WHERE X has_text "bla", X is IN(Card, Comment, Division, Email, EmailThread, File, Folder, Note, Personne, Societe, SubDivision, Tag)',
[{'X': 'Card'}, {'X': 'Comment'},
{'X': 'Division'}, {'X': 'Email'}, {'X': 'EmailThread'},
- {'X': 'File'}, {'X': 'Folder'}, {'X': 'Image'},
+ {'X': 'File'}, {'X': 'Folder'},
{'X': 'Note'}, {'X': 'Personne'}, {'X': 'Societe'},
{'X': 'SubDivision'}, {'X': 'Tag'}])],
[self.system], {}, {'X': 'table0.C0'}, []),
@@ -823,7 +823,7 @@
[{'X': 'Affaire'}, {'X': 'Basket'},
{'X': 'CWUser'}, {'X': 'Card'}, {'X': 'Comment'},
{'X': 'Division'}, {'X': 'Email'}, {'X': 'EmailThread'},
- {'X': 'File'}, {'X': 'Folder'}, {'X': 'Image'},
+ {'X': 'File'}, {'X': 'Folder'},
{'X': 'Note'}, {'X': 'Personne'}, {'X': 'Societe'},
{'X': 'SubDivision'}, {'X': 'Tag'}])],
10, 10, [self.system], {'X': 'table0.C0'}, [])
@@ -888,7 +888,7 @@
[{'X': 'Card'}, {'X': 'Note'}, {'X': 'State'}])],
[self.cards, self.system], {}, {'X': 'table0.C0'}, []),
('FetchStep',
- [('Any X WHERE X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Image, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)',
+ [('Any X WHERE X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)',
[{'X': 'BaseTransition'}, {'X': 'Bookmark'},
{'X': 'CWAttribute'}, {'X': 'CWCache'},
{'X': 'CWConstraint'}, {'X': 'CWConstraintType'},
@@ -899,7 +899,7 @@
{'X': 'Email'}, {'X': 'EmailAddress'},
{'X': 'EmailPart'}, {'X': 'EmailThread'},
{'X': 'ExternalUri'}, {'X': 'File'},
- {'X': 'Folder'}, {'X': 'Image'},
+ {'X': 'Folder'},
{'X': 'Personne'}, {'X': 'RQLExpression'},
{'X': 'Societe'}, {'X': 'SubDivision'},
{'X': 'SubWorkflowExitPoint'}, {'X': 'Tag'},
@@ -949,7 +949,7 @@
[self.system], {'X': 'table3.C0'}, {'ET': 'table0.C0', 'X': 'table0.C1'}, []),
# extra UnionFetchStep could be avoided but has no cost, so don't care
('UnionFetchStep',
- [('FetchStep', [('Any ET,X WHERE X is ET, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Image, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)',
+ [('FetchStep', [('Any ET,X WHERE X is ET, ET is CWEType, X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, CWRType, CWRelation, Comment, Division, Email, EmailAddress, EmailPart, EmailThread, ExternalUri, File, Folder, Personne, RQLExpression, Societe, SubDivision, SubWorkflowExitPoint, Tag, TrInfo, Transition, Workflow, WorkflowTransition)',
[{'X': 'BaseTransition', 'ET': 'CWEType'},
{'X': 'Bookmark', 'ET': 'CWEType'}, {'X': 'CWAttribute', 'ET': 'CWEType'},
{'X': 'CWCache', 'ET': 'CWEType'}, {'X': 'CWConstraint', 'ET': 'CWEType'},
@@ -961,7 +961,7 @@
{'X': 'EmailAddress', 'ET': 'CWEType'}, {'X': 'EmailPart', 'ET': 'CWEType'},
{'X': 'EmailThread', 'ET': 'CWEType'}, {'X': 'ExternalUri', 'ET': 'CWEType'},
{'X': 'File', 'ET': 'CWEType'}, {'X': 'Folder', 'ET': 'CWEType'},
- {'X': 'Image', 'ET': 'CWEType'}, {'X': 'Personne', 'ET': 'CWEType'},
+ {'X': 'Personne', 'ET': 'CWEType'},
{'X': 'RQLExpression', 'ET': 'CWEType'}, {'X': 'Societe', 'ET': 'CWEType'},
{'X': 'SubDivision', 'ET': 'CWEType'}, {'X': 'SubWorkflowExitPoint', 'ET': 'CWEType'},
{'X': 'Tag', 'ET': 'CWEType'}, {'X': 'TrInfo', 'ET': 'CWEType'},
@@ -1718,8 +1718,9 @@
])
def test_nonregr2(self):
- self.session.user.fire_transition('deactivate')
- treid = self.session.user.latest_trinfo().eid
+ iworkflowable = self.session.user.cw_adapt_to('IWorkflowable')
+ iworkflowable.fire_transition('deactivate')
+ treid = iworkflowable.latest_trinfo().eid
self._test('Any X ORDERBY D DESC WHERE E eid %(x)s, E wf_info_for X, X modification_date D',
[('FetchStep', [('Any X,D WHERE X modification_date D, X is Note',
[{'X': 'Note', 'D': 'Datetime'}])],
--- a/server/test/unittest_multisources.py Wed Jun 02 16:13:28 2010 +0200
+++ b/server/test/unittest_multisources.py Wed Jun 02 16:15:05 2010 +0200
@@ -110,11 +110,11 @@
self.assertEquals(len(rset), 4)
# since they are orderd by eid, we know the 3 first one is coming from the system source
# and the others from external source
- self.assertEquals(rset.get_entity(0, 0).metainformation(),
+ self.assertEquals(rset.get_entity(0, 0).cw_metainformation(),
{'source': {'adapter': 'native', 'uri': 'system'},
'type': u'Card', 'extid': None})
externent = rset.get_entity(3, 0)
- metainf = externent.metainformation()
+ metainf = externent.cw_metainformation()
self.assertEquals(metainf['source'], {'adapter': 'pyrorql', 'base-url': 'http://extern.org/', 'uri': 'extern'})
self.assertEquals(metainf['type'], 'Card')
self.assert_(metainf['extid'])
@@ -304,8 +304,9 @@
{'x': affaire.eid, 'u': ueid})
def test_nonregr2(self):
- self.session.user.fire_transition('deactivate')
- treid = self.session.user.latest_trinfo().eid
+ iworkflowable = self.session.user.cw_adapt_to('IWorkflowable')
+ iworkflowable.fire_transition('deactivate')
+ treid = iworkflowable.latest_trinfo().eid
rset = self.sexecute('Any X ORDERBY D DESC WHERE E eid %(x)s, E wf_info_for X, X modification_date D',
{'x': treid})
self.assertEquals(len(rset), 1)
--- a/server/test/unittest_querier.py Wed Jun 02 16:13:28 2010 +0200
+++ b/server/test/unittest_querier.py Wed Jun 02 16:15:05 2010 +0200
@@ -491,17 +491,17 @@
'WHERE RT name N, RDEF relation_type RT '
'HAVING COUNT(RDEF) > 10')
self.assertListEquals(rset.rows,
- [[u'description_format', 13],
- [u'description', 14],
+ [[u'description_format', 12],
+ [u'description', 13],
[u'name', 14],
- [u'created_by', 38],
- [u'creation_date', 38],
- [u'cwuri', 38],
- [u'in_basket', 38],
- [u'is', 38],
- [u'is_instance_of', 38],
- [u'modification_date', 38],
- [u'owned_by', 38]])
+ [u'created_by', 37],
+ [u'creation_date', 37],
+ [u'cwuri', 37],
+ [u'in_basket', 37],
+ [u'is', 37],
+ [u'is_instance_of', 37],
+ [u'modification_date', 37],
+ [u'owned_by', 37]])
def test_select_aggregat_having_dumb(self):
# dumb but should not raise an error
--- a/server/test/unittest_repository.py Wed Jun 02 16:13:28 2010 +0200
+++ b/server/test/unittest_repository.py Wed Jun 02 16:15:05 2010 +0200
@@ -205,7 +205,7 @@
session = repo._get_session(cnxid)
session.set_pool()
user = session.user
- user.fire_transition('deactivate')
+ user.cw_adapt_to('IWorkflowable').fire_transition('deactivate')
rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': user.eid})
self.assertEquals(len(rset), 1)
repo.rollback(cnxid)
@@ -246,7 +246,8 @@
'constrained_by',
'cardinality', 'ordernum',
'indexed', 'fulltextindexed', 'internationalizable',
- 'defaultval', 'description', 'description_format'])
+ 'defaultval', 'description', 'description_format',
+ 'in_basket'])
self.assertEquals(schema.eschema('CWEType').main_attribute(), 'name')
self.assertEquals(schema.eschema('State').main_attribute(), 'name')
--- a/server/test/unittest_rql2sql.py Wed Jun 02 16:13:28 2010 +0200
+++ b/server/test/unittest_rql2sql.py Wed Jun 02 16:15:05 2010 +0200
@@ -425,13 +425,10 @@
GROUP BY T1.C1'''),
('Any MAX(X)+MIN(LENGTH(D)), N GROUPBY N ORDERBY 1, N, DF WHERE X data_name N, X data D, X data_format DF;',
- '''SELECT (MAX(T1.C1) + MIN(LENGTH(T1.C0))), T1.C2 FROM (SELECT _X.cw_data AS C0, _X.cw_eid AS C1, _X.cw_data_name AS C2, _X.cw_data_format AS C3
+ '''SELECT (MAX(_X.cw_eid) + MIN(LENGTH(_X.cw_data))), _X.cw_data_name
FROM cw_File AS _X
-UNION ALL
-SELECT _X.cw_data AS C0, _X.cw_eid AS C1, _X.cw_data_name AS C2, _X.cw_data_format AS C3
-FROM cw_Image AS _X) AS T1
-GROUP BY T1.C2,T1.C3
-ORDER BY 1,2,T1.C3'''),
+GROUP BY _X.cw_data_name,_X.cw_data_format
+ORDER BY 1,2,_X.cw_data_format'''),
('DISTINCT Any S ORDERBY R WHERE A is Affaire, A sujet S, A ref R',
'''SELECT T1.C0 FROM (SELECT DISTINCT _A.cw_sujet AS C0, _A.cw_ref AS C1
@@ -439,12 +436,9 @@
ORDER BY 2) AS T1'''),
('DISTINCT Any MAX(X)+MIN(LENGTH(D)), N GROUPBY N ORDERBY 2, DF WHERE X data_name N, X data D, X data_format DF;',
- '''SELECT T1.C0,T1.C1 FROM (SELECT DISTINCT (MAX(T1.C1) + MIN(LENGTH(T1.C0))) AS C0, T1.C2 AS C1, T1.C3 AS C2 FROM (SELECT DISTINCT _X.cw_data AS C0, _X.cw_eid AS C1, _X.cw_data_name AS C2, _X.cw_data_format AS C3
+ '''SELECT T1.C0,T1.C1 FROM (SELECT DISTINCT (MAX(_X.cw_eid) + MIN(LENGTH(_X.cw_data))) AS C0, _X.cw_data_name AS C1, _X.cw_data_format AS C2
FROM cw_File AS _X
-UNION
-SELECT DISTINCT _X.cw_data AS C0, _X.cw_eid AS C1, _X.cw_data_name AS C2, _X.cw_data_format AS C3
-FROM cw_Image AS _X) AS T1
-GROUP BY T1.C2,T1.C3
+GROUP BY _X.cw_data_name,_X.cw_data_format
ORDER BY 2,3) AS T1
'''),
@@ -1629,5 +1623,6 @@
([{'A': 'RugbyGroup', 'B': 'RugbyTeam'}], {}, set())
)
+
if __name__ == '__main__':
unittest_main()
--- a/server/test/unittest_schemaserial.py Wed Jun 02 16:13:28 2010 +0200
+++ b/server/test/unittest_schemaserial.py Wed Jun 02 16:15:05 2010 +0200
@@ -68,8 +68,6 @@
{'et': None, 'x': None}),
('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s',
{'et': None, 'x': None}),
- # ('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s',
- # {'et': 'File', 'x': 'Image'}),
('SET X specializes ET WHERE X eid %(x)s, ET eid %(et)s',
{'et': None, 'x': None})])
--- a/server/test/unittest_security.py Wed Jun 02 16:13:28 2010 +0200
+++ b/server/test/unittest_security.py Wed Jun 02 16:15:05 2010 +0200
@@ -192,8 +192,7 @@
self.assertEquals(len(rset), 1)
ent = rset.get_entity(0, 0)
session.set_pool() # necessary
- self.assertRaises(Unauthorized,
- ent.e_schema.check_perm, session, 'update', eid=ent.eid)
+ self.assertRaises(Unauthorized, ent.cw_check_perm, 'update')
self.assertRaises(Unauthorized,
cu.execute, "SET P travaille S WHERE P is Personne, S is Societe")
# test nothing has actually been inserted:
@@ -384,7 +383,7 @@
# Note.para attribute editable by managers or if the note is in "todo" state
note = self.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0)
self.commit()
- note.fire_transition('markasdone')
+ note.cw_adapt_to('IWorkflowable').fire_transition('markasdone')
self.execute('SET X para "truc" WHERE X eid %(x)s', {'x': note.eid})
self.commit()
cnx = self.login('iaminusersgrouponly')
@@ -393,13 +392,13 @@
self.assertRaises(Unauthorized, cnx.commit)
note2 = cu.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0)
cnx.commit()
- note2.fire_transition('markasdone')
+ note2.cw_adapt_to('IWorkflowable').fire_transition('markasdone')
cnx.commit()
self.assertEquals(len(cu.execute('Any X WHERE X in_state S, S name "todo", X eid %(x)s', {'x': note2.eid})),
0)
cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid})
self.assertRaises(Unauthorized, cnx.commit)
- note2.fire_transition('redoit')
+ note2.cw_adapt_to('IWorkflowable').fire_transition('redoit')
cnx.commit()
cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid})
cnx.commit()
@@ -435,7 +434,7 @@
cnx.commit()
self.restore_connection()
affaire = self.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0)
- affaire.fire_transition('abort')
+ affaire.cw_adapt_to('IWorkflowable').fire_transition('abort')
self.commit()
self.assertEquals(len(self.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01"')),
1)
@@ -537,14 +536,15 @@
cu = cnx.cursor()
self.schema['Affaire'].set_action_permissions('read', ('users',))
aff = cu.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0)
- aff.fire_transition('abort')
+ aff.cw_adapt_to('IWorkflowable').fire_transition('abort')
cnx.commit()
# though changing a user state (even logged user) is reserved to managers
user = cnx.user(self.session)
# XXX wether it should raise Unauthorized or ValidationError is not clear
# the best would probably ValidationError if the transition doesn't exist
# from the current state but Unauthorized if it exists but user can't pass it
- self.assertRaises(ValidationError, user.fire_transition, 'deactivate')
+ self.assertRaises(ValidationError,
+ user.cw_adapt_to('IWorkflowable').fire_transition, 'deactivate')
finally:
# restore orig perms
for action, perms in affaire_perms.iteritems():
@@ -552,18 +552,19 @@
def test_trinfo_security(self):
aff = self.execute('INSERT Affaire X: X ref "ARCT01"').get_entity(0, 0)
+ iworkflowable = aff.cw_adapt_to('IWorkflowable')
self.commit()
- aff.fire_transition('abort')
+ iworkflowable.fire_transition('abort')
self.commit()
# can change tr info comment
self.execute('SET TI comment %(c)s WHERE TI wf_info_for X, X ref "ARCT01"',
{'c': u'bouh!'})
self.commit()
- aff.clear_related_cache('wf_info_for', 'object')
- trinfo = aff.latest_trinfo()
+ aff.cw_clear_relation_cache('wf_info_for', 'object')
+ trinfo = iworkflowable.latest_trinfo()
self.assertEquals(trinfo.comment, 'bouh!')
# but not from_state/to_state
- aff.clear_related_cache('wf_info_for', role='object')
+ aff.cw_clear_relation_cache('wf_info_for', role='object')
self.assertRaises(Unauthorized,
self.execute, 'SET TI from_state S WHERE TI eid %(ti)s, S name "ben non"',
{'ti': trinfo.eid})
--- a/server/test/unittest_storage.py Wed Jun 02 16:13:28 2010 +0200
+++ b/server/test/unittest_storage.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,9 +15,7 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""unit tests for module cubicweb.server.sources.storages
-
-"""
+"""unit tests for module cubicweb.server.sources.storages"""
from __future__ import with_statement
@@ -89,11 +87,11 @@
f1.set_attributes(data=Binary('the new data'))
self.rollback()
self.assertEquals(file(expected_filepath).read(), 'the-data')
- f1.delete()
+ f1.cw_delete()
self.failUnless(osp.isfile(expected_filepath))
self.rollback()
self.failUnless(osp.isfile(expected_filepath))
- f1.delete()
+ f1.cw_delete()
self.commit()
self.failIf(osp.isfile(expected_filepath))
@@ -133,7 +131,7 @@
ex = self.assertRaises(QueryError, self.execute,
'(Any D WHERE X data D, X is File)'
' UNION '
- '(Any D WHERE X data D, X is Image)')
+ '(Any D WHERE X title D, X is Bookmark)')
self.assertEquals(str(ex), 'query fetch some source mapped attribute, some not')
ex = self.assertRaises(QueryError,
self.execute, 'Any D WHERE X data D')
--- a/server/test/unittest_undo.py Wed Jun 02 16:13:28 2010 +0200
+++ b/server/test/unittest_undo.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,9 +15,6 @@
#
# 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 cubicweb import ValidationError
@@ -104,7 +101,7 @@
address=u'toto@logilab.org',
reverse_use_email=toto)
txuuid1 = self.commit()
- toto.delete()
+ toto.cw_delete()
txuuid2 = self.commit()
undoable_transactions = self.cnx.undoable_transactions
txs = undoable_transactions(action='D')
@@ -147,7 +144,7 @@
self.commit()
txs = self.cnx.undoable_transactions()
self.assertEquals(len(txs), 2)
- toto.delete()
+ toto.cw_delete()
txuuid = self.commit()
actions = self.cnx.transaction_info(txuuid).actions_list()
self.assertEquals(len(actions), 1)
@@ -160,8 +157,8 @@
self.failUnless(self.execute('Any X WHERE X eid %(x)s', {'x': toto.eid}))
self.failUnless(self.execute('Any X WHERE X eid %(x)s', {'x': e.eid}))
self.failUnless(self.execute('Any X WHERE X has_text "toto@logilab"'))
- self.assertEquals(toto.state, 'activated')
- self.assertEquals(toto.get_email(), 'toto@logilab.org')
+ self.assertEquals(toto.cw_adapt_to('IWorkflowable').state, 'activated')
+ self.assertEquals(toto.cw_adapt_to('IEmailable').get_email(), 'toto@logilab.org')
self.assertEquals([(p.pkey, p.value) for p in toto.reverse_for_user],
[('ui.default-text-format', 'text/rest')])
self.assertEquals([g.name for g in toto.in_group],
@@ -186,7 +183,7 @@
c = session.create_entity('Card', title=u'hop', content=u'hop')
p = session.create_entity('Personne', nom=u'louis', fiche=c)
self.commit()
- c.delete()
+ c.cw_delete()
txuuid = self.commit()
c2 = session.create_entity('Card', title=u'hip', content=u'hip')
p.set_relations(fiche=c2)
@@ -207,9 +204,9 @@
session.execute('DELETE U in_group G WHERE U eid %(x)s', {'x': self.toto.eid})
self.toto.set_relations(in_group=g)
self.commit()
- self.toto.delete()
+ self.toto.cw_delete()
txuuid = self.commit()
- g.delete()
+ g.cw_delete()
self.commit()
errors = self.cnx.undo_transaction(txuuid)
self.assertEquals(errors,
--- a/skeleton/data/external_resources.tmpl Wed Jun 02 16:13:28 2010 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-# -*- shell-script -*-
-###############################################################################
-#
-# put here information about external resources used by your components,
-# or to overides existing external resources configuration
-#
-###############################################################################
-
-# CSS stylesheets to include in HTML headers
-# uncomment the line below to use template specific stylesheet
-# STYLESHEETS = DATADIR/cubes.%(cubename)s.css
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/skeleton/uiprops.py.tmpl Wed Jun 02 16:15:05 2010 +0200
@@ -0,0 +1,15 @@
+###############################################################################
+#
+# Put here information about external resources / styles used by your cube,
+# or to overides existing UI properties.
+#
+# Existing properties are available through the `sheet` dictionary available
+# in the global namespace. You also have access to a `data` function which
+# will return proper url for resources in the 'data' directory.
+#
+# /!\ this file should not be imported /!\
+###############################################################################
+
+# CSS stylesheets to include in HTML headers
+# uncomment the line below to use template specific stylesheet
+# STYLESHEETS = sheet['STYLESHEETS'] + [data('cubes.%(cubename)s.css')]
--- a/sobjects/notification.py Wed Jun 02 16:13:28 2010 +0200
+++ b/sobjects/notification.py Wed Jun 02 16:15:05 2010 +0200
@@ -46,7 +46,8 @@
mode = self._cw.vreg.config['default-recipients-mode']
if mode == 'users':
execute = self._cw.execute
- dests = [(u.get_email(), u.property_value('ui.language'))
+ dests = [(u.cw_adapt_to('IEmailable').get_email(),
+ u.property_value('ui.language'))
for u in execute(self.user_rql, build_descr=True).entities()]
elif mode == 'default-dest-addrs':
lang = self._cw.vreg.property_value('ui.language')
--- a/sobjects/test/unittest_notification.py Wed Jun 02 16:13:28 2010 +0200
+++ b/sobjects/test/unittest_notification.py Wed Jun 02 16:15:05 2010 +0200
@@ -85,7 +85,7 @@
def test_status_change_view(self):
req = self.request()
u = self.create_user('toto', req=req)
- u.fire_transition('deactivate', comment=u'yeah')
+ u.cw_adapt_to('IWorkflowable').fire_transition('deactivate', comment=u'yeah')
self.failIf(MAILBOX)
self.commit()
self.assertEquals(len(MAILBOX), 1)
--- a/sobjects/test/unittest_supervising.py Wed Jun 02 16:13:28 2010 +0200
+++ b/sobjects/test/unittest_supervising.py Wed Jun 02 16:15:05 2010 +0200
@@ -84,7 +84,7 @@
self.assertEquals(op.to_send[0][1], ['test@logilab.fr'])
self.commit()
# some other changes #######
- user.fire_transition('deactivate')
+ user.cw_adapt_to('IWorkflowable').fire_transition('deactivate')
sentops = [op for op in session.pending_operations
if isinstance(op, SupervisionMailOp)]
self.assertEquals(len(sentops), 1)
--- a/sobjects/textparsers.py Wed Jun 02 16:13:28 2010 +0200
+++ b/sobjects/textparsers.py Wed Jun 02 16:15:05 2010 +0200
@@ -74,10 +74,14 @@
if not hasattr(entity, 'in_state'):
self.error('bad change state instruction for eid %s', eid)
continue
- tr = entity.current_workflow and entity.current_workflow.transition_by_name(trname)
+ iworkflowable = entity.cw_adapt_to('IWorkflowable')
+ if iworkflowable.current_workflow:
+ tr = iworkflowable.current_workflow.transition_by_name(trname)
+ else:
+ tr = None
if tr and tr.may_be_fired(entity.eid):
try:
- trinfo = entity.fire_transition(tr)
+ trinfo = iworkflowable.fire_transition(tr)
caller.fire_event('state-changed', {'trinfo': trinfo,
'entity': entity})
except:
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/scripts/script1.py Wed Jun 02 16:15:05 2010 +0200
@@ -0,0 +1,3 @@
+assert 'data/scripts/script1.py' == __file__
+assert '__main__' == __name__
+assert [] == __args__, __args__
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/scripts/script2.py Wed Jun 02 16:15:05 2010 +0200
@@ -0,0 +1,3 @@
+assert 'data/scripts/script2.py' == __file__
+assert '__main__' == __name__
+assert ['-v'] == __args__, __args__
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/scripts/script3.py Wed Jun 02 16:15:05 2010 +0200
@@ -0,0 +1,3 @@
+assert 'data/scripts/script3.py' == __file__
+assert '__main__' == __name__
+assert ['-vd', '-f', 'FILE.TXT'] == __args__, __args__
--- a/test/unittest_cwctl.py Wed Jun 02 16:13:28 2010 +0200
+++ b/test/unittest_cwctl.py Wed Jun 02 16:15:05 2010 +0200
@@ -24,8 +24,12 @@
from logilab.common.testlib import TestCase, unittest_main
from cubicweb.cwconfig import CubicWebConfiguration
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.server.migractions import ServerMigrationHelper
+
CubicWebConfiguration.load_cwctl_plugins() # XXX necessary?
+
class CubicWebCtlTC(TestCase):
def setUp(self):
self.stream = StringIO()
@@ -37,5 +41,25 @@
from cubicweb.cwctl import ListCommand
ListCommand().run([])
+
+class CubicWebShellTC(CubicWebTC):
+
+ def test_process_script_args_context(self):
+ repo = self.cnx._repo
+ mih = ServerMigrationHelper(None, repo=repo, cnx=self.cnx,
+ interactive=False,
+ # hack so it don't try to load fs schema
+ schema=1)
+ scripts = {'script1.py': list(),
+ 'script2.py': ['-v'],
+ 'script3.py': ['-vd', '-f', 'FILE.TXT'],
+ }
+ mih.cmd_process_script('data/scripts/script1.py', funcname=None)
+ for script, args in scripts.items():
+ scriptname = os.path.join('data/scripts/', script)
+ self.assert_(os.path.exists(scriptname))
+ mih.cmd_process_script(scriptname, None, scriptargs=args)
+
+
if __name__ == '__main__':
unittest_main()
--- a/test/unittest_entity.py Wed Jun 02 16:13:28 2010 +0200
+++ b/test/unittest_entity.py Wed Jun 02 16:15:05 2010 +0200
@@ -97,27 +97,27 @@
user = self.execute('INSERT CWUser X: X login "toto", X upassword %(pwd)s, X in_group G WHERE G name "users"',
{'pwd': 'toto'}).get_entity(0, 0)
self.commit()
- user.fire_transition('deactivate')
+ user.cw_adapt_to('IWorkflowable').fire_transition('deactivate')
self.commit()
eid2 = self.execute('INSERT CWUser X: X login "tutu", X upassword %(pwd)s', {'pwd': 'toto'})[0][0]
e = self.execute('Any X WHERE X eid %(x)s', {'x': eid2}).get_entity(0, 0)
e.copy_relations(user.eid)
self.commit()
- e.clear_related_cache('in_state', 'subject')
- self.assertEquals(e.state, 'activated')
+ e.cw_clear_relation_cache('in_state', 'subject')
+ self.assertEquals(e.cw_adapt_to('IWorkflowable').state, 'activated')
def test_related_cache_both(self):
user = self.execute('Any X WHERE X eid %(x)s', {'x':self.user().eid}).get_entity(0, 0)
adeleid = self.execute('INSERT EmailAddress X: X address "toto@logilab.org", U use_email X WHERE U login "admin"')[0][0]
self.commit()
- self.assertEquals(user._related_cache, {})
+ self.assertEquals(user._cw_related_cache, {})
email = user.primary_email[0]
- self.assertEquals(sorted(user._related_cache), ['primary_email_subject'])
- self.assertEquals(email._related_cache.keys(), ['primary_email_object'])
+ self.assertEquals(sorted(user._cw_related_cache), ['primary_email_subject'])
+ self.assertEquals(email._cw_related_cache.keys(), ['primary_email_object'])
groups = user.in_group
- self.assertEquals(sorted(user._related_cache), ['in_group_subject', 'primary_email_subject'])
+ self.assertEquals(sorted(user._cw_related_cache), ['in_group_subject', 'primary_email_subject'])
for group in groups:
- self.failIf('in_group_subject' in group._related_cache, group._related_cache.keys())
+ self.failIf('in_group_subject' in group._cw_related_cache, group._cw_related_cache.keys())
def test_related_limit(self):
req = self.request()
@@ -197,20 +197,20 @@
Note.fetch_attrs, Note.fetch_order = fetch_config(('type',))
SubNote.fetch_attrs, SubNote.fetch_order = fetch_config(('type',))
p = self.request().create_entity('Personne', nom=u'pouet')
- self.assertEquals(p.related_rql('evaluee'),
+ self.assertEquals(p.cw_related_rql('evaluee'),
'Any X,AA,AB ORDERBY AA ASC WHERE E eid %(x)s, E evaluee X, '
'X type AA, X modification_date AB')
Personne.fetch_attrs, Personne.fetch_order = fetch_config(('nom', ))
# XXX
- self.assertEquals(p.related_rql('evaluee'),
+ self.assertEquals(p.cw_related_rql('evaluee'),
'Any X,AA ORDERBY AA DESC '
'WHERE E eid %(x)s, E evaluee X, X modification_date AA')
tag = self.vreg['etypes'].etype_class('Tag')(self.request())
- self.assertEquals(tag.related_rql('tags', 'subject'),
+ self.assertEquals(tag.cw_related_rql('tags', 'subject'),
'Any X,AA ORDERBY AA DESC '
'WHERE E eid %(x)s, E tags X, X modification_date AA')
- self.assertEquals(tag.related_rql('tags', 'subject', ('Personne',)),
+ self.assertEquals(tag.cw_related_rql('tags', 'subject', ('Personne',)),
'Any X,AA,AB ORDERBY AA ASC '
'WHERE E eid %(x)s, E tags X, X is IN (Personne), X nom AA, '
'X modification_date AB')
@@ -219,47 +219,47 @@
tag = self.vreg['etypes'].etype_class('Tag')(self.request())
for ttype in self.schema['tags'].objects():
self.vreg['etypes'].etype_class(ttype).fetch_attrs = ('modification_date',)
- self.assertEquals(tag.related_rql('tags', 'subject'),
+ self.assertEquals(tag.cw_related_rql('tags', 'subject'),
'Any X,AA ORDERBY AA DESC '
'WHERE E eid %(x)s, E tags X, X modification_date AA')
def test_unrelated_rql_security_1(self):
user = self.request().user
- rql = user.unrelated_rql('use_email', 'EmailAddress', 'subject')[0]
+ rql = user.cw_unrelated_rql('use_email', 'EmailAddress', 'subject')[0]
self.assertEquals(rql, 'Any O,AA,AB,AC ORDERBY AC DESC '
'WHERE NOT S use_email O, S eid %(x)s, O is EmailAddress, O address AA, O alias AB, O modification_date AC')
self.create_user('toto')
self.login('toto')
user = self.request().user
- rql = user.unrelated_rql('use_email', 'EmailAddress', 'subject')[0]
+ rql = user.cw_unrelated_rql('use_email', 'EmailAddress', 'subject')[0]
self.assertEquals(rql, 'Any O,AA,AB,AC ORDERBY AC DESC '
'WHERE NOT S use_email O, S eid %(x)s, O is EmailAddress, O address AA, O alias AB, O modification_date AC')
user = self.execute('Any X WHERE X login "admin"').get_entity(0, 0)
- self.assertRaises(Unauthorized, user.unrelated_rql, 'use_email', 'EmailAddress', 'subject')
+ self.assertRaises(Unauthorized, user.cw_unrelated_rql, 'use_email', 'EmailAddress', 'subject')
self.login('anon')
user = self.request().user
- self.assertRaises(Unauthorized, user.unrelated_rql, 'use_email', 'EmailAddress', 'subject')
+ self.assertRaises(Unauthorized, user.cw_unrelated_rql, 'use_email', 'EmailAddress', 'subject')
def test_unrelated_rql_security_2(self):
email = self.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0)
- rql = email.unrelated_rql('use_email', 'CWUser', 'object')[0]
+ rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0]
self.assertEquals(rql, 'Any S,AA,AB,AC,AD ORDERBY AA ASC '
'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')
- #rql = email.unrelated_rql('use_email', 'Person', 'object')[0]
+ #rql = email.cw_unrelated_rql('use_email', 'Person', 'object')[0]
#self.assertEquals(rql, '')
self.login('anon')
email = self.execute('Any X WHERE X eid %(x)s', {'x': email.eid}).get_entity(0, 0)
- rql = email.unrelated_rql('use_email', 'CWUser', 'object')[0]
+ rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0]
self.assertEquals(rql, 'Any S,AA,AB,AC,AD ORDERBY AA '
'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, '
'A eid %(B)s, EXISTS(S identity A, NOT A in_group C, C name "guests", C is CWGroup)')
- #rql = email.unrelated_rql('use_email', 'Person', 'object')[0]
+ #rql = email.cw_unrelated_rql('use_email', 'Person', 'object')[0]
#self.assertEquals(rql, '')
def test_unrelated_rql_security_nonexistant(self):
self.login('anon')
email = self.vreg['etypes'].etype_class('EmailAddress')(self.request())
- rql = email.unrelated_rql('use_email', 'CWUser', 'object')[0]
+ rql = email.cw_unrelated_rql('use_email', 'CWUser', 'object')[0]
self.assertEquals(rql, 'Any S,AA,AB,AC,AD ORDERBY AA '
'WHERE S is CWUser, S login AA, S firstname AB, S surname AC, S modification_date AD, '
'A eid %(B)s, EXISTS(S identity A, NOT A in_group C, C name "guests", C is CWGroup)')
@@ -442,7 +442,7 @@
e['data_format'] = 'text/html'
e['data_encoding'] = 'ascii'
e._cw.transaction_data = {} # XXX req should be a session
- self.assertEquals(set(e.get_words()),
+ self.assertEquals(set(e.cw_adapt_to('IFTIndexable').get_words()),
set(['an', 'html', 'file', 'du', 'html', 'some', 'data']))
@@ -462,9 +462,9 @@
trinfo = self.execute('Any X WHERE X eid %(x)s', {'x': eid}).get_entity(0, 0)
trinfo.complete()
self.failUnless(isinstance(trinfo['creation_date'], datetime))
- self.failUnless(trinfo.relation_cached('from_state', 'subject'))
- self.failUnless(trinfo.relation_cached('to_state', 'subject'))
- self.failUnless(trinfo.relation_cached('wf_info_for', 'subject'))
+ self.failUnless(trinfo.cw_relation_cached('from_state', 'subject'))
+ self.failUnless(trinfo.cw_relation_cached('to_state', 'subject'))
+ self.failUnless(trinfo.cw_relation_cached('wf_info_for', 'subject'))
self.assertEquals(trinfo.by_transition, ())
def test_request_cache(self):
@@ -508,7 +508,7 @@
def test_metainformation_and_external_absolute_url(self):
req = self.request()
note = req.create_entity('Note', type=u'z')
- metainf = note.metainformation()
+ metainf = note.cw_metainformation()
self.assertEquals(metainf, {'source': {'adapter': 'native', 'uri': 'system'}, 'type': u'Note', 'extid': None})
self.assertEquals(note.absolute_url(), 'http://testing.fr/cubicweb/note/%s' % note.eid)
metainf['source'] = metainf['source'].copy()
--- a/test/unittest_rset.py Wed Jun 02 16:13:28 2010 +0200
+++ b/test/unittest_rset.py Wed Jun 02 16:15:05 2010 +0200
@@ -229,10 +229,10 @@
self.assertEquals(e['surname'], 'di mascio')
self.assertRaises(KeyError, e.__getitem__, 'firstname')
self.assertRaises(KeyError, e.__getitem__, 'creation_date')
- self.assertEquals(pprelcachedict(e._related_cache), [])
+ self.assertEquals(pprelcachedict(e._cw_related_cache), [])
e.complete()
self.assertEquals(e['firstname'], 'adrien')
- self.assertEquals(pprelcachedict(e._related_cache), [])
+ self.assertEquals(pprelcachedict(e._cw_related_cache), [])
def test_get_entity_advanced(self):
self.request().create_entity('Bookmark', title=u'zou', path=u'/view')
@@ -245,19 +245,19 @@
self.assertEquals(e['title'], 'zou')
self.assertRaises(KeyError, e.__getitem__, 'path')
self.assertEquals(e.view('text'), 'zou')
- self.assertEquals(pprelcachedict(e._related_cache), [])
+ self.assertEquals(pprelcachedict(e._cw_related_cache), [])
e = rset.get_entity(0, 1)
self.assertEquals(e.cw_row, 0)
self.assertEquals(e.cw_col, 1)
self.assertEquals(e['login'], 'anon')
self.assertRaises(KeyError, e.__getitem__, 'firstname')
- self.assertEquals(pprelcachedict(e._related_cache),
+ self.assertEquals(pprelcachedict(e._cw_related_cache),
[])
e.complete()
self.assertEquals(e['firstname'], None)
self.assertEquals(e.view('text'), 'anon')
- self.assertEquals(pprelcachedict(e._related_cache),
+ self.assertEquals(pprelcachedict(e._cw_related_cache),
[])
self.assertRaises(NotAnEntity, rset.get_entity, 0, 2)
@@ -269,7 +269,7 @@
seid = self.execute('State X WHERE X name "activated"')[0][0]
# for_user / in_group are prefetched in CWUser __init__, in_state should
# be filed from our query rset
- self.assertEquals(pprelcachedict(e._related_cache),
+ self.assertEquals(pprelcachedict(e._cw_related_cache),
[('in_state_subject', [seid])])
def test_get_entity_advanced_prefilled_cache(self):
@@ -279,7 +279,7 @@
'X title XT, S name SN, U login UL, X eid %s' % e.eid)
e = rset.get_entity(0, 0)
self.assertEquals(e['title'], 'zou')
- self.assertEquals(pprelcachedict(e._related_cache),
+ self.assertEquals(pprelcachedict(e._cw_related_cache),
[('created_by_subject', [5])])
# first level of recursion
u = e.created_by[0]
@@ -298,9 +298,9 @@
e = rset.get_entity(0, 0)
# if any of the assertion below fails with a KeyError, the relation is not cached
# related entities should be an empty list
- self.assertEquals(e.related_cache('primary_email', 'subject', True), ())
+ self.assertEquals(e._cw_relation_cache('primary_email', 'subject', True), ())
# related rset should be an empty rset
- cached = e.related_cache('primary_email', 'subject', False)
+ cached = e._cw_relation_cache('primary_email', 'subject', False)
self.assertIsInstance(cached, ResultSet)
self.assertEquals(cached.rowcount, 0)
--- a/test/unittest_schema.py Wed Jun 02 16:13:28 2010 +0200
+++ b/test/unittest_schema.py Wed Jun 02 16:15:05 2010 +0200
@@ -176,7 +176,7 @@
'CWCache', 'CWConstraint', 'CWConstraintType', 'CWEType',
'CWAttribute', 'CWGroup', 'EmailAddress', 'CWRelation',
'CWPermission', 'CWProperty', 'CWRType', 'CWUser',
- 'ExternalUri', 'File', 'Float', 'Image', 'Int', 'Interval', 'Note',
+ 'ExternalUri', 'File', 'Float', 'Int', 'Interval', 'Note',
'Password', 'Personne',
'RQLExpression',
'Societe', 'State', 'String', 'SubNote', 'SubWorkflowExitPoint',
--- a/test/unittest_selectors.py Wed Jun 02 16:13:28 2010 +0200
+++ b/test/unittest_selectors.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,15 +15,14 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""unit tests for selectors mechanism
-
-"""
+"""unit tests for selectors mechanism"""
from logilab.common.testlib import TestCase, unittest_main
+from cubicweb import Binary
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.appobject import Selector, AndSelector, OrSelector
-from cubicweb.selectors import implements, match_user_groups
+from cubicweb.selectors import implements, adaptable, match_user_groups
from cubicweb.interfaces import IDownloadable
from cubicweb.web import action
@@ -140,11 +139,12 @@
class ImplementsSelectorTC(CubicWebTC):
def test_etype_priority(self):
req = self.request()
- cls = self.vreg['etypes'].etype_class('File')
- anyscore = implements('Any').score_class(cls, req)
- idownscore = implements(IDownloadable).score_class(cls, req)
+ f = req.create_entity('File', data_name=u'hop.txt', data=Binary('hop'))
+ rset = f.as_rset()
+ anyscore = implements('Any')(f.__class__, req, rset=rset)
+ idownscore = adaptable('IDownloadable')(f.__class__, req, rset=rset)
self.failUnless(idownscore > anyscore, (idownscore, anyscore))
- filescore = implements('File').score_class(cls, req)
+ filescore = implements('File')(f.__class__, req, rset=rset)
self.failUnless(filescore > idownscore, (filescore, idownscore))
def test_etype_inheritance_no_yams_inheritance(self):
--- a/test/unittest_vregistry.py Wed Jun 02 16:13:28 2010 +0200
+++ b/test/unittest_vregistry.py Wed Jun 02 16:15:05 2010 +0200
@@ -56,21 +56,25 @@
def test_load_subinterface_based_appobjects(self):
- self.vreg.reset()
self.vreg.register_objects([join(BASE, 'web', 'views', 'iprogress.py')])
# check progressbar was kicked
self.failIf(self.vreg['views'].get('progressbar'))
+ # we've to emulate register_objects to add custom MyCard objects
+ path = [join(BASE, 'entities', '__init__.py'),
+ join(BASE, 'web', 'views', 'iprogress.py')]
+ filemods = self.vreg.init_registration(path, None)
+ for filepath, modname in filemods:
+ self.vreg.load_file(filepath, modname)
class MyCard(Card):
__implements__ = (IMileStone,)
- self.vreg.reset()
self.vreg._loadedmods[__name__] = {}
self.vreg.register(MyCard)
- self.vreg.register_objects([join(BASE, 'entities', '__init__.py'),
- join(BASE, 'web', 'views', 'iprogress.py')])
+ self.vreg.initialization_completed()
# check progressbar isn't kicked
self.assertEquals(len(self.vreg['views']['progressbar']), 1)
def test_properties(self):
+ self.vreg.reset()
self.failIf('system.version.cubicweb' in self.vreg['propertydefs'])
self.failUnless(self.vreg.property_info('system.version.cubicweb'))
self.assertRaises(UnknownProperty, self.vreg.property_info, 'a.non.existent.key')
--- a/view.py Wed Jun 02 16:13:28 2010 +0200
+++ b/view.py Wed Jun 02 16:15:05 2010 +0200
@@ -520,3 +520,37 @@
# XXX a generic '%s%s' % (self.__regid__, self.__registry__.capitalize()) would probably be nicer
def div_id(self):
return '%sComponent' % self.__regid__
+
+
+class Adapter(AppObject):
+ """base class for adapters"""
+ __registry__ = 'adapters'
+
+
+class EntityAdapter(Adapter):
+ """base class for entity adapters (eg adapt an entity to an interface)"""
+ def __init__(self, _cw, **kwargs):
+ try:
+ self.entity = kwargs.pop('entity')
+ except KeyError:
+ self.entity = kwargs['rset'].get_entity(kwargs.get('row') or 0,
+ kwargs.get('col') or 0)
+ Adapter.__init__(self, _cw, **kwargs)
+
+
+def implements_adapter_compat(iface):
+ def _pre39_compat(func):
+ def decorated(self, *args, **kwargs):
+ entity = self.entity
+ if hasattr(entity, func.__name__):
+ warn('[3.9] %s method is deprecated, define it on a custom '
+ '%s for %s instead' % (func.__name__, iface,
+ entity.__class__),
+ DeprecationWarning)
+ member = getattr(entity, func.__name__)
+ if callable(member):
+ return member(*args, **kwargs)
+ return member
+ return func(self, *args, **kwargs)
+ return decorated
+ return _pre39_compat
--- a/vregistry.py Wed Jun 02 16:13:28 2010 +0200
+++ b/vregistry.py Wed Jun 02 16:15:05 2010 +0200
@@ -45,7 +45,7 @@
from cubicweb import CW_SOFTWARE_ROOT
from cubicweb import RegistryNotFound, ObjectNotFound, NoSelectableObject
-from cubicweb.appobject import AppObject
+from cubicweb.appobject import AppObject, class_regid
def _toload_info(path, extrapath, _toload=None):
"""return a dictionary of <modname>: <modpath> and an ordered list of
@@ -84,16 +84,6 @@
"""returns a unique identifier for an appobject class"""
return '%s.%s' % (cls.__module__, cls.__name__)
-def class_regid(cls):
- """returns a unique identifier for an appobject class"""
- if 'id' in cls.__dict__:
- warn('[3.6] %s.%s: id is deprecated, use __regid__'
- % (cls.__module__, cls.__name__), DeprecationWarning)
- cls.__regid__ = cls.id
- if hasattr(cls, 'id') and not isinstance(cls.id, property):
- return cls.id
- return cls.__regid__
-
def class_registries(cls, registryname):
if registryname:
return (registryname,)
@@ -236,8 +226,8 @@
% (args, kwargs.keys(),
[repr(v) for v in appobjects]))
if len(winners) > 1:
- # log in production environement, error while debugging
- if self.config.debugmode:
+ # log in production environement / test, error while debugging
+ if self.config.debugmode or self.config.mode == 'test':
raise Exception('select ambiguity, args: %s\nkwargs: %s %s'
% (args, kwargs.keys(),
[repr(v) for v in winners]))
@@ -406,6 +396,7 @@
# initialization methods ###################################################
def init_registration(self, path, extrapath=None):
+ self.reset()
# compute list of all modules that have to be loaded
self._toloadmods, filemods = _toload_info(path, extrapath)
# XXX is _loadedmods still necessary ? It seems like it's useful
--- a/web/__init__.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/__init__.py Wed Jun 02 16:15:05 2010 +0200
@@ -17,9 +17,8 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""CubicWeb web client core. You'll need a apache-modpython or twisted
publisher to get a full CubicWeb web application
-
+"""
-"""
__docformat__ = "restructuredtext en"
_ = unicode
--- a/web/action.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/action.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,9 +15,8 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""abstract action classes for CubicWeb web client
+"""abstract action classes for CubicWeb web client"""
-"""
__docformat__ = "restructuredtext en"
_ = unicode
--- a/web/application.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/application.py Wed Jun 02 16:15:05 2010 +0200
@@ -235,7 +235,7 @@
def _update_last_login_time(self, req):
# XXX should properly detect missing permission / non writeable source
# and avoid "except (RepositoryError, Unauthorized)" below
- if req.user.metainformation()['source']['adapter'] == 'ldapuser':
+ if req.user.cw_metainformation()['source']['adapter'] == 'ldapuser':
return
try:
req.execute('SET X last_login_time NOW WHERE X eid %(x)s',
@@ -283,12 +283,12 @@
to publish HTTP request.
"""
- def __init__(self, config, debug=None,
+ def __init__(self, config,
session_handler_fact=CookieSessionHandler,
vreg=None):
self.info('starting web instance from %s', config.apphome)
if vreg is None:
- vreg = cwvreg.CubicWebVRegistry(config, debug=debug)
+ vreg = cwvreg.CubicWebVRegistry(config)
self.vreg = vreg
# connect to the repository and get instance's schema
self.repo = config.repository(vreg)
--- a/web/controller.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/controller.py Wed Jun 02 16:15:05 2010 +0200
@@ -106,6 +106,16 @@
view.set_http_cache_headers()
self._cw.validate_cache()
+ def sendmail(self, recipient, subject, body):
+ senderemail = self._cw.user.cw_adapt_to('IEmailable').get_email()
+ msg = format_mail({'email' : senderemail,
+ 'name' : self._cw.user.dc_title(),},
+ [recipient], body, subject)
+ if not self._cw.vreg.config.sendmails([(msg, [recipient])]):
+ msg = self._cw._('could not connect to the SMTP server')
+ url = self._cw.build_url(__message=msg)
+ raise Redirect(url)
+
def reset(self):
"""reset form parameters and redirect to a view determinated by given
parameters
--- a/web/data/cubicweb.css Wed Jun 02 16:13:28 2010 +0200
+++ b/web/data/cubicweb.css Wed Jun 02 16:15:05 2010 +0200
@@ -3,82 +3,61 @@
* :copyright: 2003-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
* :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
*/
+
/***************************************/
-/* xhtml tags styles */
+/* xhtml tags */
/***************************************/
-*{
- margin:0px;
- padding :0px;
+/* scale and rhythm cf http://lamb.cc/typograph/ */
+body {
+ font-family: %(defaultFont)s;
+ font-size: %(defaultSize)s;
+ line-height: %(defaultLineHeight)s;
+ color: %(defaultColor)s;
}
+h1, h2, h3 { margin-top:0; margin-bottom:0; }
+
+/* got rhythm ? beat of 12*1.25 = 15 px */
+.rhythm_bg { background: url(/data/%(baseRhythmBg)s) repeat ! important; }
+
+/* scale 3:5 stranded */
+/* h1 { font-size:2em; } */
+/* h2 { font-size:1.61538em; } */
+/* h3 { font-size:1.23077em; } */
+
+/* scale le corbusier */
+/* h1 { font-size:2.11538em; } */
+/* h2 { font-size:1.61538em; } */
+/* h3 { font-size:1.30769em; } */
+
+/* scale traditional */
+h1 { font-size: %(h1FontSize)s; }
+h2 { font-size: %(h2FontSize)s; }
+h3 { font-size: %(h3FontSize)s; }
+
+/* paddings */
+h1 {
+ border-bottom: %(h1BorderBottomStyle)s;
+ padding: %(h1Padding)s;
+ margin: %(h1Margin)s;
+}
+h2 { padding: %(h2Padding)s; }
+h3 { padding: %(h3Padding)s; }
html, body {
background: #e2e2e2;
}
-body {
- font-size: 69%;
- font-weight: normal;
- font-family: Verdana, sans-serif;
-}
-
-h1 {
- font-size: 188%;
- margin: 0.2em 0px 0.3em;
- border-bottom: 1px solid #000;
-}
-
-h2, h3 {
- margin-top: 0.2em;
- margin-bottom: 0.3em;
-}
-
-h2 {
- font-size: 135%;
-}
-
-h3 {
- font-size: 130%;
-}
-
-h4 {
- font-size: 120%;
- margin: 0.2em 0px;
-}
-
-h5 {
- font-size:110%;
-}
-
-h6{
- font-size:105%;
-}
-
a, a:active, a:visited, a:link {
- color: #ff4500;
+ color: %(aColor)s;
text-decoration: none;
}
-a:hover{
+a:hover {
text-decoration: underline;
}
-a img, img {
- border: none;
- text-align: center;
-}
-
-p {
- margin: 0em 0px 0.2em;
- padding-top: 2px;
-}
-
-table, td, input, select{
- font-size: 100%;
-}
-
table {
- border-collapse: collapse;
border: none;
}
@@ -86,84 +65,58 @@
vertical-align: top;
}
-table td img {
- vertical-align: middle;
- margin-right: 10px;
-}
-
-ol {
- margin: 1px 0px 1px 16px;
-}
-
-ul{
- margin: 1px 0px 1px 4px;
- list-style-type: none;
-}
-
-ul li {
- margin-top: 2px;
- padding: 0px 0px 2px 8px;
- background: url("bullet_orange.png") 0% 6px no-repeat;
-}
-
-dt {
- font-size:1.17em;
- font-weight:600;
-}
-
-dd {
- margin: 0.6em 0 1.5em 2em;
-}
-
-fieldset {
- border: none;
-}
-
-legend {
- padding: 0px 2px;
- font: bold 1em Verdana, sans-serif;
-}
-
-input, textarea {
- padding: 0.2em;
- vertical-align: middle;
- border: 1px solid #ccc;
-}
-
-input:focus {
- border: 1px inset #ff7700;
-}
-
label, .label {
font-weight: bold;
}
-iframe {
- border: 0px;
+pre {
+ clear: both;
+ font-family: 'Courier New', monospace;
+ letter-spacing: 0.015em;
+ padding: 0.5em;
+ margin: 0 1.5em 1.5em;
+ background-color: #f0f0f0;
+ border: 1px solid #ccbca7;
}
-pre {
- font-family: Courier, "Courier New", Monaco, monospace;
- font-size: 100%;
- color: #000;
- background-color: #f2f2f2;
- border: 1px solid #ccc;
+p {
+ text-align: justify;
+ margin-bottom: %(defaultLineHeightEm)s;
+}
+
+ol, ul {
+ list-style-type: disc;
+ margin-bottom: %(defaultLineHeightEm)s;
}
-code {
- font-size: 120%;
- color: #000;
- background-color: #f2f2f2;
- border: 1px solid #ccc;
+ol ol,
+ul ul{
+ margin-left: 8px;
+ margin-bottom : 0px;
+}
+
+p + ul {
+ margin-top: -%(defaultLineHeightEm)s;
+}
+
+li {
+ margin-left: 1.5em;
}
-blockquote {
- font-family: Courier, "Courier New", serif;
- font-size: 120%;
- margin: 5px 0px;
- padding: 0.8em;
- background-color: #f2f2f2;
- border: 1px solid #ccc;
+h2 a, h2 a:active, h2 a:visited, h2 a:link,
+h3 a, h3 a:active, h3 a:visited, h3 a:link {
+ color: inherit;
+ text-decoration: none;
+}
+
+input, textarea {
+ border: 1px solid %(pageContentBorderColor)s;
+ padding: 0.1em;
+ vertical-align: middle;
+}
+
+input:focus {
+ border: 1px inset %(headerBgColor)s;
}
/***************************************/
@@ -179,8 +132,8 @@
}
.hr {
- border-bottom: 1px dotted #ccc;
- margin: 1em 0px;
+ border-bottom: 1px dotted %(pageContentBorderColor)s;
+ height: 17px;
}
.left {
@@ -200,14 +153,16 @@
visibility: hidden;
}
-li.invisible { list-style: none; background: none; padding: 0px 0px
-1px 1px; }
+li.invisible {
+ list-style: none;
+ background: none;
+ padding: 0px 0px 1px 1px;
+}
li.invisible div{
display: inline;
}
-
/***************************************/
/* LAYOUT */
/***************************************/
@@ -215,7 +170,7 @@
/* header */
table#header {
- background: #ff7700 url("banner.png") left top repeat-x;
+ background: %(headerBgColor)s url("banner.png") left top repeat-x;
text-align: left;
}
@@ -224,86 +179,94 @@
}
table#header a {
-color: #000;
+ color: %(defaultColor)s;
+}
+
+table#header img#logo{
+ vertical-align: middle;
}
span#appliName {
- font-weight: bold;
- color: #000;
- white-space: nowrap;
+ font-weight: bold;
+ color: %(defaultColor)s;
+ white-space: nowrap;
}
table#header td#headtext {
width: 100%;
}
+/* Popup on login box and userActionBox */
+div.popupWrapper{
+ position:relative;
+ z-index:100;
+}
+
+div.popup {
+ position: absolute;
+ background: #fff;
+ border: 1px solid #fff;
+ text-align: left;
+ z-index: 400;
+}
+
+div.popup ul li a {
+ text-decoration: none;
+ color: #000;
+}
+
/* FIXME appear with 4px width in IE6 */
div#stateheader{
min-width: 66%;
}
-/* Popup on login box and userActionBox */
-div.popupWrapper{
- position:relative;
- z-index:100;
-}
-
-div.popup {
- position: absolute;
- background: #fff;
- border: 1px solid black;
- text-align: left;
- z-index:400;
-}
-
-div.popup ul li a {
- text-decoration: none;
- color: black;
-}
-
/* main zone */
div#page {
- background: #e2e2e2;
- position: relative;
- min-height: 800px;
+ min-height: %(pageMinHeight)s;
+ margin: %(defaultLayoutMargin)s;
}
-table#mainLayout{
- margin:0px 3px;
+table#mainLayout #navColumnLeft {
+ width: 16em;
+ padding-right: %(defaultLayoutMargin)s;
+}
+
+table#mainLayout #navColumnRight {
+ width: 16em;
+ padding-left: %(defaultLayoutMargin)s;
}
-table#mainLayout td#contentcol {
- padding: 8px 10px 5px;
+div#pageContent {
+ clear: both;
+/* margin-top:-1px; /* enable when testing rhythm */
+ background: %(pageContentBgColor)s;
+ border: 1px solid %(pageContentBorderColor)s;
+ padding: 0px %(pageContentPadding)s %(pageContentPadding)s;
}
-table#mainLayout td.navcol {
- width: 16em;
+
+div#breadcrumbs {
+ padding: %(pageContentPadding)s 0 0 0;
}
+/*FIXME */
#contentheader {
margin: 0px;
padding: 0.2em 0.5em 0.5em 0.5em;
}
#contentheader a {
- color: #000;
-}
-
-div#pageContent {
- clear: both;
- padding: 10px 1em 2em;
- background: #ffffff;
- border: 1px solid #ccc;
+ color: %(defaultColor)s;
}
/* rql bar */
div#rqlinput {
- border: 1px solid #cfceb7;
margin-bottom: 8px;
padding: 3px;
- background: #cfceb7;
+ background: %(actionBoxTitleBgColor)s;
+ border: 1px solid %(actionBoxTitleBgColor)s;
}
input#rql{
@@ -311,26 +274,17 @@
}
/* boxes */
-div.navboxes {
- margin-top: 8px;
-}
div.boxFrame {
width: 100%;
}
div.boxTitle {
- padding-top: 0px;
- padding-bottom: 0.2em;
+ overflow: hidden;
font: bold 100% Georgia;
- overflow: hidden;
color: #fff;
- background: #ff9900 url("search.png") left bottom repeat-x;
-}
-
-div.searchBoxFrame div.boxTitle,
-div.greyBoxFrame div.boxTitle {
- background: #cfceb7;
+ padding: 0px 0px 0.2em;
+ background: %(headerBgColor)s url("search.png") left bottom repeat-x;
}
div.boxTitle span,
@@ -339,14 +293,19 @@
white-space: nowrap;
}
+div.searchBoxFrame div.boxTitle,
+div.greyBoxFrame div.boxTitle {
+ background: %(actionBoxTitleBgColor)s;
+}
+
div.sideBoxTitle span,
div.searchBoxFrame div.boxTitle span,
div.greyBoxFrame div.boxTitle span {
- color: #222211;
+ color: %(defaultColor)s;
}
.boxFrame a {
- color: #000;
+ color: %(defaultColor)s;
}
div.boxContent {
@@ -355,6 +314,21 @@
border-top: none;
}
+a.boxMenu {
+ display: block;
+ padding: 1px 9px 1px 3px;
+ background: transparent url("puce_down.png") 98% 6px no-repeat;
+}
+a.boxMenu:hover {
+ background: %(sideBoxBodyBgColor)s url("puce_down.png") 98% 6px no-repeat;
+ cursor: pointer;
+}
+
+a.popupMenu {
+ background: transparent url("puce_down_black.png") 2% 6px no-repeat;
+ padding-left: 2em;
+}
+
ul.boxListing {
margin: 0px;
padding: 0px 3px;
@@ -362,65 +336,47 @@
ul.boxListing li,
ul.boxListing ul li {
- display: inline;
+ list-style-type: none;
margin: 0px;
padding: 0px;
background-image: none;
}
ul.boxListing ul {
- margin: 0px 0px 0px 7px;
padding: 1px 3px;
}
ul.boxListing a {
- color: #000;
- display: block;
+ color: %(defaultColor)s;
padding: 1px 9px 1px 3px;
}
ul.boxListing .selected {
- color: #FF4500;
+ color: %(aColor)s;
font-weight: bold;
}
ul.boxListing a.boxBookmark:hover,
ul.boxListing a:hover,
ul.boxListing ul li a:hover {
+ color: #111100;
text-decoration: none;
- background: #eeedd9;
- color: #111100;
+ background: %(sideBoxBodyBgColor)s;
}
ul.boxListing a.boxMenu:hover {
- background: #eeedd9 url(puce_down.png) no-repeat scroll 98% 6px;
- cursor:pointer;
- border-top:medium none;
- }
-a.boxMenu {
- background: transparent url("puce_down.png") 98% 6px no-repeat;
- display: block;
- padding: 1px 9px 1px 3px;
-}
-
-
-a.popupMenu {
- background: transparent url("puce_down_black.png") 2% 6px no-repeat;
- padding-left: 2em;
+ cursor: pointer;
+ border-top: medium none;
+ background: %(sideBoxBodyBgColor)s url(puce_down.png) no-repeat scroll 98% 6px;
}
ul.boxListing ul li a:hover {
- background: #eeedd9 url("bullet_orange.png") 0% 6px no-repeat;
-}
-
-a.boxMenu:hover {
- background: #eeedd9 url("puce_down.png") 98% 6px no-repeat;
- cursor: pointer;
+ background: %(sideBoxBodyBgColor)s url("bullet_orange.png") 0% 6px no-repeat;
}
ul.boxListing a.boxBookmark {
padding-left: 3px;
- background-image:none;
+ background-image: none;
background:#fff;
}
@@ -440,7 +396,7 @@
}
div.sideBoxTitle {
- background: #cfceb7;
+ background: %(actionBoxTitleBgColor)s;
display: block;
font: bold 100% Georgia;
}
@@ -450,15 +406,20 @@
margin-bottom: 0.5em;
}
+ul.sideBox,
+ul.sideBox ul{
+ margin-bottom: 0px;
+}
+
ul.sideBox li{
- list-style: none;
- background: none;
+ list-style-type : none;
padding: 0px 0px 1px 1px;
- }
+ margin: 1px 0 1px 4px;
+}
div.sideBoxBody {
padding: 0.2em 5px;
- background: #eeedd9;
+ background: %(sideBoxBodyBgColor)s;
}
div.sideBoxBody a {
@@ -474,10 +435,10 @@
}
input.rqlsubmit{
- background: #fffff8 url("go.png") 50% 50% no-repeat;
+ margin: 0px;
width: 20px;
height: 20px;
- margin: 0px;
+ background: %(buttonBgColor)s url("go.png") 50% 50% no-repeat;
}
input#norql{
@@ -497,7 +458,7 @@
}
div#userActionsBox a.popupMenu {
- color: black;
+ color: #000;
text-decoration: underline;
padding-right: 2em;
}
@@ -521,7 +482,7 @@
/**************/
div#etyperestriction {
margin-bottom: 1ex;
- border-bottom: 1px solid #ccc;
+ border-bottom: 1px solid %(pageContentBorderColor)s;
}
span.slice a:visited,
@@ -531,7 +492,7 @@
span.selectedSlice a:visited,
span.selectedSlice a {
- color: #000;
+ color: %(defaultColor)s;
}
/* FIXME should be moved to cubes/folder */
@@ -546,19 +507,13 @@
}
div.prevnext a {
- color: #000;
+ color: %(defaultColor)s;
}
/***************************************/
/* entity views */
/***************************************/
-.mainInfo {
- margin-right: 1em;
- padding: 0.2em;
-}
-
-
div.mainRelated {
border: none;
margin-right: 1em;
@@ -577,7 +532,7 @@
}
div.section {
- margin-top: 0.5em;
+/* margin-top: 0.5em; */
width:100%;
}
@@ -611,50 +566,44 @@
.warning,
.message,
-.errorMessage ,
-.searchMessage{
+.errorMessage{
padding: 0.3em 0.3em 0.3em 1em;
font-weight: bold;
}
-.simpleMessage {
- margin: 4px 0px;
- font-weight: bold;
- color: #ff7700;
+.searchMessage{
+ padding-top: %(defaultLayoutMargin)s;
}
-div#appMsg, div.appMsg {
- border: 1px solid #cfceb7;
- margin-bottom: 8px;
- padding: 3px;
- background: #f8f8ee;
+.loginMessage {
+ margin: 4px 0px;
+ font-weight: bold;
+ color: %(headerBgColor)s;
+}
+
+div#appMsg {
+ border: 1px solid %(actionBoxTitleBgColor)s;
+ margin-bottom: %(defaultLayoutMargin)s;
}
.message {
margin: 0px;
- background: #f8f8ee url("information.png") 5px center no-repeat;
+ background: #fff url("information.png") 5px center no-repeat;
padding-left: 15px;
}
.errorMessage {
margin: 10px 0px;
padding-left: 25px;
- background: #f7f6f1 url("critical.png") 2px center no-repeat;
+ background: #fff url("critical.png") 2px center no-repeat;
color: #ed0d0d;
- border: 1px solid #cfceb7;
-}
-
-.searchMessage {
- margin-top: 0.5em;
- border-top: 1px solid #cfceb7;
- background: #eeedd9 url("information.png") 0% 50% no-repeat; /*dcdbc7*/
+ border: 1px solid %(actionBoxTitleBgColor)s;
}
.stateMessage {
- border: 1px solid #ccc;
- background: #f8f8ee url("information.png") 10px 50% no-repeat;
+ border: 1px solid %(pageContentBorderColor)s;
+ background: #fff url("information.png") 10px 50% no-repeat;
padding:4px 0px 4px 20px;
- border-width: 1px 0px 1px 0px;
}
/* warning messages like "There are too many results ..." */
@@ -668,8 +617,8 @@
position: fixed;
right: 5px;
top: 0px;
- background: #222211;
- color: white;
+ background: %(defaultColor)s;
+ color: #fff;
font-weight: bold;
display: none;
}
@@ -679,64 +628,59 @@
/***************************************/
table.listing {
+ width: 100%;
padding: 10px 0em;
- color: #000;
- width: 100%;
- border-right: 1px solid #dfdfdf;
+ color: %(defaultColor)s;
+ border: 1px solid %(listingBorderColor)s;
+}
+
+table.listing tr th {
+ font-weight: bold;
+ background: #dfdfdf;
+ font-size: 8pt;
+ padding: 3px 0px 3px 5px;
+ border: 1px solid %(listingBorderColor)s;
+ border-right:none}
+
+table.listing thead tr {
+/* border: 1px solid #dfdfdf; */
}
table.listing thead th.over {
- background-color: #746B6B;
+ background-color: #746b6b;
cursor: pointer;
}
-table.listing tr th {
- border: 1px solid #dfdfdf;
- border-right:none;
- font-size: 8pt;
- padding: 4px;
-}
-
table.listing tr .header {
- border-right: 1px solid #dfdfdf;
+ border-right: 1px solid %(listingBorderColor)s;
cursor: pointer;
}
table.listing td {
- color: #3D3D3D;
padding: 4px;
- background-color: #FFF;
+ padding: 3px 0px 3px 5px;
vertical-align: top;
-}
-
-table.listing th,
-table.listing td {
- padding: 3px 0px 3px 5px;
- border: 1px solid #dfdfdf;
+ border: 1px solid %(listingBorderColor)s;
border-right: none;
-}
-
-table.listing th {
- font-weight: bold;
- background: #ebe8d9 url("button.png") repeat-x;
+ background-color: #fff;
}
table.listing td a,
table.listing td a:visited {
- color: #666;
+ color: %(defaultColor)s;
}
table.listing a:hover,
table.listing tr.highlighted td a {
- color:#000;
+ color:%(defaultColor)s;
}
table.listing td.top {
- border: 1px solid white;
+ border: 1px solid #fff;
border-bottom: none;
text-align: right ! important;
- /* insane IE row bug workaround */
+ /* insane IE row bug workraound */
position: relative;
left: -1px;
top: -1px;
@@ -790,7 +734,7 @@
}
#add_newopt{
- background: #fffff8 url("go.png") 50% 50% no-repeat;
+ background: %(buttonBgColor)s url("go.png") 50% 50% no-repeat;
width: 20px;
line-height: 20px;
display:block;
@@ -803,9 +747,9 @@
input.button{
margin: 1em 1em 0px 0px;
- border: 1px solid #edecd2;
- border-color:#edecd2 #cfceb7 #cfceb7 #edecd2;
- background: #fffff8 url("button.png") bottom left repeat-x;
+ border: 1px solid %(buttonBorderColor)s;
+ border-color: %(buttonBorderColor)s %(actionBoxTitleBgColor)s %(actionBoxTitleBgColor)s %(buttonBorderColor)s;
+ background: %(buttonBgColor)s url("button.png") bottom left repeat-x;
}
/* FileItemInnerView jquery.treeview.css */
@@ -818,11 +762,11 @@
/* footer */
/***************************************/
-div.footer {
+div#footer {
text-align: center;
}
-div.footer a {
- color: #000;
+div#footer a {
+ color: %(defaultColor)s;
text-decoration: none;
}
@@ -841,15 +785,15 @@
/***************************************/
.title {
text-align: left;
- font-size: large;
+ font-size: large;
font-weight: bold;
}
.validateButton {
margin: 1em 1em 0px 0px;
- border: 1px solid #edecd2;
- border-color:#edecd2 #cfceb7 #cfceb7 #edecd2;
- background: #fffff8 url("button.png") bottom left repeat-x;
+ border: 1px solid %(buttonBorderColor)s;
+ border-color: %(buttonBorderColor)s %(actionBoxTitleBgColor)s %(actionBoxTitleBgColor)s %(buttonBorderColor)s;
+ background: %(buttonBgColor)s url("button.png") bottom left repeat-x;
}
/********************************/
@@ -859,3 +803,14 @@
.otherView {
float: right;
}
+
+/********************************/
+/* overwite other css here */
+/********************************/
+
+/* ui.tabs.css */
+ul .ui-tabs-nav,
+ul .ui-tabs-panel {
+ font-family: %(defaultFont)s;
+ font-size: %(defaultSize)s;
+}
--- a/web/data/cubicweb.manageview.css Wed Jun 02 16:13:28 2010 +0200
+++ b/web/data/cubicweb.manageview.css Wed Jun 02 16:15:05 2010 +0200
@@ -6,9 +6,9 @@
width: 100%;
}
-table.startup td {
- padding: 0.1em 0.2em;
-}
+/* table.startup td { */
+/* padding: 0.1em 0.2em; */
+/* } */
table.startup td.addcol {
text-align: right;
@@ -16,7 +16,5 @@
}
table.startup th{
- padding-top: 3px;
- padding-bottom: 3px;
text-align: left;
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/cubicweb.old.css Wed Jun 02 16:15:05 2010 +0200
@@ -0,0 +1,862 @@
+/*
+ * :organization: Logilab
+ * :copyright: 2003-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+ * :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+ */
+
+/***************************************/
+/* xhtml tags */
+/***************************************/
+* {
+ margin: 0px;
+ padding: 0px;
+}
+
+html, body {
+ background: #e2e2e2;
+}
+
+body {
+ font-size: 69%;
+ font-weight: normal;
+ font-family: Verdana, sans-serif;
+}
+
+h1 {
+ font-size: 188%;
+ margin: 0.2em 0px 0.3em;
+ border-bottom: 1px solid #000;
+}
+
+h2, h3 {
+ margin-top: 0.2em;
+ margin-bottom: 0.3em;
+}
+
+h2 {
+ font-size: 135%;
+}
+
+h3 {
+ font-size: 130%;
+}
+
+h4 {
+ font-size: 120%;
+ margin: 0.2em 0px;
+}
+
+h5 {
+ font-size:110%;
+}
+
+h6{
+ font-size:105%;
+}
+
+a, a:active, a:visited, a:link {
+ color: #ff4500;
+ text-decoration: none;
+}
+
+a:hover{
+ text-decoration: underline;
+}
+
+a img, img {
+ border: none;
+ text-align: center;
+}
+
+p {
+ margin: 0em 0px 0.2em;
+ padding-top: 2px;
+}
+
+table, td, input, select{
+ font-size: 100%;
+}
+
+table {
+ border-collapse: collapse;
+ border: none;
+}
+
+table th, table td {
+ vertical-align: top;
+}
+
+table td img {
+ vertical-align: middle;
+ margin-right: 10px;
+}
+
+ol {
+ margin: 1px 0px 1px 16px;
+}
+
+ul{
+ margin: 1px 0px 1px 4px;
+ list-style-type: none;
+}
+
+ul li {
+ margin-top: 2px;
+ padding: 0px 0px 2px 8px;
+ background: url("bullet_orange.png") 0% 6px no-repeat;
+}
+
+dt {
+ font-size:1.17em;
+ font-weight:600;
+}
+
+dd {
+ margin: 0.6em 0 1.5em 2em;
+}
+
+fieldset {
+ border: none;
+}
+
+legend {
+ padding: 0px 2px;
+ font: bold 1em Verdana, sans-serif;
+}
+
+input, textarea {
+ padding: 0.2em;
+ vertical-align: middle;
+ border: 1px solid #ccc;
+}
+
+input:focus {
+ border: 1px inset #ff7700;
+}
+
+label, .label {
+ font-weight: bold;
+}
+
+iframe {
+ border: 0px;
+}
+
+pre {
+ font-family: Courier, "Courier New", Monaco, monospace;
+ font-size: 100%;
+ color: #000;
+ background-color: #f2f2f2;
+ border: 1px solid #ccc;
+}
+
+code {
+ font-size: 120%;
+ color: #000;
+ background-color: #f2f2f2;
+ border: 1px solid #ccc;
+}
+
+blockquote {
+ font-family: Courier, "Courier New", serif;
+ font-size: 120%;
+ margin: 5px 0px;
+ padding: 0.8em;
+ background-color: #f2f2f2;
+ border: 1px solid #ccc;
+}
+
+/***************************************/
+/* generic classes */
+/***************************************/
+
+.odd {
+ background-color: #f7f6f1;
+}
+
+.even {
+ background-color: transparent;
+}
+
+.hr {
+ border-bottom: 1px dotted #ccc;
+ margin: 1em 0px;
+}
+
+.left {
+ float: left;
+}
+
+.right {
+ float: right;
+}
+
+.clear {
+ clear: both;
+}
+
+.hidden {
+ display: none;
+ visibility: hidden;
+}
+
+li.invisible { list-style: none; background: none; padding: 0px 0px
+1px 1px; }
+
+li.invisible div{
+ display: inline;
+}
+
+
+/***************************************/
+/* LAYOUT */
+/***************************************/
+
+/* header */
+
+table#header {
+ background: #ff7700 url("banner.png") left top repeat-x;
+ text-align: left;
+}
+
+table#header td {
+ vertical-align: middle;
+}
+
+table#header a {
+color: #000;
+}
+
+span#appliName {
+ font-weight: bold;
+ color: #000;
+ white-space: nowrap;
+}
+
+table#header td#headtext {
+ width: 100%;
+}
+
+/* FIXME appear with 4px width in IE6 */
+div#stateheader{
+ min-width: 66%;
+}
+
+/* Popup on login box and userActionBox */
+div.popupWrapper{
+ position:relative;
+ z-index:100;
+}
+
+div.popup {
+ position: absolute;
+ background: #fff;
+ border: 1px solid black;
+ text-align: left;
+ z-index:400;
+}
+
+div.popup ul li a {
+ text-decoration: none;
+ color: black;
+}
+
+/* main zone */
+
+div#page {
+ background: #e2e2e2;
+ position: relative;
+ min-height: 800px;
+}
+
+table#mainLayout{
+ margin:0px 3px;
+}
+
+table#mainLayout td#contentColumn {
+ padding: 8px 10px 5px;
+}
+
+table#mainLayout td#navColumnLeft,
+table#mainLayout td#navColumnRight {
+ width: 16em;
+}
+
+#contentheader {
+ margin: 0px;
+ padding: 0.2em 0.5em 0.5em 0.5em;
+}
+
+#contentheader a {
+ color: #000;
+}
+
+div#pageContent {
+ clear: both;
+ padding: 10px 1em 2em;
+ background: #ffffff;
+ border: 1px solid #ccc;
+}
+
+/* rql bar */
+
+div#rqlinput {
+ border: 1px solid #cfceb7;
+ margin-bottom: 8px;
+ padding: 3px;
+ background: #cfceb7;
+}
+
+input#rql{
+ width: 95%;
+}
+
+/* boxes */
+div.navboxes {
+ margin-top: 8px;
+}
+
+div.boxFrame {
+ width: 100%;
+}
+
+div.boxTitle {
+ padding-top: 0px;
+ padding-bottom: 0.2em;
+ font: bold 100% Georgia;
+ overflow: hidden;
+ color: #fff;
+ background: #ff9900 url("search.png") left bottom repeat-x;
+}
+
+div.searchBoxFrame div.boxTitle,
+div.greyBoxFrame div.boxTitle {
+ background: #cfceb7;
+}
+
+div.boxTitle span,
+div.sideBoxTitle span {
+ padding: 0px 5px;
+ white-space: nowrap;
+}
+
+div.sideBoxTitle span,
+div.searchBoxFrame div.boxTitle span,
+div.greyBoxFrame div.boxTitle span {
+ color: #222211;
+}
+
+.boxFrame a {
+ color: #000;
+}
+
+div.boxContent {
+ padding: 3px 0px;
+ background: #fff;
+ border-top: none;
+}
+
+ul.boxListing {
+ margin: 0px;
+ padding: 0px 3px;
+}
+
+ul.boxListing li,
+ul.boxListing ul li {
+ display: inline;
+ margin: 0px;
+ padding: 0px;
+ background-image: none;
+}
+
+ul.boxListing ul {
+ margin: 0px 0px 0px 7px;
+ padding: 1px 3px;
+}
+
+ul.boxListing a {
+ color: #000;
+ display: block;
+ padding: 1px 9px 1px 3px;
+}
+
+ul.boxListing .selected {
+ color: #FF4500;
+ font-weight: bold;
+}
+
+ul.boxListing a.boxBookmark:hover,
+ul.boxListing a:hover,
+ul.boxListing ul li a:hover {
+ text-decoration: none;
+ background: #eeedd9;
+ color: #111100;
+}
+
+ul.boxListing a.boxMenu:hover {
+ background: #eeedd9 url(puce_down.png) no-repeat scroll 98% 6px;
+ cursor:pointer;
+ border-top:medium none;
+ }
+a.boxMenu {
+ background: transparent url("puce_down.png") 98% 6px no-repeat;
+ display: block;
+ padding: 1px 9px 1px 3px;
+}
+
+
+a.popupMenu {
+ background: transparent url("puce_down_black.png") 2% 6px no-repeat;
+ padding-left: 2em;
+}
+
+ul.boxListing ul li a:hover {
+ background: #eeedd9 url("bullet_orange.png") 0% 6px no-repeat;
+}
+
+a.boxMenu:hover {
+ background: #eeedd9 url("puce_down.png") 98% 6px no-repeat;
+ cursor: pointer;
+}
+
+ul.boxListing a.boxBookmark {
+ padding-left: 3px;
+ background-image:none;
+ background:#fff;
+}
+
+ul.boxListing ul li a {
+ background: #fff url("bullet_orange.png") 0% 6px no-repeat;
+ padding: 1px 3px 0px 10px;
+}
+
+div.searchBoxFrame div.boxContent {
+ padding: 4px 4px 3px;
+ background: #f0eff0 url("gradient-grey-up.png") left top repeat-x;
+}
+
+div.shadow{
+ height: 14px;
+ background: url("shadow.gif") no-repeat top right;
+}
+
+div.sideBoxTitle {
+ background: #cfceb7;
+ display: block;
+ font: bold 100% Georgia;
+}
+
+div.sideBox {
+ padding: 0 0 0.2em;
+ margin-bottom: 0.5em;
+}
+
+ul.sideBox li{
+ list-style: none;
+ background: none;
+ padding: 0px 0px 1px 1px;
+ }
+
+div.sideBoxBody {
+ padding: 0.2em 5px;
+ background: #eeedd9;
+}
+
+div.sideBoxBody a {
+ color:#555544;
+}
+
+div.sideBoxBody a:hover {
+ text-decoration: underline;
+}
+
+div.sideBox table td {
+ padding-right: 1em;
+}
+
+input.rqlsubmit{
+ background: #fffff8 url("go.png") 50% 50% no-repeat;
+ width: 20px;
+ height: 20px;
+ margin: 0px;
+}
+
+input#norql{
+ width:13em;
+ margin-right: 2px;
+}
+
+/* user actions menu */
+a.logout, a.logout:visited, a.logout:hover{
+ color: #fff;
+ text-decoration: none;
+}
+
+div#userActionsBox {
+ width: 14em;
+ text-align: right;
+}
+
+div#userActionsBox a.popupMenu {
+ color: black;
+ text-decoration: underline;
+ padding-right: 2em;
+}
+
+/* download box XXX move to its own file? */
+div.downloadBoxTitle{
+ background : #8FBC8F;
+ font-weight: bold;
+}
+
+div.downloadBox{
+ font-weight: bold;
+}
+
+div.downloadBox div.sideBoxBody{
+ background : #EEFED9;
+}
+
+/**************/
+/* navigation */
+/**************/
+div#etyperestriction {
+ margin-bottom: 1ex;
+ border-bottom: 1px solid #ccc;
+}
+
+span.slice a:visited,
+span.slice a:hover{
+ color: #555544;
+}
+
+span.selectedSlice a:visited,
+span.selectedSlice a {
+ color: #000;
+}
+
+/* FIXME should be moved to cubes/folder */
+div.navigation a {
+ text-align: center;
+ text-decoration: none;
+}
+
+div.prevnext {
+ width: 100%;
+ margin-bottom: 1em;
+}
+
+div.prevnext a {
+ color: #000;
+}
+
+/***************************************/
+/* entity views */
+/***************************************/
+
+.mainInfo {
+ margin-right: 1em;
+ padding: 0.2em;
+}
+
+
+div.mainRelated {
+ border: none;
+ margin-right: 1em;
+ padding: 0.5em 0.2em 0.2em;
+}
+
+div.primaryRight{
+ }
+
+div.metadata {
+ font-size: 90%;
+ margin: 5px 0px 3px;
+ color: #666;
+ font-style: italic;
+ text-align: right;
+}
+
+div.section {
+ margin-top: 0.5em;
+ width:100%;
+}
+
+div.section a:hover {
+ text-decoration: none;
+}
+
+/* basic entity view */
+
+tr.entityfield th {
+ text-align: left;
+ padding-right: 0.5em;
+}
+
+div.field {
+ display: inline;
+}
+
+div.ctxtoolbar {
+ float: right;
+ padding-left: 24px;
+ position: relative;
+}
+div.toolbarButton {
+ display: inline;
+}
+
+/***************************************/
+/* messages */
+/***************************************/
+
+.warning,
+.message,
+.errorMessage ,
+.searchMessage{
+ padding: 0.3em 0.3em 0.3em 1em;
+ font-weight: bold;
+}
+
+.loginMessage {
+ margin: 4px 0px;
+ font-weight: bold;
+ color: #ff7700;
+}
+
+div#appMsg, div.appMsg{
+ border: 1px solid #cfceb7;
+ margin-bottom: 8px;
+ padding: 3px;
+ background: #f8f8ee;
+}
+
+.message {
+ margin: 0px;
+ background: #f8f8ee url("information.png") 5px center no-repeat;
+ padding-left: 15px;
+}
+
+.errorMessage {
+ margin: 10px 0px;
+ padding-left: 25px;
+ background: #f7f6f1 url("critical.png") 2px center no-repeat;
+ color: #ed0d0d;
+ border: 1px solid #cfceb7;
+}
+
+.searchMessage {
+ margin-top: 0.5em;
+ border-top: 1px solid #cfceb7;
+ background: #eeedd9 url("information.png") 0% 50% no-repeat; /*dcdbc7*/
+}
+
+.stateMessage {
+ border: 1px solid #ccc;
+ background: #f8f8ee url("information.png") 10px 50% no-repeat;
+ padding:4px 0px 4px 20px;
+ border-width: 1px 0px 1px 0px;
+}
+
+/* warning messages like "There are too many results ..." */
+.warning {
+ padding-left: 25px;
+ background: #f2f2f2 url("critical.png") 3px 50% no-repeat;
+}
+
+/* label shown in the top-right hand corner during form validation */
+div#progress {
+ position: fixed;
+ right: 5px;
+ top: 0px;
+ background: #222211;
+ color: white;
+ font-weight: bold;
+ display: none;
+}
+
+/***************************************/
+/* listing table */
+/***************************************/
+
+table.listing {
+ padding: 10px 0em;
+ color: #000;
+ width: 100%;
+ border-right: 1px solid #dfdfdf;
+}
+
+
+table.listing thead th.over {
+ background-color: #746B6B;
+ cursor: pointer;
+}
+
+table.listing tr th {
+ border: 1px solid #dfdfdf;
+ border-right:none;
+ font-size: 8pt;
+ padding: 4px;
+}
+
+table.listing tr .header {
+ border-right: 1px solid #dfdfdf;
+ cursor: pointer;
+}
+
+table.listing td {
+ color: #3D3D3D;
+ padding: 4px;
+ background-color: #FFF;
+ vertical-align: top;
+}
+
+table.listing th,
+table.listing td {
+ padding: 3px 0px 3px 5px;
+ border: 1px solid #dfdfdf;
+ border-right: none;
+}
+
+table.listing th {
+ font-weight: bold;
+ background: #ebe8d9 url("button.png") repeat-x;
+}
+
+table.listing td a,
+table.listing td a:visited {
+ color: #666;
+}
+
+table.listing a:hover,
+table.listing tr.highlighted td a {
+ color:#000;
+}
+
+table.listing td.top {
+ border: 1px solid white;
+ border-bottom: none;
+ text-align: right ! important;
+ /* insane IE row bug workaround */
+ position: relative;
+ left: -1px;
+ top: -1px;
+}
+
+table.htableForm {
+ vertical-align: middle;
+}
+table.htableForm td{
+ padding-left: 1em;
+ padding-top: 0.5em;
+}
+table.htableForm th{
+ padding-left: 1em;
+}
+table.htableForm .validateButton {
+ margin-right: 0.2em;
+ vertical-align: top;
+ margin-bottom: 0.2em; /* because vertical-align doesn't seems to have any effect */
+}
+
+/***************************************/
+/* error view (views/management.py) */
+/***************************************/
+
+div.pycontext { /* html traceback */
+ font-family: Verdana, sans-serif;
+ font-size: 80%;
+ padding: 1em;
+ margin: 10px 0px 5px 20px;
+ background-color: #dee7ec;
+}
+
+div.pycontext span.name {
+ color: #ff0000;
+}
+
+
+/***************************************/
+/* addcombobox */
+/***************************************/
+
+input#newopt{
+ width:120px ;
+ display:block;
+ float:left;
+ }
+
+div#newvalue{
+ margin-top:2px;
+ }
+
+#add_newopt{
+ background: #fffff8 url("go.png") 50% 50% no-repeat;
+ width: 20px;
+ line-height: 20px;
+ display:block;
+ float:left;
+}
+
+/***************************************/
+/* buttons */
+/***************************************/
+
+input.button{
+ margin: 1em 1em 0px 0px;
+ border: 1px solid #edecd2;
+ border-color:#edecd2 #cfceb7 #cfceb7 #edecd2;
+ background: #fffff8 url("button.png") bottom left repeat-x;
+}
+
+/* FileItemInnerView jquery.treeview.css */
+.folder {
+ /* disable odd/even under folder class */
+ background-color: transparent;
+}
+
+/***************************************/
+/* footer */
+/***************************************/
+
+div#footer {
+ text-align: center;
+}
+div#footer a {
+ color: #000;
+ text-decoration: none;
+}
+
+
+/****************************************/
+/* FIXME must by managed by cubes */
+/****************************************/
+.needsvalidation {
+ font-style: italic;
+ color: gray;
+}
+
+
+/***************************************/
+/* FIXME : Deprecated ? entity view ? */
+/***************************************/
+.title {
+ text-align: left;
+ font-size: large;
+ font-weight: bold;
+}
+
+.validateButton {
+ margin: 1em 1em 0px 0px;
+ border: 1px solid #edecd2;
+ border-color:#edecd2 #cfceb7 #cfceb7 #edecd2;
+ background: #fffff8 url("button.png") bottom left repeat-x;
+}
+
+/********************************/
+/* placement of alt. view icons */
+/********************************/
+
+.otherView {
+ float: right;
+}
--- a/web/data/cubicweb.print.css Wed Jun 02 16:13:28 2010 +0200
+++ b/web/data/cubicweb.print.css Wed Jun 02 16:15:05 2010 +0200
@@ -1,4 +1,15 @@
-td#speedbar, img.logo, div.header{
- display:none }
+* {
+ color: #000 !important;
+}
-a{color:black }
\ No newline at end of file
+div#popupLoginBox,
+div#popupLoginBox,
+img#logo, div.header,
+#navColumnLeft, #navColumnRight,
+#footer {
+ display: none
+}
+
+div#pageContent{
+ border: none;
+}
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/cubicweb.reset.css Wed Jun 02 16:15:05 2010 +0200
@@ -0,0 +1,62 @@
+/* http://meyerweb.com/eric/tools/css/reset/ */
+/* v1.0 | 20080212 */
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, font, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ outline: 0;
+ font-size: 100%;
+ vertical-align: baseline;
+ background: transparent;
+}
+body {
+ line-height: 1;
+}
+ol, ul {
+ list-style: none;
+}
+blockquote, q {
+ quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+ content: '';
+ content: none;
+}
+
+/* remember to define focus styles! */
+:focus {
+ outline: 0;
+}
+
+/* remember to highlight inserts somehow! */
+ins {
+ text-decoration: none;
+}
+del {
+ text-decoration: line-through;
+}
+
+/* tables still need 'cellspacing="0"' in the markup */
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+/* Logilab */
+img{
+ border: none;
+}
+
+fieldset {
+ border: none;
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/cubicweb.rhythm.js Wed Jun 02 16:15:05 2010 +0200
@@ -0,0 +1,7 @@
+$(document).ready(function() {
+ $('a.rhythm').click(function (event){
+ $('div#pageContent').toggleClass('rhythm_bg');
+ $('div#page').toggleClass('rhythm_bg');
+ event.preventDefault();
+ });
+ });
--- a/web/data/cubicweb.timeline-bundle.js Wed Jun 02 16:13:28 2010 +0200
+++ b/web/data/cubicweb.timeline-bundle.js Wed Jun 02 16:15:05 2010 +0200
@@ -1,14 +1,14 @@
var SimileAjax_urlPrefix = baseuri() + 'data/';
var Timeline_urlPrefix = baseuri() + 'data/';
-/*==================================================
+/*
* Simile Ajax API
*
* Include this file in your HTML file as follows:
*
* <script src="http://simile.mit.edu/ajax/api/simile-ajax-api.js" type="text/javascript"></script>
*
- *==================================================
+ *
*/
if (typeof SimileAjax == "undefined") {
@@ -213,9 +213,9 @@
SimileAjax.loaded = true;
})();
}
-/*==================================================
+/*
* Platform Utility Functions and Constants
- *==================================================
+ *
*/
/* This must be called after our jQuery has been loaded
@@ -319,9 +319,10 @@
SimileAjax.Platform.getDefaultLocale = function() {
return SimileAjax.Platform.clientLocale;
-};/*==================================================
+};
+/*
* Debug Utility Functions
- *==================================================
+ *
*/
SimileAjax.Debug = {
@@ -678,9 +679,9 @@
}
};
})();
-/*==================================================
+/*
* DOM Utility Functions
- *==================================================
+ *
*/
SimileAjax.DOM = new Object();
@@ -1040,9 +1041,9 @@
SimileAjax.includeCssFile(document, SimileAjax.urlPrefix + "styles/graphics-ie6.css");
}
-/*==================================================
+/*
* Opacity, translucency
- *==================================================
+ *
*/
SimileAjax.Graphics._createTranslucentImage1 = function(url, verticalAlign) {
var elmt = document.createElement("img");
@@ -1119,9 +1120,9 @@
}
};
-/*==================================================
+/*
* Bubble
- *==================================================
+ *
*/
SimileAjax.Graphics.bubbleConfig = {
@@ -1479,9 +1480,9 @@
};
};
-/*==================================================
+/*
* Animation
- *==================================================
+ *
*/
/**
@@ -1549,11 +1550,11 @@
}
};
-/*==================================================
+/*
* CopyPasteButton
*
* Adapted from http://spaces.live.com/editorial/rayozzie/demo/liveclip/liveclipsample/techPreview.html.
- *==================================================
+ *
*/
/**
@@ -1606,9 +1607,9 @@
return div;
};
-/*==================================================
+/*
* getWidthHeight
- *==================================================
+ *
*/
SimileAjax.Graphics.getWidthHeight = function(el) {
// RETURNS hash {width: w, height: h} in pixels
@@ -1633,9 +1634,9 @@
};
-/*==================================================
+/*
* FontRenderingContext
- *==================================================
+ *
*/
SimileAjax.Graphics.getFontRenderingContext = function(elmt, width) {
return new SimileAjax.Graphics._FontRenderingContext(elmt, width);
@@ -2127,9 +2128,9 @@
var d = new Date().getTimezoneOffset();
return d / -60;
};
-/*==================================================
+/*
* String Utility Functions and Constants
- *==================================================
+ *
*/
String.prototype.trim = function() {
@@ -2170,9 +2171,9 @@
}
return result;
};
-/*==================================================
+/*
* HTML Utility Functions
- *==================================================
+ *
*/
SimileAjax.HTML = new Object();
@@ -2655,9 +2656,9 @@
return (this._a.length > 0) ? this._a[this._a.length - 1] : null;
};
-/*==================================================
+/*
* Event Index
- *==================================================
+ *
*/
SimileAjax.EventIndex = function(unit) {
@@ -2889,9 +2890,9 @@
return this._index < this._events.length() ?
this._events.elementAt(this._index++) : null;
}
-};/*==================================================
+};/*
* Default Unit
- *==================================================
+ *
*/
SimileAjax.NativeDateUnit = new Object();
@@ -2953,9 +2954,9 @@
return new Date(v.getTime() + n);
};
-/*==================================================
+/*
* General, miscellaneous SimileAjax stuff
- *==================================================
+ *
*/
SimileAjax.ListenerQueue = function(wildcardHandlerName) {
@@ -2998,7 +2999,7 @@
}
};
-/*======================================================================
+/*
* History
*
* This is a singleton that keeps track of undoable user actions and
@@ -3020,7 +3021,7 @@
*
* An iframe is inserted into the document's body element to track
* onload events.
- *======================================================================
+ *
*/
SimileAjax.History = {
@@ -3632,7 +3633,7 @@
}
return elmt;
};
-/*==================================================
+/*
* Timeline API
*
* This file will load all the Javascript files
@@ -3696,7 +3697,7 @@
* Note that the Ajax version is usually NOT the same as the Timeline version.
* See variable simile_ajax_ver below for the current version
*
- *==================================================
+ *
*/
(function() {
@@ -3928,7 +3929,7 @@
loadMe();
}
})();
-/*=================================================
+/*
*
* Coding standards:
*
@@ -3950,14 +3951,14 @@
* We also want to use jslint: http://www.jslint.com/
*
*
- *==================================================
+ *
*/
-/*==================================================
+/*
* Timeline VERSION
- *==================================================
+ *
*/
// Note: version is also stored in the build.xml file
Timeline.version = 'pre 2.4.0'; // use format 'pre 1.2.3' for trunk versions
@@ -3965,9 +3966,9 @@
Timeline.display_version = Timeline.version + ' (with Ajax lib ' + Timeline.ajax_lib_version + ')';
// cf method Timeline.writeVersion
-/*==================================================
+/*
* Timeline
- *==================================================
+ *
*/
Timeline.strings = {}; // localization string tables
Timeline.HORIZONTAL = 0;
@@ -4183,9 +4184,9 @@
-/*==================================================
+/*
* Timeline Implementation object
- *==================================================
+ *
*/
Timeline._Impl = function(elmt, bandInfos, orientation, unit, timelineID) {
SimileAjax.WindowManager.initialize();
@@ -4585,7 +4586,7 @@
this.paint();
};
-/*=================================================
+/*
*
* Coding standards:
*
@@ -4607,14 +4608,14 @@
* We also want to use jslint: http://www.jslint.com/
*
*
- *==================================================
+ *
*/
-/*==================================================
+/*
* Band
- *==================================================
+ *
*/
Timeline._Band = function(timeline, bandInfo, index) {
// hack for easier subclassing
@@ -5344,9 +5345,9 @@
Timeline._Band.prototype.closeBubble = function() {
SimileAjax.WindowManager.cancelPopups();
};
-/*==================================================
+/*
* Classic Theme
- *==================================================
+ *
*/
@@ -5523,14 +5524,14 @@
};
this.mouseWheel = 'scroll'; // 'default', 'zoom', 'scroll'
-};/*==================================================
+};/*
* An "ether" is a object that maps date/time to pixel coordinates.
- *==================================================
+ *
*/
-/*==================================================
+/*
* Linear Ether
- *==================================================
+ *
*/
Timeline.LinearEther = function(params) {
@@ -5601,9 +5602,9 @@
};
-/*==================================================
+/*
* Hot Zone Ether
- *==================================================
+ *
*/
Timeline.HotZoneEther = function(params) {
@@ -5828,9 +5829,9 @@
Timeline.HotZoneEther.prototype._getScale = function() {
return this._interval / this._pixelsPerInterval;
};
-/*==================================================
+/*
* Gregorian Ether Painter
- *==================================================
+ *
*/
Timeline.GregorianEtherPainter = function(params) {
@@ -5919,9 +5920,9 @@
};
-/*==================================================
+/*
* Hot Zone Gregorian Ether Painter
- *==================================================
+ *
*/
Timeline.HotZoneGregorianEtherPainter = function(params) {
@@ -6080,9 +6081,9 @@
}
};
-/*==================================================
+/*
* Year Count Ether Painter
- *==================================================
+ *
*/
Timeline.YearCountEtherPainter = function(params) {
@@ -6169,9 +6170,9 @@
Timeline.YearCountEtherPainter.prototype.softPaint = function() {
};
-/*==================================================
+/*
* Quarterly Ether Painter
- *==================================================
+ *
*/
Timeline.QuarterlyEtherPainter = function(params) {
@@ -6257,9 +6258,9 @@
Timeline.QuarterlyEtherPainter.prototype.softPaint = function() {
};
-/*==================================================
+/*
* Ether Interval Marker Layout
- *==================================================
+ *
*/
Timeline.EtherIntervalMarkerLayout = function(timeline, band, theme, align, showLine) {
@@ -6363,9 +6364,9 @@
};
};
-/*==================================================
+/*
* Ether Highlight Layout
- *==================================================
+ *
*/
Timeline.EtherHighlight = function(timeline, band, theme, backgroundLayer) {
@@ -6404,9 +6405,9 @@
}
}
};
-/*==================================================
+/*
* Event Utils
- *==================================================
+ *
*/
Timeline.EventUtils = {};
@@ -6421,7 +6422,7 @@
};
Timeline.EventUtils.decodeEventElID = function(elementID) {
- /*==================================================
+ /*
*
* Use this function to decode an event element's id on a band (label div,
* tape div or icon img).
@@ -6447,7 +6448,7 @@
* by using Timeline.getTimeline, Timeline.getBand, or
* Timeline.getEvent and passing in the element's id
*
- *==================================================
+ *
*/
var parts = elementID.split('-');
@@ -6467,9 +6468,9 @@
// elType should be one of {label | icon | tapeN | highlightN}
return elType + "-tl-" + timeline.timelineID +
"-" + band.getIndex() + "-" + evt.getID();
-};/*==================================================
+};/*
* Gregorian Date Labeller
- *==================================================
+ *
*/
Timeline.GregorianDateLabeller = function(locale, timeZone) {
@@ -6558,9 +6559,9 @@
return { text: text, emphasized: emphasized };
}
-/*==================================================
+/*
* Default Event Source
- *==================================================
+ *
*/
@@ -7125,12 +7126,12 @@
};
-/*==================================================
+/*
* Original Event Painter
- *==================================================
+ *
*/
-/*==================================================
+/*
*
* To enable a single event listener to monitor everything
* on a Timeline, we need a way to map from an event's icon,
@@ -7152,7 +7153,7 @@
* You can then retrieve the band/timeline objects and event object
* by using Timeline.EventUtils.decodeEventElID
*
- *==================================================
+ *
*/
/*
@@ -7818,9 +7819,9 @@
this._eventPaintListeners[i](this._band, op, evt, els);
}
};
-/*==================================================
+/*
* Detailed Event Painter
- *==================================================
+ *
*/
// Note: a number of features from original-painter
@@ -8509,9 +8510,9 @@
this._onSelectListeners[i](eventID);
}
};
-/*==================================================
+/*
* Overview Event Painter
- *==================================================
+ *
*/
Timeline.OverviewEventPainter = function(params) {
@@ -8767,9 +8768,9 @@
Timeline.OverviewEventPainter.prototype.showBubble = function(evt) {
// not implemented
};
-/*==================================================
+/*
* Compact Event Painter
- *==================================================
+ *
*/
Timeline.CompactEventPainter = function(params) {
@@ -9831,9 +9832,9 @@
this._onSelectListeners[i](eventIDs);
}
};
-/*==================================================
+/*
* Span Highlight Decorator
- *==================================================
+ *
*/
Timeline.SpanHighlightDecorator = function(params) {
@@ -9948,9 +9949,9 @@
Timeline.SpanHighlightDecorator.prototype.softPaint = function() {
};
-/*==================================================
+/*
* Point Highlight Decorator
- *==================================================
+ *
*/
Timeline.PointHighlightDecorator = function(params) {
@@ -10015,9 +10016,9 @@
Timeline.PointHighlightDecorator.prototype.softPaint = function() {
};
-/*==================================================
+/*
* Default Unit
- *==================================================
+ *
*/
Timeline.NativeDateUnit = new Object();
@@ -10083,35 +10084,35 @@
return new Date(v.getTime() + n);
};
-/*==================================================
+/*
* Common localization strings
- *==================================================
+ *
*/
Timeline.strings["fr"] = {
wikiLinkLabel: "Discute"
};
-/*==================================================
+/*
* Localization of labellers.js
- *==================================================
+ *
*/
Timeline.GregorianDateLabeller.monthNames["fr"] = [
"jan", "fev", "mar", "avr", "mai", "jui", "jui", "aou", "sep", "oct", "nov", "dec"
];
-/*==================================================
+/*
* Common localization strings
- *==================================================
+ *
*/
Timeline.strings["en"] = {
wikiLinkLabel: "Discuss"
};
-/*==================================================
+/*
* Localization of labellers.js
- *==================================================
+ *
*/
Timeline.GregorianDateLabeller.monthNames["en"] = [
--- a/web/data/cubicweb.widgets.js Wed Jun 02 16:13:28 2010 +0200
+++ b/web/data/cubicweb.widgets.js Wed Jun 02 16:15:05 2010 +0200
@@ -313,34 +313,5 @@
});
-/*
- * ComboBox with a textinput : allows to add a new value
- */
-
-Widgets.AddComboBox = defclass('AddComboBox', null, {
- __init__ : function(wdgnode) {
- jQuery("#add_newopt").click(function() {
- var new_val = jQuery("#newopt").val();
- if (!new_val){
- return false;
- }
- name = wdgnode.getAttribute('name').split(':');
- this.rel = name[0];
- this.eid_to = name[1];
- this.etype_to = wdgnode.getAttribute('cubicweb:etype_to');
- this.etype_from = wdgnode.getAttribute('cubicweb:etype_from');
- var d = asyncRemoteExec('add_and_link_new_entity', this.etype_to, this.rel, this.eid_to, this.etype_from, 'new_val');
- d.addCallback(function (eid) {
- jQuery(wdgnode).find("option[selected]").removeAttr("selected");
- var new_option = OPTION({'value':eid, 'selected':'selected'}, value=new_val);
- wdgnode.appendChild(new_option);
- });
- d.addErrback(function (xxx) {
- log('xxx =', xxx);
- });
- });
- }
-});
-
CubicWeb.provide('widgets.js');
--- a/web/data/external_resources Wed Jun 02 16:13:28 2010 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,62 +0,0 @@
-# -*- shell-script -*-
-###############################################################################
-#
-# external resources file for core library resources
-#
-# Commented values are default values used by the application.
-#
-###############################################################################
-
-
-# CSS stylesheets to include in HTML headers
-#STYLESHEETS = DATADIR/cubicweb.css
-
-# CSS stylesheets for print
-#STYLESHEETS_PRINT = DATADIR/cubicweb.print.css
-
-#CSS stylesheets for IE
-#IE_STYLESHEETS = DATADIR/cubicweb.ie.css
-
-# Javascripts files to include in HTML headers
-#JAVASCRIPTS = DATADIR/jquery.js, DATADIR/cubicweb.python.js, DATADIR/jquery.json.js, DATADIR/cubicweb.compat.js, DATADIR/cubicweb.htmlhelpers.js
-
-# path to favicon (relative to the application main script, seen as a
-# directory, hence .. when you are not using an absolute path)
-#FAVICON = DATADIR/favicon.ico
-
-# path to the logo (relative to the application main script, seen as a
-# directory, hence .. when you are not using an absolute path)
-LOGO = DATADIR/logo.png
-
-# rss logo (link to get the rss view of a selection)
-RSS_LOGO = DATADIR/rss.png
-RSS_LOGO_16 = DATADIR/feed-icon16x16.png
-RSS_LOGO_32 = DATADIR/feed-icon32x32.png
-
-# path to search image
-SEARCH_GO = DATADIR/go.png
-
-#FCKEDITOR_PATH = /usr/share/fckeditor/
-
-PUCE_UP = DATADIR/puce_up.png
-PUCE_DOWN = DATADIR/puce_down.png
-
-# icons for entity types
-BOOKMARK_ICON = DATADIR/icon_bookmark.gif
-EMAILADDRESS_ICON = DATADIR/icon_emailaddress.gif
-EUSER_ICON = DATADIR/icon_euser.gif
-STATE_ICON = DATADIR/icon_state.gif
-
-# other icons
-CALENDAR_ICON = DATADIR/calendar.gif
-CANCEL_EMAIL_ICON = DATADIR/sendcancel.png
-SEND_EMAIL_ICON = DATADIR/sendok.png
-DOWNLOAD_ICON = DATADIR/download.gif
-UPLOAD_ICON = DATADIR/upload.gif
-GMARKER_ICON = DATADIR/gmap_blue_marker.png
-UP_ICON = DATADIR/up.gif
-
-OK_ICON = DATADIR/ok.png
-CANCEL_ICON = DATADIR/cancel.png
-APPLY_ICON = DATADIR/plus.png
-TRASH_ICON = DATADIR/trash_can_small.png
Binary file web/data/logo.png has changed
Binary file web/data/mail.gif has changed
Binary file web/data/nomail.gif has changed
Binary file web/data/rhythm15.png has changed
Binary file web/data/rhythm18.png has changed
Binary file web/data/rhythm20.png has changed
Binary file web/data/rhythm22.png has changed
Binary file web/data/rhythm24.png has changed
Binary file web/data/rhythm26.png has changed
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/uiprops.py Wed Jun 02 16:15:05 2010 +0200
@@ -0,0 +1,111 @@
+"""define default ui properties"""
+
+# CSS stylesheets to include systematically in HTML headers
+# use the following line if you *need* to keep the old stylesheet
+#STYLESHEETS = [data('cubicweb.old.css')]
+STYLESHEETS = [data('cubicweb.reset.css'),
+ data('cubicweb.css')]
+STYLESHEETS_IE = [data('cubicweb.ie.css')]
+STYLESHEETS_PRINT = [data('cubicweb.print.css')]
+
+# Javascripts files to include systematically in HTML headers
+JAVASCRIPTS = [data('jquery.js'),
+ data('jquery.corner.js'),
+ data('jquery.json.js'),
+ data('cubicweb.compat.js'),
+ data('cubicweb.python.js'),
+ data('cubicweb.htmlhelpers.js')]
+
+# where is installed fckeditor
+FCKEDITOR_PATH = '/usr/share/fckeditor/'
+
+# favicon and logo for the instance
+FAVICON = data('favicon.ico')
+LOGO = data('logo.png')
+
+# rss logo (link to get the rss view of a selection)
+RSS_LOGO = data('rss.png')
+RSS_LOGO_16 = data('feed-icon16x16.png')
+RSS_LOGO_32 = data('feed-icon32x32.png')
+
+# XXX cleanup resources below, some of them are probably not used
+# (at least entity types icons...)
+
+# images
+HELP = data('help.png')
+SEARCH_GO = data('go.png')
+PUCE_UP = data('puce_up.png')
+PUCE_DOWN = data('puce_down.png')
+
+# button icons
+OK_ICON = data('ok.png')
+CANCEL_ICON = data('cancel.png')
+APPLY_ICON = data('plus.png')
+TRASH_ICON = data('trash_can_small.png')
+
+# icons for entity types
+BOOKMARK_ICON = data('icon_bookmark.gif')
+EMAILADDRESS_ICON = data('icon_emailaddress.gif')
+EUSER_ICON = data('icon_euser.gif')
+STATE_ICON = data('icon_state.gif')
+
+# other icons
+CALENDAR_ICON = data('calendar.gif')
+CANCEL_EMAIL_ICON = data('sendcancel.png')
+SEND_EMAIL_ICON = data('sendok.png')
+DOWNLOAD_ICON = data('download.gif')
+UPLOAD_ICON = data('upload.gif')
+GMARKER_ICON = data('gmap_blue_marker.png')
+UP_ICON = data('up.gif')
+
+# colors, fonts, etc
+
+# default (body, html)
+defaultColor = '#000'
+defaultFont = 'Verdana,sans-serif'
+defaultSize = '12px'
+defaultLineHeight = '1.5'
+defaultLineHeightEm = defaultLineHeight + 'em'
+baseRhythmBg = 'rhythm18.png'
+
+# XXX
+defaultLayoutMargin = '8px'
+
+# header
+headerBgColor = '#ff7700'
+
+# h
+h1FontSize = '1.5em'
+h1BorderBottomStyle = '0.06em solid black'
+h1Padding = '0 0 0.14em 0 '
+h1Margin = '0.8em 0 0.5em'
+
+h2FontSize = '1.33333em'
+h2Padding = '0.4em 0 0.35em 0'
+h2Margin = '0'
+
+h3FontSize = '1.16667em'
+h3Padding = '0.5em 0 0.57em 0'
+h3Margin = '0'
+
+# links
+aColor = '#ff4500'
+aActiveColor = aVisitedColor = aLinkColor = aColor
+
+# page frame
+pageContentBorderColor = '#ccc'
+pageContentBgColor = '#fff'
+pageContentPadding = '1em'
+pageMinHeight = '800px'
+
+# button
+buttonBorderColor = '#edecd2'
+buttonBgColor = '#fffff8'
+
+# action, search, sideBoxes
+actionBoxTitleBgColor = '#cfceb7'
+sideBoxBodyBgColor = '#eeedd9'
+
+
+# table listing
+listingBorderColor = '#878787'
--- a/web/formfields.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/formfields.py Wed Jun 02 16:15:05 2010 +0200
@@ -323,7 +323,7 @@
value = getattr(entity, self.name)
if value is not None or not self.fallback_on_none_attribute:
return value
- elif entity.has_eid() or entity.relation_cached(self.name, self.role):
+ elif entity.has_eid() or entity.cw_relation_cached(self.name, self.role):
value = [r[0] for r in entity.related(self.name, self.role)]
if value or not self.fallback_on_none_attribute:
return value
@@ -399,7 +399,7 @@
entity = form.edited_entity
if entity.e_schema.has_metadata(self.name, 'format') and (
entity.has_eid() or '%s_format' % self.name in entity):
- return form.edited_entity.attr_metadata(self.name, 'format')
+ return form.edited_entity.cw_attr_metadata(self.name, 'format')
return form._cw.property_value('ui.default-text-format')
def encoding(self, form):
@@ -408,7 +408,7 @@
entity = form.edited_entity
if entity.e_schema.has_metadata(self.name, 'encoding') and (
entity.has_eid() or '%s_encoding' % self.name in entity):
- return form.edited_entity.attr_metadata(self.name, 'encoding')
+ return form.edited_entity.cw_attr_metadata(self.name, 'encoding')
return form._cw.encoding
def form_init(self, form):
--- a/web/formwidgets.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/formwidgets.py Wed Jun 02 16:15:05 2010 +0200
@@ -60,7 +60,6 @@
.. autoclass:: cubicweb.web.formwidgets.AjaxWidget
.. autoclass:: cubicweb.web.formwidgets.AutoCompletionWidget
-.. kill or document AddComboBoxWidget
.. kill or document StaticFileAutoCompletionWidget
.. kill or document LazyRestrictedAutoCompletionWidget
.. kill or document RestrictedAutoCompletionWidget
@@ -550,7 +549,7 @@
return (u"""<a onclick="toggleCalendar('%s', '%s', %s, %s);" class="calhelper">
<img src="%s" title="%s" alt="" /></a><div class="calpopup hidden" id="%s"></div>"""
% (helperid, inputid, year, month,
- form._cw.external_resource('CALENDAR_ICON'),
+ form._cw.uiprops['CALENDAR_ICON'],
form._cw._('calendar'), helperid) )
@@ -574,7 +573,7 @@
req.add_onload(u'jqNode("%s").datepicker('
'{buttonImage: "%s", dateFormat: "%s", firstDay: 1,'
' showOn: "button", buttonImageOnly: true})' % (
- domid, req.external_resource('CALENDAR_ICON'), fmt))
+ domid, req.uiprops['CALENDAR_ICON'], fmt))
if self.datestr is None:
value = self.values(form, field)[0]
else:
@@ -776,24 +775,6 @@
return entity.view('combobox')
-class AddComboBoxWidget(Select):
- def attributes(self, form, field):
- attrs = super(AddComboBoxWidget, self).attributes(form, field)
- init_ajax_attributes(attrs, 'AddComboBox')
- # XXX entity form specific
- entity = form.edited_entity
- attrs['cubicweb:etype_to'] = entity.e_schema
- etype_from = entity.e_schema.subjrels[field.name].objects(entity.e_schema)[0]
- attrs['cubicweb:etype_from'] = etype_from
- return attrs
-
- def _render(self, form, field, renderer):
- return super(AddComboBoxWidget, self)._render(form, field, renderer) + u'''
-<div id="newvalue">
- <input type="text" id="newopt" />
- <a href="javascript:noop()" id="add_newopt"> </a></div>
-'''
-
# more widgets #################################################################
class IntervalWidget(FieldWidget):
@@ -954,7 +935,7 @@
if self.settabindex and not 'tabindex' in attrs:
attrs['tabindex'] = form._cw.next_tabindex()
if self.icon:
- img = tags.img(src=form._cw.external_resource(self.icon), alt=self.icon)
+ img = tags.img(src=form._cw.uiprops[self.icon], alt=self.icon)
else:
img = u''
return tags.button(img + xml_escape(label), escapecontent=False,
@@ -985,7 +966,7 @@
def render(self, form, field=None, renderer=None):
label = form._cw._(self.label)
- imgsrc = form._cw.external_resource(self.imgressource)
+ imgsrc = form._cw.uiprops[self.imgressource]
return '<a id="%(domid)s" href="%(href)s">'\
'<img src="%(imgsrc)s" alt="%(label)s"/>%(label)s</a>' % {
'label': label, 'imgsrc': imgsrc,
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/propertysheet.py Wed Jun 02 16:15:05 2010 +0200
@@ -0,0 +1,100 @@
+# copyright 2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+"""property sheets allowing configuration of the web ui"""
+
+__docformat__ = "restructuredtext en"
+
+import re
+import os
+import os.path as osp
+
+
+class PropertySheet(dict):
+ def __init__(self, cache_directory, **context):
+ self._cache_directory = cache_directory
+ self.context = context
+ self.reset()
+ context['sheet'] = self
+ self._percent_rgx = re.compile('%(?!\()')
+
+ def reset(self):
+ self.clear()
+ self._ordered_propfiles = []
+ self._propfile_mtime = {}
+ self._sourcefile_mtime = {}
+ self._cache = {}
+
+ def load(self, fpath):
+ scriptglobals = self.context.copy()
+ scriptglobals['__file__'] = fpath
+ execfile(fpath, scriptglobals, self)
+ self._propfile_mtime[fpath] = os.stat(fpath)[-2]
+ self._ordered_propfiles.append(fpath)
+
+ def need_reload(self):
+ for rid, (adirectory, rdirectory, mtime) in self._cache.items():
+ if os.stat(osp.join(rdirectory, rid))[-2] > mtime:
+ del self._cache[rid]
+ for fpath, mtime in self._propfile_mtime.iteritems():
+ if os.stat(fpath)[-2] > mtime:
+ return True
+ return False
+
+ def reload(self):
+ ordered_files = self._ordered_propfiles
+ self.reset()
+ for fpath in ordered_files:
+ self.load(fpath)
+
+ def reload_if_needed(self):
+ if self.need_reload():
+ self.reload()
+
+ def process_resource(self, rdirectory, rid):
+ try:
+ return self._cache[rid][0]
+ except KeyError:
+ cachefile = osp.join(self._cache_directory, rid)
+ self.debug('caching processed %s/%s into %s',
+ rdirectory, rid, cachefile)
+ rcachedir = osp.dirname(cachefile)
+ if not osp.exists(rcachedir):
+ os.makedirs(rcachedir)
+ sourcefile = osp.join(rdirectory, rid)
+ content = file(sourcefile).read()
+ # XXX replace % not followed by a paren by %% to avoid having to do
+ # this in the source css file ?
+ try:
+ content = self.compile(content)
+ except ValueError, ex:
+ self.error("can't process %s/%s: %s", rdirectory, rid, ex)
+ adirectory = rdirectory
+ else:
+ stream = file(cachefile, 'w')
+ stream.write(content)
+ stream.close()
+ adirectory = self._cache_directory
+ self._cache[rid] = (adirectory, rdirectory, os.stat(sourcefile)[-2])
+ return adirectory
+
+ def compile(self, content):
+ return self._percent_rgx.sub('%%', content) % self
+
+from cubicweb.web import LOGGER
+from logilab.common.logging_ext import set_log_methods
+set_log_methods(PropertySheet, LOGGER)
--- a/web/request.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/request.py Wed Jun 02 16:15:05 2010 +0200
@@ -83,6 +83,12 @@
super(CubicWebRequestBase, self).__init__(vreg)
self.authmode = vreg.config['auth-mode']
self.https = https
+ if https:
+ self.uiprops = vreg.config.https_uiprops
+ self.datadir_url = vreg.config.https_datadir_url
+ else:
+ 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()
# form parameters
@@ -99,7 +105,6 @@
self.next_tabindex = self.tabindexgen.next
# page id, set by htmlheader template
self.pageid = None
- self.datadir_url = self._datadir_url()
self._set_pageid()
# prepare output header
self.headers_out = Headers()
@@ -589,10 +594,6 @@
"""return currently accessed url"""
return self.base_url() + self.relative_path(includeparams)
- def _datadir_url(self):
- """return url of the instance's data directory"""
- return self.base_url() + 'data%s/' % self.vreg.config.instance_md5_version()
-
def selected(self, url):
"""return True if the url is equivalent to currently accessed url"""
reqpath = self.relative_path().lower()
@@ -618,25 +619,6 @@
return controller
return 'view'
- def external_resource(self, rid, default=_MARKER):
- """return a path to an external resource, using its identifier
-
- raise KeyError if the resource is not defined
- """
- try:
- value = self.vreg.config.ext_resources[rid]
- except KeyError:
- if default is _MARKER:
- raise
- return default
- if value is None:
- return None
- baseurl = self.datadir_url[:-1] # remove trailing /
- if isinstance(value, list):
- return [v.replace('DATADIR', baseurl) for v in value]
- return value.replace('DATADIR', baseurl)
- external_resource = cached(external_resource, keyarg=1)
-
def validate_cache(self):
"""raise a `DirectResponse` exception if a cached page along the way
exists and is still usable.
@@ -712,12 +694,6 @@
auth, ex.__class__.__name__, ex)
return None, None
- @deprecated("[3.4] use parse_accept_header('Accept-Language')")
- def header_accept_language(self):
- """returns an ordered list of preferred languages"""
- return [value.split('-')[0] for value in
- self.parse_accept_header('Accept-Language')]
-
def parse_accept_header(self, header):
"""returns an ordered list of preferred languages"""
accepteds = self.get_header(header, '')
@@ -823,5 +799,25 @@
u'<div xmlns="http://www.w3.org/1999/xhtml" xmlns:cubicweb="http://www.logilab.org/2008/cubicweb">')
return u'<div>'
+ @deprecated('[3.9] use req.uiprops[rid]')
+ def external_resource(self, rid, default=_MARKER):
+ """return a path to an external resource, using its identifier
+
+ raise `KeyError` if the resource is not defined
+ """
+ try:
+ return self.uiprops[rid]
+ except KeyError:
+ if default is _MARKER:
+ raise
+ return default
+
+ @deprecated("[3.4] use parse_accept_header('Accept-Language')")
+ def header_accept_language(self):
+ """returns an ordered list of preferred languages"""
+ return [value.split('-')[0] for value in
+ self.parse_accept_header('Accept-Language')]
+
+
from cubicweb import set_log_methods
set_log_methods(CubicWebRequestBase, LOGGER)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/pouet.css Wed Jun 02 16:15:05 2010 +0200
@@ -0,0 +1,3 @@
+body { background-color: %(bgcolor)s
+ font-size: 100%;
+ }
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/sheet1.py Wed Jun 02 16:15:05 2010 +0200
@@ -0,0 +1,3 @@
+bgcolor = '#000000'
+stylesheets = ['%s/cubicweb.css' % datadir_url]
+logo = '%s/logo.png' % datadir_url
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/sheet2.py Wed Jun 02 16:15:05 2010 +0200
@@ -0,0 +1,3 @@
+fontcolor = 'black'
+bgcolor = '#FFFFFF'
+stylesheets = sheet['stylesheets'] + ['%s/mycube.css' % datadir_url]
--- a/web/test/unittest_breadcrumbs.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/test/unittest_breadcrumbs.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,8 +15,10 @@
#
# 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 cubicweb.devtools.testlib import CubicWebTC
+
class BreadCrumbsTC(CubicWebTC):
def test_base(self):
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/unittest_propertysheet.py Wed Jun 02 16:15:05 2010 +0200
@@ -0,0 +1,49 @@
+import os
+from os.path import join, dirname
+from shutil import rmtree
+
+from logilab.common.testlib import TestCase, unittest_main
+
+from cubicweb.web.propertysheet import *
+
+DATADIR = join(dirname(__file__), 'data')
+CACHEDIR = join(DATADIR, 'uicache')
+
+class PropertySheetTC(TestCase):
+
+ def tearDown(self):
+ rmtree(CACHEDIR)
+
+ def test(self):
+ ps = PropertySheet(CACHEDIR, datadir_url='http://cwtest.com')
+ ps.load(join(DATADIR, 'sheet1.py'))
+ ps.load(join(DATADIR, 'sheet2.py'))
+ # defined by sheet1
+ self.assertEquals(ps['logo'], 'http://cwtest.com/logo.png')
+ # defined by sheet1, overriden by sheet2
+ self.assertEquals(ps['bgcolor'], '#FFFFFF')
+ # defined by sheet2
+ self.assertEquals(ps['fontcolor'], 'black')
+ # defined by sheet1, extended by sheet2
+ self.assertEquals(ps['stylesheets'], ['http://cwtest.com/cubicweb.css',
+ 'http://cwtest.com/mycube.css'])
+ self.assertEquals(ps.compile('a {bgcolor: %(bgcolor)s; size: 1%;}'),
+ 'a {bgcolor: #FFFFFF; size: 1%;}')
+ self.assertEquals(ps.process_resource(DATADIR, 'pouet.css'),
+ CACHEDIR)
+ self.failUnless('pouet.css' in ps._cache)
+ self.failIf(ps.need_reload())
+ os.utime(join(DATADIR, 'sheet1.py'), None)
+ self.failUnless('pouet.css' in ps._cache)
+ self.failUnless(ps.need_reload())
+ self.failUnless('pouet.css' in ps._cache)
+ ps.reload()
+ self.failIf('pouet.css' in ps._cache)
+ self.failIf(ps.need_reload())
+ ps.process_resource(DATADIR, 'pouet.css') # put in cache
+ os.utime(join(DATADIR, 'pouet.css'), None)
+ self.failIf(ps.need_reload())
+ self.failIf('pouet.css' in ps._cache)
+
+if __name__ == '__main__':
+ unittest_main()
--- a/web/test/unittest_views_basecontrollers.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/test/unittest_views_basecontrollers.py Wed Jun 02 16:15:05 2010 +0200
@@ -128,7 +128,7 @@
self.assertEquals(e.firstname, u'Th\xe9nault')
self.assertEquals(e.surname, u'Sylvain')
self.assertEquals([g.eid for g in e.in_group], groupeids)
- self.assertEquals(e.state, 'activated')
+ self.assertEquals(e.cw_adapt_to('IWorkflowable').state, 'activated')
def test_create_multiple_linked(self):
@@ -643,7 +643,7 @@
# silly tests
def test_external_resource(self):
self.assertEquals(self.remote_call('external_resource', 'RSS_LOGO')[0],
- json.dumps(self.request().external_resource('RSS_LOGO')))
+ json.dumps(self.config.uiprops['RSS_LOGO']))
def test_i18n(self):
self.assertEquals(self.remote_call('i18n', ['bimboom'])[0],
json.dumps(['bimboom']))
--- a/web/test/unittest_viewselector.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/test/unittest_viewselector.py Wed Jun 02 16:15:05 2010 +0200
@@ -408,19 +408,19 @@
tableview.TableView)
def test_interface_selector(self):
- image = self.request().create_entity('Image', data_name=u'bim.png', data=Binary('bim'))
+ image = self.request().create_entity('File', data_name=u'bim.png', data=Binary('bim'))
# image primary view priority
req = self.request()
- rset = req.execute('Image X WHERE X data_name "bim.png"')
+ rset = req.execute('File X WHERE X data_name "bim.png"')
self.assertIsInstance(self.vreg['views'].select('primary', req, rset=rset),
idownloadable.IDownloadablePrimaryView)
def test_score_entity_selector(self):
- image = self.request().create_entity('Image', data_name=u'bim.png', data=Binary('bim'))
+ image = self.request().create_entity('File', data_name=u'bim.png', data=Binary('bim'))
# image primary view priority
req = self.request()
- rset = req.execute('Image X WHERE X data_name "bim.png"')
+ rset = req.execute('File X WHERE X data_name "bim.png"')
self.assertIsInstance(self.vreg['views'].select('image', req, rset=rset),
idownloadable.ImageView)
fileobj = self.request().create_entity('File', data_name=u'bim.txt', data=Binary('bim'))
--- a/web/test/unittest_webconfig.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/test/unittest_webconfig.py Wed Jun 02 16:15:05 2010 +0200
@@ -33,15 +33,14 @@
def test_nonregr_print_css_as_list(self):
"""make sure PRINT_CSS *must* is a list"""
config = self.config
- req = fake.FakeRequest()
- print_css = req.external_resource('STYLESHEETS_PRINT')
+ print_css = config.uiprops['STYLESHEETS_PRINT']
self.failUnless(isinstance(print_css, list))
- ie_css = req.external_resource('IE_STYLESHEETS')
+ ie_css = config.uiprops['STYLESHEETS_IE']
self.failUnless(isinstance(ie_css, list))
def test_locate_resource(self):
- self.failUnless('FILE_ICON' in self.config.ext_resources)
- rname = self.config.ext_resources['FILE_ICON'].replace('DATADIR/', '')
+ self.failUnless('FILE_ICON' in self.config.uiprops)
+ rname = self.config.uiprops['FILE_ICON'].replace(self.config.datadir_url, '')
self.failUnless('file' in self.config.locate_resource(rname).split(os.sep))
cubicwebcsspath = self.config.locate_resource('cubicweb.css').split(os.sep)
self.failUnless('web' in cubicwebcsspath or 'shared' in cubicwebcsspath) # 'shared' if tests under apycot
--- a/web/views/actions.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/actions.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,21 +15,22 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""Set of HTML base actions
+"""Set of HTML base actions"""
-"""
__docformat__ = "restructuredtext en"
_ = unicode
from warnings import warn
+from logilab.mtconverter import xml_escape
+
from cubicweb.schema import display_name
from cubicweb.appobject import objectify_selector
from cubicweb.selectors import (EntitySelector, yes,
one_line_rset, multi_lines_rset, one_etype_rset, relation_possible,
nonempty_rset, non_final_entity,
authenticated_user, match_user_groups, match_search_state,
- has_permission, has_add_permission, implements,
+ has_permission, has_add_permission, implements, debug_mode,
)
from cubicweb.web import uicfg, controller, action
from cubicweb.web.views import linksearch_select_url, vid_from_rset
@@ -415,6 +416,20 @@
def url(self):
return 'http://www.cubicweb.org'
+class GotRhythmAction(action.Action):
+ __regid__ = 'rhythm'
+ __select__ = debug_mode()
+
+ category = 'footer'
+ order = 3
+ title = _('Got rhythm?')
+
+ def url(self):
+ return xml_escape(self._cw.url()+'#')
+
+ def html_class(self):
+ self._cw.add_js('cubicweb.rhythm.js')
+ return 'rhythm'
## default actions ui configuration ###########################################
--- a/web/views/autoform.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/autoform.py Wed Jun 02 16:15:05 2010 +0200
@@ -765,7 +765,7 @@
"""return a list of (relation schema, role) to edit for the entity"""
if self.display_fields is not None:
return self.display_fields
- if self.edited_entity.has_eid() and not self.edited_entity.has_perm('update'):
+ if self.edited_entity.has_eid() and not self.edited_entity.cw_has_perm('update'):
return []
# XXX we should simply put eid in the generated section, no?
return [(rtype, role) for rtype, _, role in self._relations_by_section(
@@ -868,7 +868,7 @@
vvreg = self._cw.vreg['views']
# display inline-edition view for all existing related entities
for i, relentity in enumerate(related.entities()):
- if relentity.has_perm('update'):
+ if relentity.cw_has_perm('update'):
yield vvreg.select('inline-edition', self._cw,
rset=related, row=i, col=0,
etype=ttype, rtype=rschema, role=role,
--- a/web/views/basecomponents.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/basecomponents.py Wed Jun 02 16:15:05 2010 +0200
@@ -78,8 +78,8 @@
site_wide = True
def call(self):
- self.w(u'<a href="%s"><img class="logo" src="%s" alt="logo"/></a>'
- % (self._cw.base_url(), self._cw.external_resource('LOGO')))
+ self.w(u'<a href="%s"><img id="logo" src="%s" alt="logo"/></a>'
+ % (self._cw.base_url(), self._cw.uiprops['LOGO']))
class ApplHelp(component.Component):
--- a/web/views/basecontrollers.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/basecontrollers.py Wed Jun 02 16:15:05 2010 +0200
@@ -22,9 +22,6 @@
__docformat__ = "restructuredtext en"
-from smtplib import SMTP
-
-from logilab.common.decorators import cached
from logilab.common.date import strptime
from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError,
@@ -32,7 +29,8 @@
from cubicweb.utils import CubicWebJsonEncoder
from cubicweb.selectors import authenticated_user, anonymous_user, match_form_params
from cubicweb.mail import format_mail
-from cubicweb.web import Redirect, RemoteCallFailed, DirectResponse, json_dumps, json
+from cubicweb.web import (Redirect, RemoteCallFailed, DirectResponse,
+ json, json_dumps)
from cubicweb.web.controller import Controller
from cubicweb.web.views import vid_from_rset, formrenderers
@@ -250,7 +248,7 @@
errback = str(self._cw.form.get('__onfailure', 'null'))
cbargs = str(self._cw.form.get('__cbargs', 'null'))
self._cw.set_content_type('text/html')
- jsargs = json.dumps((status, args, entity), cls=CubicWebJsonEncoder)
+ jsargs = json_dumps((status, args, entity))
return """<script type="text/javascript">
window.parent.handleFormValidationResponse('%s', %s, %s, %s, %s);
</script>""" % (domid, callback, errback, jsargs, cbargs)
@@ -343,12 +341,11 @@
return None
return None
- def _call_view(self, view, **kwargs):
- req = self._cw
- divid = req.form.get('divid', 'pageContent')
+ def _call_view(self, view, paginate=False, **kwargs):
+ divid = self._cw.form.get('divid', 'pageContent')
# we need to call pagination before with the stream set
stream = view.set_stream()
- if req.form.get('paginate'):
+ if paginate:
if divid == 'pageContent':
# mimick main template behaviour
stream.write(u'<div id="pageContent">')
@@ -359,12 +356,12 @@
if divid == 'pageContent':
stream.write(u'<div id="contentmain">')
view.render(**kwargs)
- extresources = req.html_headers.getvalue(skiphead=True)
+ extresources = self._cw.html_headers.getvalue(skiphead=True)
if extresources:
stream.write(u'<div class="ajaxHtmlHead">\n') # XXX use a widget ?
stream.write(extresources)
stream.write(u'</div>\n')
- if req.form.get('paginate') and divid == 'pageContent':
+ if paginate and divid == 'pageContent':
stream.write(u'</div></div>')
return stream.getvalue()
@@ -384,7 +381,7 @@
vid = req.form.get('fallbackvid', 'noresult')
view = self._cw.vreg['views'].select(vid, req, rset=rset)
self.validate_cache(view)
- return self._call_view(view)
+ return self._call_view(view, paginate=req.form.get('paginate'))
@xhtmlize
def js_prop_widget(self, propkey, varname, tabindex=None):
@@ -422,16 +419,7 @@
**extraargs)
#except NoSelectableObject:
# raise RemoteCallFailed('unselectable')
- extraargs = extraargs or {}
- stream = comp.set_stream()
- comp.render(**extraargs)
- # XXX why not _call_view ?
- extresources = self._cw.html_headers.getvalue(skiphead=True)
- if extresources:
- stream.write(u'<div class="ajaxHtmlHead">\n')
- stream.write(extresources)
- stream.write(u'</div>\n')
- return stream.getvalue()
+ return self._call_view(comp, **extraargs)
@check_pageid
@xhtmlize
@@ -460,15 +448,7 @@
args['reload'] = json.loads(args['reload'])
rset = req.eid_rset(int(self._cw.form['eid']))
view = req.vreg['views'].select('doreledit', req, rset=rset, rtype=args['rtype'])
- stream = view.set_stream()
- view.render(**args)
- # XXX why not _call_view ?
- extresources = req.html_headers.getvalue(skiphead=True)
- if extresources:
- stream.write(u'<div class="ajaxHtmlHead">\n')
- stream.write(extresources)
- stream.write(u'</div>\n')
- return stream.getvalue()
+ return self._call_view(view, **args)
@jsonize
def js_i18n(self, msgids):
@@ -484,7 +464,7 @@
@jsonize
def js_external_resource(self, resource):
"""returns the URL of the external resource named `resource`"""
- return self._cw.external_resource(resource)
+ return self._cw.uiprops[resource]
@check_pageid
@jsonize
@@ -584,52 +564,10 @@
def js_add_pending_delete(self, (eidfrom, rel, eidto)):
self._add_pending(eidfrom, rel, eidto, 'delete')
- # XXX specific code. Kill me and my AddComboBox friend
- @jsonize
- def js_add_and_link_new_entity(self, etype_to, rel, eid_to, etype_from, value_from):
- # create a new entity
- eid_from = self._cw.execute('INSERT %s T : T name "%s"' % ( etype_from, value_from ))[0][0]
- # link the new entity to the main entity
- rql = 'SET F %(rel)s T WHERE F eid %(eid_to)s, T eid %(eid_from)s' % {'rel' : rel, 'eid_to' : eid_to, 'eid_from' : eid_from}
- return eid_from
# XXX move to massmailing
-class SendMailController(Controller):
- __regid__ = 'sendmail'
- __select__ = authenticated_user() & match_form_params('recipient', 'mailbody', 'subject')
- def recipients(self):
- """returns an iterator on email's recipients as entities"""
- eids = self._cw.form['recipient']
- # eids may be a string if only one recipient was specified
- if isinstance(eids, basestring):
- rset = self._cw.execute('Any X WHERE X eid %(x)s', {'x': eids})
- else:
- rset = self._cw.execute('Any X WHERE X eid in (%s)' % (','.join(eids)))
- return rset.entities()
-
- def sendmail(self, recipient, subject, body):
- msg = format_mail({'email' : self._cw.user.get_email(),
- 'name' : self._cw.user.dc_title(),},
- [recipient], body, subject)
- if not self._cw.vreg.config.sendmails([(msg, [recipient])]):
- msg = self._cw._('could not connect to the SMTP server')
- url = self._cw.build_url(__message=msg)
- raise Redirect(url)
-
- def publish(self, rset=None):
- # XXX this allows users with access to an cubicweb instance to use it as
- # a mail relay
- body = self._cw.form['mailbody']
- subject = self._cw.form['subject']
- for recipient in self.recipients():
- text = body % recipient.as_email_context()
- self.sendmail(recipient.get_email(), subject, text)
- url = self._cw.build_url(__message=self._cw._('emails successfully sent'))
- raise Redirect(url)
-
-
-class MailBugReportController(SendMailController):
+class MailBugReportController(Controller):
__regid__ = 'reportbug'
__select__ = match_form_params('description')
@@ -640,7 +578,7 @@
raise Redirect(url)
-class UndoController(SendMailController):
+class UndoController(Controller):
__regid__ = 'undo'
__select__ = authenticated_user() & match_form_params('txuuid')
--- a/web/views/basetemplates.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/basetemplates.py Wed Jun 02 16:15:05 2010 +0200
@@ -168,7 +168,7 @@
self.wview('header', rset=self.cw_rset, view=view)
w(u'<div id="page"><table width="100%" border="0" id="mainLayout"><tr>\n')
self.nav_column(view, 'left')
- w(u'<td id="contentcol">\n')
+ w(u'<td id="contentColumn">\n')
components = self._cw.vreg['components']
rqlcomp = components.select_or_none('rqlinput', self._cw, rset=self.cw_rset)
if rqlcomp:
@@ -190,7 +190,7 @@
boxes = list(self._cw.vreg['boxes'].poss_visible_objects(
self._cw, rset=self.cw_rset, view=view, context=context))
if boxes:
- self.w(u'<td class="navcol"><div class="navboxes">\n')
+ self.w(u'<td id="navColumn%s"><div class="navboxes">\n' % context.capitalize())
for box in boxes:
box.render(w=self.w, view=view)
self.w(u'</div></td>\n')
@@ -254,7 +254,7 @@
w(u'<body>\n')
w(u'<div id="page">')
w(u'<table width="100%" height="100%" border="0"><tr>\n')
- w(u'<td class="navcol">\n')
+ w(u'<td id="navColumnLeft">\n')
self.topleft_header()
boxes = list(self._cw.vreg['boxes'].poss_visible_objects(
self._cw, rset=self.cw_rset, view=view, context='left'))
@@ -294,22 +294,22 @@
self.alternates()
def favicon(self):
- favicon = self._cw.external_resource('FAVICON', None)
+ favicon = self._cw.uiprops.get('FAVICON', None)
if favicon:
self.whead(u'<link rel="shortcut icon" href="%s"/>\n' % favicon)
def stylesheets(self):
req = self._cw
add_css = req.add_css
- for css in req.external_resource('STYLESHEETS'):
+ for css in req.uiprops['STYLESHEETS']:
add_css(css, localfile=False)
- for css in req.external_resource('STYLESHEETS_PRINT'):
+ for css in req.uiprops['STYLESHEETS_PRINT']:
add_css(css, u'print', localfile=False)
- for css in req.external_resource('IE_STYLESHEETS'):
+ for css in req.uiprops['STYLESHEETS_IE']:
add_css(css, localfile=False, ieonly=True)
def javascripts(self):
- for jscript in self._cw.external_resource('JAVASCRIPTS'):
+ for jscript in self._cw.uiprops['JAVASCRIPTS']:
self._cw.add_js(jscript, localfile=False)
def alternates(self):
@@ -389,13 +389,15 @@
def call(self, **kwargs):
req = self._cw
- self.w(u'<div class="footer">')
+ self.w(u'<div id="footer">')
actions = self._cw.vreg['actions'].possible_actions(self._cw,
rset=self.cw_rset)
footeractions = actions.get('footer', ())
for i, action in enumerate(footeractions):
- self.w(u'<a href="%s">%s</a>' % (action.url(),
- self._cw._(action.title)))
+ self.w(u'<a href="%s"' % action.url())
+ if getattr(action, 'html_class'):
+ self.w(u' class="%s"' % action.html_class())
+ self.w(u'>%s</a>' % self._cw._(action.title))
if i < (len(footeractions) - 1):
self.w(u' | ')
self.w(u'</div>')
@@ -469,7 +471,7 @@
self.w(u'<div id="loginTitle">%s</div>' % stitle)
self.w(u'<div id="loginContent">\n')
if showmessage and self._cw.message:
- self.w(u'<div class="simpleMessage">%s</div>\n' % self._cw.message)
+ self.w(u'<div class="loginMessage">%s</div>\n' % self._cw.message)
if self._cw.vreg.config['auth-mode'] != 'http':
# Cookie authentication
self.login_form(id)
--- a/web/views/calendar.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/calendar.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,20 +15,36 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""html calendar views
+"""html calendar views"""
-"""
__docformat__ = "restructuredtext en"
_ = unicode
from datetime import datetime, date, timedelta
from logilab.mtconverter import xml_escape
-from logilab.common.date import strptime, date_range, todate, todatetime
+from logilab.common.date import ONEDAY, strptime, date_range, todate, todatetime
from cubicweb.interfaces import ICalendarable
-from cubicweb.selectors import implements
-from cubicweb.view import EntityView
+from cubicweb.selectors import implements, adaptable
+from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
+
+
+class ICalendarableAdapter(EntityAdapter):
+ __regid__ = 'ICalendarable'
+ __select__ = implements(ICalendarable) # XXX for bw compat, should be abstract
+
+ @property
+ @implements_adapter_compat('ICalendarable')
+ def start(self):
+ """return start date"""
+ raise NotImplementedError
+
+ @property
+ @implements_adapter_compat('ICalendarable')
+ def stop(self):
+ """return stop state"""
+ raise NotImplementedError
# useful constants & functions ################################################
@@ -52,7 +68,7 @@
Does apply to ICalendarable compatible entities
"""
- __select__ = implements(ICalendarable)
+ __select__ = adaptable('ICalendarable')
paginable = False
content_type = 'text/calendar'
title = _('iCalendar')
@@ -66,10 +82,11 @@
event = ical.add('vevent')
event.add('summary').value = task.dc_title()
event.add('description').value = task.dc_description()
- if task.start:
- event.add('dtstart').value = task.start
- if task.stop:
- event.add('dtend').value = task.stop
+ icalendarable = task.cw_adapt_to('ICalendarable')
+ if icalendarable.start:
+ event.add('dtstart').value = icalendarable.start
+ if icalendarable.stop:
+ event.add('dtend').value = icalendarable.stop
buff = ical.serialize()
if not isinstance(buff, unicode):
@@ -85,7 +102,7 @@
Does apply to ICalendarable compatible entities
"""
__regid__ = 'hcal'
- __select__ = implements(ICalendarable)
+ __select__ = adaptable('ICalendarable')
paginable = False
title = _('hCalendar')
#templatable = False
@@ -98,10 +115,15 @@
self.w(u'<h3 class="summary">%s</h3>' % xml_escape(task.dc_title()))
self.w(u'<div class="description">%s</div>'
% task.dc_description(format='text/html'))
- if task.start:
- self.w(u'<abbr class="dtstart" title="%s">%s</abbr>' % (task.start.isoformat(), self._cw.format_date(task.start)))
- if task.stop:
- self.w(u'<abbr class="dtstop" title="%s">%s</abbr>' % (task.stop.isoformat(), self._cw.format_date(task.stop)))
+ icalendarable = task.cw_adapt_to('ICalendarable')
+ if icalendarable.start:
+ self.w(u'<abbr class="dtstart" title="%s">%s</abbr>'
+ % (icalendarable.start.isoformat(),
+ self._cw.format_date(icalendarable.start)))
+ if icalendarable.stop:
+ self.w(u'<abbr class="dtstop" title="%s">%s</abbr>'
+ % (icalendarable.stop.isoformat(),
+ self._cw.format_date(icalendarable.stop)))
self.w(u'</div>')
self.w(u'</div>')
@@ -113,10 +135,15 @@
task = self.cw_rset.complete_entity(row, 0)
task.view('oneline', w=self.w)
if dates:
- if task.start and task.stop:
- self.w('<br/>' % self._cw._('from %(date)s' % {'date': self._cw.format_date(task.start)}))
- self.w('<br/>' % self._cw._('to %(date)s' % {'date': self._cw.format_date(task.stop)}))
- self.w('<br/>to %s'%self._cw.format_date(task.stop))
+ icalendarable = task.cw_adapt_to('ICalendarable')
+ if icalendarable.start and icalendarable.stop:
+ self.w('<br/> %s' % self._cw._('from %(date)s')
+ % {'date': self._cw.format_date(icalendarable.start)})
+ self.w('<br/> %s' % self._cw._('to %(date)s')
+ % {'date': self._cw.format_date(icalendarable.stop)})
+ else:
+ self.w('<br/>%s'%self._cw.format_date(icalendarable.start
+ or icalendarable.stop))
class CalendarLargeItemView(CalendarItemView):
__regid__ = 'calendarlargeitem'
@@ -128,22 +155,25 @@
self.color = color
self.index = index
self.length = 1
+ icalendarable = task.cw_adapt_to('ICalendarable')
+ self.start = icalendarable.start
+ self.stop = icalendarable.stop
def in_working_hours(self):
"""predicate returning True is the task is in working hours"""
- if todatetime(self.task.start).hour > 7 and todatetime(self.task.stop).hour < 20:
+ if todatetime(self.start).hour > 7 and todatetime(self.stop).hour < 20:
return True
return False
def is_one_day_task(self):
- task = self.task
- return task.start and task.stop and task.start.isocalendar() == task.stop.isocalendar()
+ return self.start and self.stop and self.start.isocalendar() == self.stop.isocalendar()
class OneMonthCal(EntityView):
"""At some point, this view will probably replace ampm calendars"""
__regid__ = 'onemonthcal'
- __select__ = implements(ICalendarable)
+ __select__ = adaptable('ICalendarable')
+
paginable = False
title = _('one month')
@@ -181,13 +211,14 @@
else:
user = None
the_dates = []
- tstart = task.start
+ icalendarable = task.cw_adapt_to('ICalendarable')
+ tstart = icalendarable.start
if tstart:
- tstart = todate(task.start)
+ tstart = todate(icalendarable.start)
if tstart > lastday:
continue
the_dates = [tstart]
- tstop = task.stop
+ tstop = icalendarable.stop
if tstop:
tstop = todate(tstop)
if tstop < firstday:
@@ -199,7 +230,7 @@
the_dates = [tstart]
else:
the_dates = date_range(max(tstart, firstday),
- min(tstop, lastday))
+ min(tstop + ONEDAY, lastday))
if not the_dates:
continue
@@ -335,7 +366,8 @@
class OneWeekCal(EntityView):
"""At some point, this view will probably replace ampm calendars"""
__regid__ = 'oneweekcal'
- __select__ = implements(ICalendarable)
+ __select__ = adaptable('ICalendarable')
+
paginable = False
title = _('one week')
@@ -368,8 +400,9 @@
continue
done_tasks.append(task)
the_dates = []
- tstart = task.start
- tstop = task.stop
+ icalendarable = task.cw_adapt_to('ICalendarable')
+ tstart = icalendarable.start
+ tstop = icalendarable.stop
if tstart:
tstart = todate(tstart)
if tstart > lastday:
@@ -382,7 +415,7 @@
the_dates = [tstop]
if tstart and tstop:
the_dates = date_range(max(tstart, firstday),
- min(tstop, lastday))
+ min(tstop + ONEDAY, lastday))
if not the_dates:
continue
@@ -462,7 +495,7 @@
def _build_calendar_cell(self, date, task_descrs):
inday_tasks = [t for t in task_descrs if t.is_one_day_task() and t.in_working_hours()]
wholeday_tasks = [t for t in task_descrs if not t.is_one_day_task()]
- inday_tasks.sort(key=lambda t:t.task.start)
+ inday_tasks.sort(key=lambda t:t.start)
sorted_tasks = []
for i, t in enumerate(wholeday_tasks):
t.index = i
@@ -470,7 +503,7 @@
while inday_tasks:
t = inday_tasks.pop(0)
for i, c in enumerate(sorted_tasks):
- if not c or c[-1].task.stop <= t.task.start:
+ if not c or c[-1].stop <= t.start:
c.append(t)
t.index = i+ncols
break
@@ -491,15 +524,15 @@
start_min = 0
stop_hour = 20
stop_min = 0
- if task.start:
- if date < todate(task.start) < date + ONEDAY:
- start_hour = max(8, task.start.hour)
- start_min = task.start.minute
- if task.stop:
- if date < todate(task.stop) < date + ONEDAY:
- stop_hour = min(20, task.stop.hour)
+ if task_desc.start:
+ if date < todate(task_desc.start) < date + ONEDAY:
+ start_hour = max(8, task_desc.start.hour)
+ start_min = task_desc.start.minute
+ if task_desc.stop:
+ if date < todate(task_desc.stop) < date + ONEDAY:
+ stop_hour = min(20, task_desc.stop.hour)
if stop_hour < 20:
- stop_min = task.stop.minute
+ stop_min = task_desc.stop.minute
height = 100.0*(stop_hour+stop_min/60.0-start_hour-start_min/60.0)/(20-8)
top = 100.0*(start_hour+start_min/60.0-8)/(20-8)
@@ -518,7 +551,7 @@
self.w(u'<div class="tooltip" ondblclick="stopPropagation(event); window.location.assign(\'%s\'); return false;">' % xml_escape(url))
task.view('tooltip', w=self.w)
self.w(u'</div>')
- if task.start is None:
+ if task_desc.start is None:
self.w(u'<div class="bottommarker">')
self.w(u'<div class="bottommarkerline" style="margin: 0px 3px 0px 3px; height: 1px;">')
self.w(u'</div>')
--- a/web/views/cwproperties.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/cwproperties.py Wed Jun 02 16:15:05 2010 +0200
@@ -35,7 +35,7 @@
from cubicweb.web.formfields import FIELDS, StringField
from cubicweb.web.formwidgets import (Select, TextInput, Button, SubmitButton,
FieldWidget)
-from cubicweb.web.views import primary, formrenderers
+from cubicweb.web.views import primary, formrenderers, editcontroller
uicfg.primaryview_section.tag_object_of(('*', 'for_user', '*'), 'hidden')
@@ -396,6 +396,15 @@
w(u'</div>')
+class CWPropertyIEditControlAdapter(editcontroller.IEditControlAdapter):
+ __select__ = implements('CWProperty')
+
+ def after_deletion_path(self):
+ """return (path, parameters) which should be used as redirect
+ information when this entity is being deleted
+ """
+ return 'view', {}
+
_afs = uicfg.autoform_section
_afs.tag_subject_of(('*', 'for_user', '*'), 'main', 'hidden')
_afs.tag_object_of(('*', 'for_user', '*'), 'main', 'hidden')
--- a/web/views/cwuser.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/cwuser.py Wed Jun 02 16:15:05 2010 +0200
@@ -80,7 +80,7 @@
if entity.firstname:
self.w(u'<foaf:givenname>%s</foaf:givenname>\n'
% xml_escape(entity.firstname))
- emailaddr = entity.get_email()
+ emailaddr = entity.cw_adapt_to('IEmailable').get_email()
if emailaddr:
self.w(u'<foaf:mbox>%s</foaf:mbox>\n' % xml_escape(emailaddr))
self.w(u'</foaf:Person>\n')
--- a/web/views/editcontroller.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/editcontroller.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,9 +15,8 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""The edit controller, handling form submitting.
+"""The edit controller, automatically handling entity form submitting"""
-"""
__docformat__ = "restructuredtext en"
from warnings import warn
@@ -27,9 +26,37 @@
from logilab.common.textutils import splitstrip
from cubicweb import Binary, ValidationError, typed_eid
-from cubicweb.web import INTERNAL_FIELD_VALUE, RequestError, NothingToEdit, ProcessFormError
+from cubicweb.view import EntityAdapter, implements_adapter_compat
+from cubicweb.selectors import implements
+from cubicweb.web import (INTERNAL_FIELD_VALUE, RequestError, NothingToEdit,
+ ProcessFormError)
from cubicweb.web.views import basecontrollers, autoform
+
+class IEditControlAdapter(EntityAdapter):
+ __regid__ = 'IEditControl'
+ __select__ = implements('Any')
+
+ @implements_adapter_compat('IEditControl')
+ def after_deletion_path(self):
+ """return (path, parameters) which should be used as redirect
+ information when this entity is being deleted
+ """
+ parent = self.entity.cw_adapt_to('IBreadCrumbs').parent_entity()
+ if parent is not None:
+ return parent.rest_path(), {}
+ return str(self.entity.e_schema).lower(), {}
+
+ @implements_adapter_compat('IEditControl')
+ def pre_web_edit(self):
+ """callback called by the web editcontroller when an entity will be
+ created/modified, to let a chance to do some entity specific stuff.
+
+ Do nothing by default.
+ """
+ pass
+
+
def valerror_eid(eid):
try:
return typed_eid(eid)
@@ -133,8 +160,6 @@
def _insert_entity(self, etype, eid, rqlquery):
rql = rqlquery.insert_query(etype)
try:
- # get the new entity (in some cases, the type might have
- # changed as for the File --> Image mutation)
entity = self._cw.execute(rql, rqlquery.kwargs).get_entity(0, 0)
neweid = entity.eid
except ValidationError, ex:
@@ -155,7 +180,7 @@
entity.eid = formparams['eid']
is_main_entity = self._cw.form.get('__maineid') == formparams['eid']
# let a chance to do some entity specific stuff
- entity.pre_web_edit()
+ entity.cw_adapt_to('IEditControl').pre_web_edit()
# create a rql query from parameters
rqlquery = RqlQuery()
# process inlined relations at the same time as attributes
@@ -276,9 +301,9 @@
eidtypes = tuple(eidtypes)
for eid, etype in eidtypes:
entity = self._cw.entity_from_eid(eid, etype)
- path, params = entity.after_deletion_path()
+ path, params = entity.cw_adapt_to('IEditControl').after_deletion_path()
redirect_info.add( (path, tuple(params.iteritems())) )
- entity.delete()
+ entity.cw_delete()
if len(redirect_info) > 1:
# In the face of ambiguity, refuse the temptation to guess.
self._after_deletion_path = 'view', ()
--- a/web/views/editforms.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/editforms.py Wed Jun 02 16:15:05 2010 +0200
@@ -207,7 +207,7 @@
if not rschema.final:
# ensure relation cache is filed
rset = self.copying.related(rschema, role)
- self.newentity.set_related_cache(rschema, role, rset)
+ self.newentity.cw_set_relation_cache(rschema, role, rset)
def submited_message(self):
"""return the message that will be displayed on successful edition"""
@@ -344,7 +344,7 @@
rdef = entity.e_schema.rdef(rtype)
afs = uicfg.autoform_section.etype_get(
entity.__regid__, rtype, 'subject', rdef.object)
- if 'main_hidden' in afs or not entity.has_perm('update'):
+ if 'main_hidden' in afs or not entity.cw_has_perm('update'):
return False
if not rdef.has_perm(self._cw, 'update', eid=entity.eid):
return False
--- a/web/views/editviews.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/editviews.py Wed Jun 02 16:15:05 2010 +0200
@@ -59,9 +59,9 @@
# them. Use fetch_order and not fetch_unrelated_order as sort method
# since the latter is mainly there to select relevant items in the combo
# box, it doesn't give interesting result in this context
- rql, args = entity.unrelated_rql(rtype, etype, role,
- ordermethod='fetch_order',
- vocabconstraints=False)
+ rql, args = entity.cw_unrelated_rql(rtype, etype, role,
+ ordermethod='fetch_order',
+ vocabconstraints=False)
rset = self._cw.execute(rql, args, tuple(args))
return rset, 'list', "search-associate-content", True
--- a/web/views/emailaddress.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/emailaddress.py Wed Jun 02 16:15:05 2010 +0200
@@ -26,7 +26,7 @@
from cubicweb.selectors import implements
from cubicweb import Unauthorized
from cubicweb.web import uicfg
-from cubicweb.web.views import baseviews, primary
+from cubicweb.web.views import baseviews, primary, ibreadcrumbs
_pvs = uicfg.primaryview_section
_pvs.tag_subject_of(('*', 'use_email', '*'), 'attributes')
@@ -138,3 +138,10 @@
def cell_call(self, row, col, **kwargs):
self.w(self.cw_rset.get_entity(row, col).display_address())
+
+
+class EmailAddressIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+ __select__ = implements('EmailAddress')
+
+ def parent_entity(self):
+ return self.entity.email_of
--- a/web/views/embedding.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/embedding.py Wed Jun 02 16:15:05 2010 +0200
@@ -16,10 +16,8 @@
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""Objects interacting together to provides the external page embeding
-functionality.
+functionality."""
-
-"""
__docformat__ = "restructuredtext en"
import re
@@ -29,16 +27,27 @@
from logilab.mtconverter import guess_encoding
-from cubicweb.selectors import (one_line_rset, score_entity,
- match_search_state, implements)
+from cubicweb.selectors import (one_line_rset, score_entity, implements,
+ adaptable, match_search_state)
from cubicweb.interfaces import IEmbedable
-from cubicweb.view import NOINDEX, NOFOLLOW
+from cubicweb.view import NOINDEX, NOFOLLOW, EntityAdapter, implements_adapter_compat
from cubicweb.uilib import soup2xhtml
from cubicweb.web.controller import Controller
from cubicweb.web.action import Action
from cubicweb.web.views import basetemplates
+class IEmbedableAdapter(EntityAdapter):
+ """interface for embedable entities"""
+ __regid__ = 'IEmbedable'
+ __select__ = implements(IEmbedable) # XXX for bw compat, should be abstract
+
+ @implements_adapter_compat('IEmbedable')
+ def embeded_url(self):
+ """embed action interface"""
+ raise NotImplementedError
+
+
class ExternalTemplate(basetemplates.TheMainTemplate):
"""template embeding an external web pages into CubicWeb web interface
"""
@@ -92,7 +101,7 @@
def entity_has_embedable_url(entity):
"""return 1 if the entity provides an allowed embedable url"""
- url = entity.embeded_url()
+ url = entity.cw_adapt_to('IEmbedable').embeded_url()
if not url or not url.strip():
return 0
allowed = entity._cw.vreg.config['embed-allowed']
@@ -107,14 +116,14 @@
"""
__regid__ = 'embed'
__select__ = (one_line_rset() & match_search_state('normal')
- & implements(IEmbedable)
+ & adaptable('IEmbedable')
& score_entity(entity_has_embedable_url))
title = _('embed')
def url(self, row=0):
entity = self.cw_rset.get_entity(row, 0)
- url = urljoin(self._cw.base_url(), entity.embeded_url())
+ url = urljoin(self._cw.base_url(), entity.cw_adapt_to('IEmbedable').embeded_url())
if self._cw.form.has_key('rql'):
return self._cw.build_url('embed', url=url, rql=self._cw.form['rql'])
return self._cw.build_url('embed', url=url)
--- a/web/views/ibreadcrumbs.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/ibreadcrumbs.py Wed Jun 02 16:15:05 2010 +0200
@@ -21,20 +21,79 @@
__docformat__ = "restructuredtext en"
_ = unicode
+from warnings import warn
+
from logilab.mtconverter import xml_escape
-from cubicweb.interfaces import IBreadCrumbs
-from cubicweb.selectors import (one_line_rset, implements, one_etype_rset,
- multi_lines_rset, any_rset)
-from cubicweb.view import EntityView, Component
+#from cubicweb.interfaces import IBreadCrumbs
+from cubicweb.selectors import (implements, one_line_rset, adaptable,
+ one_etype_rset, multi_lines_rset, any_rset)
+from cubicweb.view import EntityView, Component, EntityAdapter
# don't use AnyEntity since this may cause bug with isinstance() due to reloading
from cubicweb.entity import Entity
from cubicweb import tags, uilib
+# ease bw compat
+def ibreadcrumb_adapter(entity):
+ if hasattr(entity, 'breadcrumbs'):
+ warn('[3.9] breadcrumbs() method is deprecated, define a custom '
+ 'IBreadCrumbsAdapter for %s instead' % entity.__class__,
+ DeprecationWarning)
+ return entity
+ return entity.cw_adapt_to('IBreadCrumbs')
+
+
+class IBreadCrumbsAdapter(EntityAdapter):
+ """adapters for entities which can be"located" on some path to display in
+ the web ui
+ """
+ __regid__ = 'IBreadCrumbs'
+ __select__ = implements('Any', accept_none=False)
+
+ def parent_entity(self):
+ if hasattr(self.entity, 'parent'):
+ warn('[3.9] parent() method is deprecated, define a '
+ 'custom IBreadCrumbsAdapter/ITreeAdapter for %s instead'
+ % self.entity.__class__, DeprecationWarning)
+ return self.entity.parent()
+ itree = self.entity.cw_adapt_to('ITree')
+ if itree is not None:
+ return itree.parent()
+ return None
+
+ def breadcrumbs(self, view=None, recurs=False):
+ """return a list containing some:
+
+ * tuple (url, label)
+ * entity
+ * simple label string
+
+ defining path from a root to the current view
+
+ the main view is given as argument so breadcrumbs may vary according
+ to displayed view (may be None). When recursing on a parent entity,
+ the `recurs` argument should be set to True.
+ """
+ parent = self.parent_entity()
+ if parent is not None:
+ adapter = ibreadcrumb_adapter(parent)
+ path = adapter.breadcrumbs(view, True) + [self.entity]
+ else:
+ path = [self.entity]
+ if not recurs:
+ if view is None:
+ if 'vtitle' in self._cw.form:
+ # embeding for instance
+ path.append( self._cw.form['vtitle'] )
+ elif view.__regid__ != 'primary' and hasattr(view, 'title'):
+ path.append( self._cw._(view.title) )
+ return path
+
+
class BreadCrumbEntityVComponent(Component):
__regid__ = 'breadcrumbs'
- __select__ = one_line_rset() & implements(IBreadCrumbs, accept_none=False)
+ __select__ = one_line_rset() & adaptable('IBreadCrumbs')
cw_property_defs = {
_('visible'): dict(type='Boolean', default=True,
@@ -47,7 +106,8 @@
def call(self, view=None, first_separator=True):
entity = self.cw_rset.get_entity(0, 0)
- path = entity.breadcrumbs(view)
+ adapter = ibreadcrumb_adapter(entity)
+ path = adapter.breadcrumbs(view)
if path:
self.open_breadcrumbs()
if first_separator:
@@ -73,7 +133,7 @@
self.w(u"\n")
self.wpath_part(parent, contextentity, i == len(path) - 1)
- def wpath_part(self, part, contextentity, last=False):
+ def wpath_part(self, part, contextentity, last=False): # XXX deprecates last argument?
if isinstance(part, Entity):
self.w(part.view('breadcrumbs'))
elif isinstance(part, tuple):
@@ -88,7 +148,7 @@
class BreadCrumbETypeVComponent(BreadCrumbEntityVComponent):
__select__ = multi_lines_rset() & one_etype_rset() & \
- implements(IBreadCrumbs, accept_none=False)
+ adaptable('IBreadCrumbs')
def render_breadcrumbs(self, contextentity, path):
# XXX hack: only display etype name or first non entity path part
--- a/web/views/idownloadable.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/idownloadable.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,29 +15,21 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""Specific views for entities implementing IDownloadable
+"""Specific views for entities adapting to IDownloadable"""
-"""
__docformat__ = "restructuredtext en"
_ = unicode
from logilab.mtconverter import BINARY_ENCODINGS, TransformError, xml_escape
from cubicweb.view import EntityView
-from cubicweb.selectors import (one_line_rset, score_entity,
- implements, match_context_prop)
-from cubicweb.interfaces import IDownloadable
+from cubicweb.selectors import (one_line_rset, implements, match_context_prop,
+ adaptable, has_mimetype)
from cubicweb.mttransforms import ENGINE
from cubicweb.web.box import EntityBoxTemplate
from cubicweb.web.views import primary, baseviews
-def is_image(entity):
- mt = entity.download_content_type()
- if not (mt and mt.startswith('image/')):
- return 0
- return 1
-
def download_box(w, entity, title=None, label=None, footer=u''):
req = entity._cw
w(u'<div class="sideBox">')
@@ -47,8 +39,8 @@
% xml_escape(title))
w(u'<div class="sideBox downloadBox"><div class="sideBoxBody">')
w(u'<a href="%s"><img src="%s" alt="%s"/> %s</a>'
- % (xml_escape(entity.download_url()),
- req.external_resource('DOWNLOAD_ICON'),
+ % (xml_escape(entity.cw_adapt_to('IDownloadable').download_url()),
+ req.uiprops['DOWNLOAD_ICON'],
_('download icon'), xml_escape(label or entity.dc_title())))
w(u'%s</div>' % footer)
w(u'</div></div>\n')
@@ -58,8 +50,8 @@
__regid__ = 'download_box'
# no download box for images
# XXX primary_view selector ?
- __select__ = (one_line_rset() & implements(IDownloadable) &
- match_context_prop() & ~score_entity(is_image))
+ __select__ = (one_line_rset() & match_context_prop()
+ & adaptable('IDownloadable') & ~has_mimetype('image/'))
order = 10
def cell_call(self, row, col, title=None, label=None, **kwargs):
@@ -72,7 +64,7 @@
downloading of entities providing the necessary interface
"""
__regid__ = 'download'
- __select__ = one_line_rset() & implements(IDownloadable)
+ __select__ = one_line_rset() & adaptable('IDownloadable')
templatable = False
content_type = 'application/octet-stream'
@@ -81,47 +73,52 @@
def set_request_content_type(self):
"""overriden to set the correct filetype and filename"""
- entity = self.cw_rset.complete_entity(0, 0)
- encoding = entity.download_encoding()
+ entity = self.cw_rset.complete_entity(self.cw_row or 0, self.cw_col or 0)
+ adapter = entity.cw_adapt_to('IDownloadable')
+ encoding = adapter.download_encoding()
if encoding in BINARY_ENCODINGS:
contenttype = 'application/%s' % encoding
encoding = None
else:
- contenttype = entity.download_content_type()
+ contenttype = adapter.download_content_type()
self._cw.set_content_type(contenttype or self.content_type,
- filename=entity.download_file_name(),
+ filename=adapter.download_file_name(),
encoding=encoding)
def call(self):
- self.w(self.cw_rset.complete_entity(0, 0).download_data())
+ entity = self.cw_rset.complete_entity(self.cw_row or 0, self.cw_col or 0)
+ adapter = entity.cw_adapt_to('IDownloadable')
+ self.w(adapter.download_data())
class DownloadLinkView(EntityView):
"""view displaying a link to download the file"""
__regid__ = 'downloadlink'
- __select__ = implements(IDownloadable)
+ __select__ = adaptable('IDownloadable')
title = None # should not be listed in possible views
def cell_call(self, row, col, title=None, **kwargs):
entity = self.cw_rset.get_entity(row, col)
- url = xml_escape(entity.download_url())
+ url = xml_escape(entity.cw_adapt_to('IDownloadable').download_url())
self.w(u'<a href="%s">%s</a>' % (url, xml_escape(title or entity.dc_title())))
class IDownloadablePrimaryView(primary.PrimaryView):
- __select__ = implements(IDownloadable)
+ __select__ = adaptable('IDownloadable')
def render_entity_attributes(self, entity):
super(IDownloadablePrimaryView, self).render_entity_attributes(entity)
self.w(u'<div class="content">')
- contenttype = entity.download_content_type()
+ adapter = entity.cw_adapt_to('IDownloadable')
+ contenttype = adapter.download_content_type()
if contenttype.startswith('image/'):
self.wview('image', entity.cw_rset, row=entity.cw_row)
else:
self.wview('downloadlink', entity.cw_rset, title=self._cw._('download'), row=entity.cw_row)
try:
if ENGINE.has_input(contenttype):
+ # XXX expect File like schema (access to 'data' attribute)
self.w(entity.printable_value('data'))
except TransformError:
pass
@@ -133,21 +130,22 @@
class IDownloadableLineView(baseviews.OneLineView):
- __select__ = implements(IDownloadable)
+ __select__ = adaptable('IDownloadable')
def cell_call(self, row, col, title=None, **kwargs):
"""the oneline view is a link to download the file"""
entity = self.cw_rset.get_entity(row, col)
url = xml_escape(entity.absolute_url())
- name = xml_escape(title or entity.download_file_name())
- durl = xml_escape(entity.download_url())
+ adapter = entity.cw_adapt_to('IDownloadable')
+ name = xml_escape(title or adapter.download_file_name())
+ durl = xml_escape(adapter.download_url())
self.w(u'<a href="%s">%s</a> [<a href="%s">%s</a>]' %
(url, name, durl, self._cw._('download')))
class ImageView(EntityView):
__regid__ = 'image'
- __select__ = implements(IDownloadable) & score_entity(is_image)
+ __select__ = has_mimetype('image/')
title = _('image')
@@ -160,10 +158,11 @@
def cell_call(self, row, col, width=None, height=None, link=False):
entity = self.cw_rset.get_entity(row, col)
+ adapter = entity.cw_adapt_to('IDownloadable')
#if entity.data_format.startswith('image/'):
imgtag = u'<img src="%s" alt="%s" ' % (
- xml_escape(entity.download_url()),
- (self._cw._('download %s') % xml_escape(entity.download_file_name())))
+ xml_escape(adapter.download_url()),
+ (self._cw._('download %s') % xml_escape(adapter.download_file_name())))
if width:
imgtag += u'width="%i" ' % width
if height:
--- a/web/views/igeocodable.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/igeocodable.py Wed Jun 02 16:15:05 2010 +0200
@@ -21,27 +21,59 @@
__docformat__ = "restructuredtext en"
from cubicweb.interfaces import IGeocodable
-from cubicweb.view import EntityView
-from cubicweb.selectors import implements
+from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
+from cubicweb.selectors import implements, adaptable
from cubicweb.web import json
+class IGeocodableAdapter(EntityAdapter):
+ """interface required by geocoding views such as gmap-view"""
+ __regid__ = 'IGeocodable'
+ __select__ = implements(IGeocodable) # XXX for bw compat, should be abstract
+
+ @property
+ @implements_adapter_compat('IGeocodable')
+ def latitude(self):
+ """returns the latitude of the entity"""
+ raise NotImplementedError
+
+ @property
+ @implements_adapter_compat('IGeocodable')
+ def longitude(self):
+ """returns the longitude of the entity"""
+ raise NotImplementedError
+
+ @implements_adapter_compat('IGeocodable')
+ def marker_icon(self):
+ """returns the icon that should be used as the marker.
+
+ an icon is defined by a 4-uple:
+
+ (icon._url, icon.size, icon.iconAnchor, icon.shadow)
+ """
+ return (self._cw.uiprops['GMARKER_ICON'], (20, 34), (4, 34), None)
+
+
class GeocodingJsonView(EntityView):
__regid__ = 'geocoding-json'
- __select__ = implements(IGeocodable)
+ __select__ = adaptable('IGeocodable')
binary = True
templatable = False
content_type = 'application/json'
def call(self):
- # remove entities that don't define latitude and longitude
- self.cw_rset = self.cw_rset.filtered_rset(lambda e: e.latitude and e.longitude)
zoomlevel = self._cw.form.pop('zoomlevel', 8)
extraparams = self._cw.form.copy()
extraparams.pop('vid', None)
extraparams.pop('rql', None)
- markers = [self.build_marker_data(rowidx, extraparams)
- for rowidx in xrange(len(self.cw_rset))]
+ markers = []
+ for entity in self.cw_rset.entities():
+ igeocodable = entity.cw_adapt_to('IGeocodable')
+ # remove entities that don't define latitude and longitude
+ if not (igeocodable.latitude and igeocodable.longitude):
+ continue
+ markers.append(self.build_marker_data(entity, igeocodable,
+ extraparams))
center = {
'latitude': sum(marker['latitude'] for marker in markers) / len(markers),
'longitude': sum(marker['longitude'] for marker in markers) / len(markers),
@@ -53,24 +85,19 @@
}
self.w(json.dumps(geodata))
- def build_marker_data(self, row, extraparams):
- entity = self.cw_rset.get_entity(row, 0)
- icon = None
- if hasattr(entity, 'marker_icon'):
- icon = entity.marker_icon()
- else:
- icon = (self._cw.external_resource('GMARKER_ICON'), (20, 34), (4, 34), None)
- return {'latitude': entity.latitude, 'longitude': entity.longitude,
+ def build_marker_data(self, entity, igeocodable, extraparams):
+ return {'latitude': igeocodable.latitude,
+ 'longitude': igeocodable.longitude,
+ 'icon': igeocodable.marker_icon(),
'title': entity.dc_long_title(),
- #icon defines : (icon._url, icon.size, icon.iconAncho', icon.shadow)
- 'icon': icon,
- 'bubbleUrl': entity.absolute_url(vid='gmap-bubble', __notemplate=1, **extraparams),
+ 'bubbleUrl': entity.absolute_url(
+ vid='gmap-bubble', __notemplate=1, **extraparams),
}
class GoogleMapBubbleView(EntityView):
__regid__ = 'gmap-bubble'
- __select__ = implements(IGeocodable)
+ __select__ = adaptable('IGeocodable')
def cell_call(self, row, col):
entity = self.cw_rset.get_entity(row, col)
@@ -80,16 +107,14 @@
class GoogleMapsView(EntityView):
__regid__ = 'gmap-view'
- __select__ = implements(IGeocodable)
+ __select__ = adaptable('IGeocodable')
paginable = False
def call(self, gmap_key, width=400, height=400, uselabel=True, urlparams=None):
self._cw.demote_to_html()
- # remove entities that don't define latitude and longitude
- self.cw_rset = self.cw_rset.filtered_rset(lambda e: e.latitude and e.longitude)
- self._cw.add_js('http://maps.google.com/maps?sensor=false&file=api&v=2&key=%s' % gmap_key,
- localfile=False)
+ self._cw.add_js('http://maps.google.com/maps?sensor=false&file=api&v=2&key=%s'
+ % gmap_key, localfile=False)
self._cw.add_js( ('cubicweb.widgets.js', 'cubicweb.gmap.js', 'gmap.utility.labeledmarker.js') )
rql = self.cw_rset.printable_rql()
if urlparams is None:
@@ -98,7 +123,8 @@
loadurl = self._cw.build_url(rql=rql, vid='geocoding-json', **urlparams)
self.w(u'<div style="width: %spx; height: %spx;" class="widget gmap" '
u'cubicweb:wdgtype="GMapWidget" cubicweb:loadtype="auto" '
- u'cubicweb:loadurl="%s" cubicweb:uselabel="%s"> </div>' % (width, height, loadurl, uselabel))
+ u'cubicweb:loadurl="%s" cubicweb:uselabel="%s"> </div>'
+ % (width, height, loadurl, uselabel))
class GoogeMapsLegend(EntityView):
--- a/web/views/iprogress.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/iprogress.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,9 +15,8 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""Specific views for entities implementing IProgress
+"""Specific views for entities implementing IProgress/IMileStone"""
-"""
__docformat__ = "restructuredtext en"
_ = unicode
@@ -26,12 +25,120 @@
from logilab.mtconverter import xml_escape
from cubicweb.utils import make_uid
-from cubicweb.selectors import implements
+from cubicweb.selectors import implements, adaptable
from cubicweb.interfaces import IProgress, IMileStone
from cubicweb.schema import display_name
-from cubicweb.view import EntityView
+from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
from cubicweb.web.views.tableview import EntityAttributesTableView
+
+class IProgressAdapter(EntityAdapter):
+ """something that has a cost, a state and a progression.
+
+ You should at least override progress_info an in_progress methods on concret
+ implementations.
+ """
+ __regid__ = 'IProgress'
+ __select__ = implements(IProgress) # XXX for bw compat, should be abstract
+
+ @property
+ @implements_adapter_compat('IProgress')
+ def cost(self):
+ """the total cost"""
+ return self.progress_info()['estimated']
+
+ @property
+ @implements_adapter_compat('IProgress')
+ def revised_cost(self):
+ return self.progress_info().get('estimatedcorrected', self.cost)
+
+ @property
+ @implements_adapter_compat('IProgress')
+ def done(self):
+ """what is already done"""
+ return self.progress_info()['done']
+
+ @property
+ @implements_adapter_compat('IProgress')
+ def todo(self):
+ """what remains to be done"""
+ return self.progress_info()['todo']
+
+ @implements_adapter_compat('IProgress')
+ def progress_info(self):
+ """returns a dictionary describing progress/estimated cost of the
+ version.
+
+ - mandatory keys are (''estimated', 'done', 'todo')
+
+ - optional keys are ('notestimated', 'notestimatedcorrected',
+ 'estimatedcorrected')
+
+ 'noestimated' and 'notestimatedcorrected' should default to 0
+ 'estimatedcorrected' should default to 'estimated'
+ """
+ raise NotImplementedError
+
+ @implements_adapter_compat('IProgress')
+ def finished(self):
+ """returns True if status is finished"""
+ return not self.in_progress()
+
+ @implements_adapter_compat('IProgress')
+ def in_progress(self):
+ """returns True if status is not finished"""
+ raise NotImplementedError
+
+ @implements_adapter_compat('IProgress')
+ def progress(self):
+ """returns the % progress of the task item"""
+ try:
+ return 100. * self.done / self.revised_cost
+ except ZeroDivisionError:
+ # total cost is 0 : if everything was estimated, task is completed
+ if self.progress_info().get('notestimated'):
+ return 0.
+ return 100
+
+ @implements_adapter_compat('IProgress')
+ def progress_class(self):
+ return ''
+
+
+class IMileStoneAdapter(IProgressAdapter):
+ __regid__ = 'IMileStone'
+ __select__ = implements(IMileStone) # XXX for bw compat, should be abstract
+
+ parent_type = None # specify main task's type
+
+ @implements_adapter_compat('IMileStone')
+ def get_main_task(self):
+ """returns the main ITask entity"""
+ raise NotImplementedError
+
+ @implements_adapter_compat('IMileStone')
+ def initial_prevision_date(self):
+ """returns the initial expected end of the milestone"""
+ raise NotImplementedError
+
+ @implements_adapter_compat('IMileStone')
+ def eta_date(self):
+ """returns expected date of completion based on what remains
+ to be done
+ """
+ raise NotImplementedError
+
+ @implements_adapter_compat('IMileStone')
+ def completion_date(self):
+ """returns date on which the subtask has been completed"""
+ raise NotImplementedError
+
+ @implements_adapter_compat('IMileStone')
+ def contractors(self):
+ """returns the list of persons supposed to work on this task"""
+ raise NotImplementedError
+
+
class ProgressTableView(EntityAttributesTableView):
"""The progress table view is able to display progress information
of any object implement IMileStone.
@@ -50,8 +157,8 @@
"""
__regid__ = 'progress_table_view'
+ __select__ = adaptable('IMileStone')
title = _('task progression')
- __select__ = implements(IMileStone)
table_css = "progress"
css_files = ('cubicweb.iprogress.css',)
@@ -71,10 +178,7 @@
else:
content = entity.printable_value(col)
infos[col] = content
- if hasattr(entity, 'progress_class'):
- cssclass = entity.progress_class()
- else:
- cssclass = u''
+ cssclass = entity.cw_adapt_to('IMileStone').progress_class()
self.w(u"""<tr class="%s" onmouseover="addElementClass(this, 'highlighted');"
onmouseout="removeElementClass(this, 'highlighted')">""" % cssclass)
line = u''.join(u'<td>%%(%s)s</td>' % col for col in self.columns)
@@ -83,18 +187,18 @@
## header management ######################################################
- def header_for_project(self, ecls):
+ def header_for_project(self, sample):
"""use entity's parent type as label"""
- return display_name(self._cw, ecls.parent_type)
+ return display_name(self._cw, sample.cw_adapt_to('IMileStone').parent_type)
- def header_for_milestone(self, ecls):
+ def header_for_milestone(self, sample):
"""use entity's type as label"""
- return display_name(self._cw, ecls.__regid__)
+ return display_name(self._cw, sample.__regid__)
## cell management ########################################################
def build_project_cell(self, entity):
"""``project`` column cell renderer"""
- project = entity.get_main_task()
+ project = entity.cw_adapt_to('IMileStone').get_main_task()
if project:
return project.view('incontext')
return self._cw._('no related project')
@@ -105,15 +209,16 @@
def build_state_cell(self, entity):
"""``state`` column cell renderer"""
- return xml_escape(self._cw._(entity.state))
+ return xml_escape(entity.cw_adapt_to('IWorkflowable').printable_state)
def build_eta_date_cell(self, entity):
"""``eta_date`` column cell renderer"""
- if entity.finished():
- return self._cw.format_date(entity.completion_date())
- formated_date = self._cw.format_date(entity.initial_prevision_date())
- if entity.in_progress():
- eta_date = self._cw.format_date(entity.eta_date())
+ imilestone = entity.cw_adapt_to('IMileStone')
+ if imilestone.finished():
+ return self._cw.format_date(imilestone.completion_date())
+ formated_date = self._cw.format_date(imilestone.initial_prevision_date())
+ if imilestone.in_progress():
+ eta_date = self._cw.format_date(imilestone.eta_date())
_ = self._cw._
if formated_date:
formated_date += u' (%s %s)' % (_('expected:'), eta_date)
@@ -123,12 +228,14 @@
def build_todo_by_cell(self, entity):
"""``todo_by`` column cell renderer"""
- return u', '.join(p.view('outofcontext') for p in entity.contractors())
+ imilestone = entity.cw_adapt_to('IMileStone')
+ return u', '.join(p.view('outofcontext') for p in imilestone.contractors())
def build_cost_cell(self, entity):
"""``cost`` column cell renderer"""
_ = self._cw._
- pinfo = entity.progress_info()
+ imilestone = entity.cw_adapt_to('IMileStone')
+ pinfo = imilestone.progress_info()
totalcost = pinfo.get('estimatedcorrected', pinfo['estimated'])
missing = pinfo.get('notestimatedcorrected', pinfo.get('notestimated', 0))
costdescr = []
@@ -167,8 +274,9 @@
class ProgressBarView(EntityView):
"""displays a progress bar"""
__regid__ = 'progressbar'
+ __select__ = adaptable('IProgress')
+
title = _('progress bar')
- __select__ = implements(IProgress)
precision = 0.1
red_threshold = 1.1
@@ -176,10 +284,10 @@
yellow_threshold = 1
@classmethod
- def overrun(cls, entity):
+ def overrun(cls, iprogress):
"""overrun = done + todo - """
- if entity.done + entity.todo > entity.revised_cost:
- overrun = entity.done + entity.todo - entity.revised_cost
+ if iprogress.done + iprogress.todo > iprogress.revised_cost:
+ overrun = iprogress.done + iprogress.todo - iprogress.revised_cost
else:
overrun = 0
if overrun < cls.precision:
@@ -187,20 +295,21 @@
return overrun
@classmethod
- def overrun_percentage(cls, entity):
+ def overrun_percentage(cls, iprogress):
"""pourcentage overrun = overrun / budget"""
- if entity.revised_cost == 0:
+ if iprogress.revised_cost == 0:
return 0
else:
- return cls.overrun(entity) * 100. / entity.revised_cost
+ return cls.overrun(iprogress) * 100. / iprogress.revised_cost
def cell_call(self, row, col):
self._cw.add_css('cubicweb.iprogress.css')
self._cw.add_js('cubicweb.iprogress.js')
entity = self.cw_rset.get_entity(row, col)
- done = entity.done
- todo = entity.todo
- budget = entity.revised_cost
+ iprogress = entity.cw_adapt_to('IProgress')
+ done = iprogress.done
+ todo = iprogress.todo
+ budget = iprogress.revised_cost
if budget == 0:
pourcent = 100
else:
@@ -229,25 +338,23 @@
title = u'%s/%s = %i%%' % (done_str, budget_str, pourcent)
short_title = title
- if self.overrun_percentage(entity):
- title += u' overrun +%sj (+%i%%)' % (self.overrun(entity),
- self.overrun_percentage(entity))
- overrun = self.overrun(entity)
- if floor(overrun) == overrun or overrun>100:
- overrun_str = '%i' % overrun
+ overrunpercent = self.overrun_percentage(iprogress)
+ if overrunpercent:
+ overrun = self.overrun(iprogress)
+ title += u' overrun +%sj (+%i%%)' % (overrun, overrunpercent)
+ if floor(overrun) == overrun or overrun > 100:
+ short_title += u' +%i' % overrun
else:
- overrun_str = '%.1f' % overrun
- short_title += u' +%s' % overrun_str
+ short_title += u' +%.1f' % overrun
# write bars
maxi = max(done+todo, budget)
if maxi == 0:
maxi = 1
-
cid = make_uid('progress_bar')
- self._cw.html_headers.add_onload('draw_progressbar("canvas%s", %i, %i, %i, "%s");' %
- (cid,
- int(100.*done/maxi), int(100.*(done+todo)/maxi),
- int(100.*budget/maxi), color))
+ self._cw.html_headers.add_onload(
+ 'draw_progressbar("canvas%s", %i, %i, %i, "%s");' %
+ (cid, int(100.*done/maxi), int(100.*(done+todo)/maxi),
+ int(100.*budget/maxi), color))
self.w(u'%s<br/>'
u'<canvas class="progressbar" id="canvas%s" width="100" height="10"></canvas>'
% (xml_escape(short_title), cid))
--- a/web/views/isioc.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/isioc.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,20 +15,70 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""Specific views for SIOC interfaces
+"""Specific views for SIOC (Semantically-Interlinked Online Communities)
+http://sioc-project.org
"""
+
__docformat__ = "restructuredtext en"
from logilab.mtconverter import xml_escape
-from cubicweb.view import EntityView
-from cubicweb.selectors import implements
+from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
+from cubicweb.selectors import implements, adaptable
from cubicweb.interfaces import ISiocItem, ISiocContainer
+
+class ISIOCItemAdapter(EntityAdapter):
+ """interface for entities which may be represented as an ISIOC items"""
+ __regid__ = 'ISIOCItem'
+ __select__ = implements(ISiocItem) # XXX for bw compat, should be abstract
+
+ @implements_adapter_compat('ISIOCItem')
+ def isioc_content(self):
+ """return item's content"""
+ raise NotImplementedError
+
+ @implements_adapter_compat('ISIOCItem')
+ def isioc_container(self):
+ """return container entity"""
+ raise NotImplementedError
+
+ @implements_adapter_compat('ISIOCItem')
+ def isioc_type(self):
+ """return container type (post, BlogPost, MailMessage)"""
+ raise NotImplementedError
+
+ @implements_adapter_compat('ISIOCItem')
+ def isioc_replies(self):
+ """return replies items"""
+ raise NotImplementedError
+
+ @implements_adapter_compat('ISIOCItem')
+ def isioc_topics(self):
+ """return topics items"""
+ raise NotImplementedError
+
+
+class ISIOCContainerAdapter(EntityAdapter):
+ """interface for entities which may be represented as an ISIOC container"""
+ __regid__ = 'ISIOCContainer'
+ __select__ = implements(ISiocContainer) # XXX for bw compat, should be abstract
+
+ @implements_adapter_compat('ISIOCContainer')
+ def isioc_type(self):
+ """return container type (forum, Weblog, MailingList)"""
+ raise NotImplementedError
+
+ @implements_adapter_compat('ISIOCContainer')
+ def isioc_items(self):
+ """return contained items"""
+ raise NotImplementedError
+
+
class SIOCView(EntityView):
__regid__ = 'sioc'
- __select__ = EntityView.__select__ & implements(ISiocItem, ISiocContainer)
+ __select__ = adaptable('ISIOCItem', 'ISIOCContainer')
title = _('sioc')
templatable = False
content_type = 'text/xml'
@@ -52,48 +102,51 @@
class SIOCContainerView(EntityView):
__regid__ = 'sioc_element'
- __select__ = EntityView.__select__ & implements(ISiocContainer)
+ __select__ = adaptable('ISIOCContainer')
templatable = False
content_type = 'text/xml'
def cell_call(self, row, col):
entity = self.cw_rset.complete_entity(row, col)
- sioct = xml_escape(entity.isioc_type())
+ isioc = entity.cw_adapt_to('ISIOCContainer')
+ isioct = isioc.isioc_type()
self.w(u'<sioc:%s rdf:about="%s">\n'
- % (sioct, xml_escape(entity.absolute_url())))
+ % (isioct, xml_escape(entity.absolute_url())))
self.w(u'<dcterms:title>%s</dcterms:title>'
% xml_escape(entity.dc_title()))
self.w(u'<dcterms:created>%s</dcterms:created>'
- % entity.creation_date)
+ % entity.creation_date) # XXX format
self.w(u'<dcterms:modified>%s</dcterms:modified>'
- % entity.modification_date)
+ % entity.modification_date) # XXX format
self.w(u'<!-- FIXME : here be items -->')#entity.isioc_items()
- self.w(u'</sioc:%s>\n' % sioct)
+ self.w(u'</sioc:%s>\n' % isioct)
class SIOCItemView(EntityView):
__regid__ = 'sioc_element'
- __select__ = EntityView.__select__ & implements(ISiocItem)
+ __select__ = adaptable('ISIOCItem')
templatable = False
content_type = 'text/xml'
def cell_call(self, row, col):
entity = self.cw_rset.complete_entity(row, col)
- sioct = xml_escape(entity.isioc_type())
+ isioc = entity.cw_adapt_to('ISIOCItem')
+ isioct = isioc.isioc_type()
self.w(u'<sioc:%s rdf:about="%s">\n'
- % (sioct, xml_escape(entity.absolute_url())))
+ % (isioct, xml_escape(entity.absolute_url())))
self.w(u'<dcterms:title>%s</dcterms:title>'
% xml_escape(entity.dc_title()))
self.w(u'<dcterms:created>%s</dcterms:created>'
- % entity.creation_date)
+ % entity.creation_date) # XXX format
self.w(u'<dcterms:modified>%s</dcterms:modified>'
- % entity.modification_date)
- if entity.content:
- self.w(u'<sioc:content>%s</sioc:content>'''
- % xml_escape(entity.isioc_content()))
- if entity.related('entry_of'):
+ % entity.modification_date) # XXX format
+ content = isioc.isioc_content()
+ if content:
+ self.w(u'<sioc:content>%s</sioc:content>' % xml_escape(content))
+ container = isioc.isioc_container()
+ if container:
self.w(u'<sioc:has_container rdf:resource="%s"/>\n'
- % xml_escape(entity.isioc_container().absolute_url()))
+ % xml_escape(container.absolute_url()))
if entity.creator:
self.w(u'<sioc:has_creator>\n')
self.w(u'<sioc:User rdf:about="%s">\n'
@@ -103,5 +156,5 @@
self.w(u'</sioc:has_creator>\n')
self.w(u'<!-- FIXME : here be topics -->')#entity.isioc_topics()
self.w(u'<!-- FIXME : here be replies -->')#entity.isioc_replies()
- self.w(u' </sioc:%s>\n' % sioct)
+ self.w(u' </sioc:%s>\n' % isioct)
--- a/web/views/management.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/management.py Wed Jun 02 16:15:05 2010 +0200
@@ -203,7 +203,7 @@
cversions = []
for cube in self._cw.vreg.config.cubes():
cubeversion = vcconf.get(cube, self._cw._('no version information'))
- w(u"<b>Package %s version:</b> %s<br/>\n" % (cube, cubeversion))
+ w(u"<b>Cube %s version:</b> %s<br/>\n" % (cube, cubeversion))
cversions.append((cube, cubeversion))
w(u"</div>")
# creates a bug submission link if submit-mail is set
@@ -237,7 +237,7 @@
binfo += u'\n'.join(u' * %s = %s' % (k, v) for k, v in req.form.iteritems())
binfo += u'\n\n:CubicWeb version: %s\n' % (eversion,)
for pkg, pkgversion in cubes:
- binfo += u":Package %s version: %s\n" % (pkg, pkgversion)
+ binfo += u":Cube %s version: %s\n" % (pkg, pkgversion)
binfo += '\n'
return binfo
--- a/web/views/massmailing.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/massmailing.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,18 +15,17 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""Mass mailing form views
+"""Mass mailing handling: send mail to entities adaptable to IEmailable"""
-"""
__docformat__ = "restructuredtext en"
_ = unicode
import operator
-from cubicweb.interfaces import IEmailable
-from cubicweb.selectors import implements, authenticated_user
+from cubicweb.selectors import (implements, authenticated_user,
+ adaptable, match_form_params)
from cubicweb.view import EntityView
-from cubicweb.web import stdmsgs, action, form, formfields as ff
+from cubicweb.web import stdmsgs, controller, action, form, formfields as ff
from cubicweb.web.formwidgets import CheckBox, TextInput, AjaxWidget, ImgButton
from cubicweb.web.views import forms, formrenderers
@@ -34,8 +33,9 @@
class SendEmailAction(action.Action):
__regid__ = 'sendemail'
# XXX should check email is set as well
- __select__ = (action.Action.__select__ & implements(IEmailable)
- & authenticated_user())
+ __select__ = (action.Action.__select__
+ & authenticated_user()
+ & adaptable('IEmailable'))
title = _('send email')
category = 'mainactions'
@@ -49,9 +49,11 @@
def recipient_vocabulary(form, field):
- vocab = [(entity.get_email(), entity.eid) for entity in form.cw_rset.entities()]
+ vocab = [(entity.cw_adapt_to('IEmailable').get_email(), entity.eid)
+ for entity in form.cw_rset.entities()]
return [(label, value) for label, value in vocab if label]
+
class MassMailingForm(forms.FieldsForm):
__regid__ = 'massmailing'
@@ -62,10 +64,13 @@
sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}),
label=_('From:'),
- value=lambda f: '%s <%s>' % (f._cw.user.dc_title(), f._cw.user.get_email()))
+ value=lambda f: '%s <%s>' % (
+ f._cw.user.dc_title(),
+ f._cw.user.cw_adapt_to('IEmailable').get_email()))
recipient = ff.StringField(widget=CheckBox(), label=_('Recipients:'),
choices=recipient_vocabulary,
- value= lambda f: [entity.eid for entity in f.cw_rset.entities() if entity.get_email()])
+ value= lambda f: [entity.eid for entity in f.cw_rset.entities()
+ if entity.cw_adapt_to('IEmailable').get_email()])
subject = ff.StringField(label=_('Subject:'), max_length=256)
mailbody = ff.StringField(widget=AjaxWidget(wdgtype='TemplateTextField',
inputid='mailbody'))
@@ -84,8 +89,8 @@
def get_allowed_substitutions(self):
attrs = []
for coltype in self.cw_rset.column_types(0):
- eclass = self._cw.vreg['etypes'].etype_class(coltype)
- attrs.append(eclass.allowed_massmail_keys())
+ entity = self._cw.vreg['etypes'].etype_class(coltype)(self._cw)
+ attrs.append(entity.cw_adapt_to('IEmailable').allowed_massmail_keys())
return sorted(reduce(operator.and_, attrs))
def build_substitutions_help(self):
@@ -135,9 +140,36 @@
class MassMailingFormView(form.FormViewMixIn, EntityView):
__regid__ = 'massmailing'
- __select__ = implements(IEmailable) & authenticated_user()
+ __select__ = authenticated_user() & adaptable('IEmailable')
def call(self):
form = self._cw.vreg['forms'].select('massmailing', self._cw,
rset=self.cw_rset)
self.w(form.render())
+
+
+class SendMailController(controller.Controller):
+ __regid__ = 'sendmail'
+ __select__ = authenticated_user() & match_form_params('recipient', 'mailbody', 'subject')
+
+ def recipients(self):
+ """returns an iterator on email's recipients as entities"""
+ eids = self._cw.form['recipient']
+ # eids may be a string if only one recipient was specified
+ if isinstance(eids, basestring):
+ rset = self._cw.execute('Any X WHERE X eid %(x)s', {'x': eids})
+ else:
+ rset = self._cw.execute('Any X WHERE X eid in (%s)' % (','.join(eids)))
+ return rset.entities()
+
+ def publish(self, rset=None):
+ # XXX this allows users with access to an cubicweb instance to use it as
+ # a mail relay
+ body = self._cw.form['mailbody']
+ subject = self._cw.form['subject']
+ for recipient in self.recipients():
+ iemailable = recipient.cw_adapt_to('IEmailable')
+ text = body % iemailable.as_email_context()
+ self.sendmail(iemailable.get_email(), subject, text)
+ url = self._cw.build_url(__message=self._cw._('emails successfully sent'))
+ raise Redirect(url)
--- a/web/views/navigation.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/navigation.py Wed Jun 02 16:15:05 2010 +0200
@@ -25,11 +25,10 @@
from logilab.mtconverter import xml_escape
from logilab.common.deprecation import deprecated
-from cubicweb.interfaces import IPrevNext
from cubicweb.selectors import (paginated_rset, sorted_rset,
- primary_view, match_context_prop,
- one_line_rset, implements)
+ adaptable, implements)
from cubicweb.uilib import cut
+from cubicweb.view import EntityAdapter, implements_adapter_compat
from cubicweb.web.component import EntityVComponent, NavigationComponent
@@ -182,20 +181,41 @@
self.w(u'</div>')
+from cubicweb.interfaces import IPrevNext
+
+class IPrevNextAdapter(EntityAdapter):
+ """interface for entities which can be linked to a previous and/or next
+ entity
+ """
+ __regid__ = 'IPrevNext'
+ __select__ = implements(IPrevNext) # XXX for bw compat, else should be abstract
+
+ @implements_adapter_compat('IPrevNext')
+ def next_entity(self):
+ """return the 'next' entity"""
+ raise NotImplementedError
+
+ @implements_adapter_compat('IPrevNext')
+ def previous_entity(self):
+ """return the 'previous' entity"""
+ raise NotImplementedError
+
+
class NextPrevNavigationComponent(EntityVComponent):
__regid__ = 'prevnext'
# register msg not generated since no entity implements IPrevNext in cubicweb
# itself
title = _('contentnavigation_prevnext')
help = _('contentnavigation_prevnext_description')
- __select__ = (one_line_rset() & primary_view()
- & match_context_prop() & implements(IPrevNext))
+ __select__ = (EntityVComponent.__select__
+ & adaptable('IPrevNext'))
context = 'navbottom'
order = 10
def call(self, view=None):
entity = self.cw_rset.get_entity(0, 0)
- previous = entity.previous_entity()
- next = entity.next_entity()
+ adapter = entity.cw_adapt_to('IPrevNext')
+ previous = adapter.previous_entity()
+ next = adapter.next_entity()
if previous or next:
textsize = self._cw.property_value('navigation.short-line-size')
self.w(u'<div class="prevnext">')
--- a/web/views/old_calendar.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/old_calendar.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,9 +15,7 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""html calendar views
-
-"""
+"""html calendar views"""
from datetime import date, time, timedelta
@@ -26,8 +24,25 @@
next_month, first_day, last_day, date_range)
from cubicweb.interfaces import ICalendarViews
-from cubicweb.selectors import implements
-from cubicweb.view import EntityView
+from cubicweb.selectors import implements, adaptable
+from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
+
+class ICalendarViewsAdapter(EntityAdapter):
+ """calendar views interface"""
+ __regid__ = 'ICalendarViews'
+ __select__ = implements(ICalendarViews) # XXX for bw compat, should be abstract
+
+ @implements_adapter_compat('ICalendarViews')
+ def matching_dates(self, begin, end):
+ """
+ :param begin: day considered as begin of the range (`DateTime`)
+ :param end: day considered as end of the range (`DateTime`)
+
+ :return:
+ a list of dates (`DateTime`) in the range [`begin`, `end`] on which
+ this entity apply
+ """
+ raise NotImplementedError
# used by i18n tools
WEEKDAYS = [_("monday"), _("tuesday"), _("wednesday"), _("thursday"),
@@ -39,7 +54,7 @@
class _CalendarView(EntityView):
"""base calendar view containing helpful methods to build calendar views"""
- __select__ = implements(ICalendarViews,)
+ __select__ = adaptable('ICalendarViews')
paginable = False
# Navigation building methods / views ####################################
@@ -126,7 +141,7 @@
infos = u'<div class="event">'
infos += self._cw.view(itemvid, self.cw_rset, row=row)
infos += u'</div>'
- for date_ in entity.matching_dates(begin, end):
+ for date_ in entity.cw_adapt_to('ICalendarViews').matching_dates(begin, end):
day = date(date_.year, date_.month, date_.day)
try:
dt = time(date_.hour, date_.minute, date_.second)
@@ -288,7 +303,7 @@
monthlink = '<a href="%s">%s</a>' % (xml_escape(url), umonth)
self.w(u'<tr><th colspan="3">%s %s (%s)</th></tr>' \
% (_('week'), monday.isocalendar()[1], monthlink))
- for day in date_range(monday, sunday):
+ for day in date_range(monday, sunday+ONEDAY):
self.w(u'<tr>')
self.w(u'<td>%s</td>' % _(WEEKDAYS[day.weekday()]))
self.w(u'<td>%s</td>' % (day.strftime('%Y-%m-%d')))
@@ -478,7 +493,7 @@
w(u'<tr>%s</tr>' % (
WEEK_TITLE % (_('week'), monday.isocalendar()[1], monthlink)))
w(u'<tr><th>%s</th><th> </th></tr>'% _(u'Date'))
- for day in date_range(monday, sunday):
+ for day in date_range(monday, sunday+ONEDAY):
events = schedule.get(day)
style = day.weekday() % 2 and "even" or "odd"
w(u'<tr class="%s">' % style)
--- a/web/views/schema.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/schema.py Wed Jun 02 16:15:05 2010 +0200
@@ -35,7 +35,7 @@
from cubicweb import tags, uilib
from cubicweb.web import action, facet, uicfg, schemaviewer
from cubicweb.web.views import TmpFileViewMixin
-from cubicweb.web.views import primary, baseviews, tabs, tableview, iprogress
+from cubicweb.web.views import primary, baseviews, tabs, tableview, ibreadcrumbs
ALWAYS_SKIP_TYPES = BASE_TYPES | SCHEMA_TYPES
SKIP_TYPES = (ALWAYS_SKIP_TYPES | META_RTYPES | SYSTEM_RTYPES | WORKFLOW_TYPES
@@ -248,7 +248,7 @@
eschema.type, self._cw.build_url('cwetype/%s' % eschema.type),
eschema.type, _(eschema.type)))
self.w(u'<a href="%s#schema_security"><img src="%s" alt="%s"/></a>' % (
- url, self._cw.external_resource('UP_ICON'), _('up')))
+ url, self._cw.uiprops['UP_ICON'], _('up')))
self.w(u'</h3>')
self.w(u'<div style="margin: 0px 1.5em">')
self.permissions_table(eschema)
@@ -277,7 +277,7 @@
rschema.type, self._cw.build_url('cwrtype/%s' % rschema.type),
rschema.type, _(rschema.type)))
self.w(u'<a href="%s#schema_security"><img src="%s" alt="%s"/></a>' % (
- url, self._cw.external_resource('UP_ICON'), _('up')))
+ url, self._cw.uiprops['UP_ICON'], _('up')))
self.w(u'</h3>')
self.grouped_permissions_table(rschema)
@@ -680,6 +680,37 @@
visitor = OneHopRSchemaVisitor(self._cw, rschema)
s2d.schema2dot(outputfile=tmpfile, visitor=visitor)
+# breadcrumbs ##################################################################
+
+class CWRelationIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+ __select__ = implements('CWRelation')
+ def parent_entity(self):
+ return self.entity.rtype
+
+class CWAttributeIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+ __select__ = implements('CWAttribute')
+ def parent_entity(self):
+ return self.entity.stype
+
+class CWConstraintIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+ __select__ = implements('CWConstraint')
+ def parent_entity(self):
+ if self.entity.reverse_constrained_by:
+ return self.entity.reverse_constrained_by[0]
+
+class RQLExpressionIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+ __select__ = implements('RQLExpression')
+ def parent_entity(self):
+ return self.entity.expression_of
+
+class CWPermissionIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+ __select__ = implements('CWPermission')
+ def parent_entity(self):
+ # XXX useless with permission propagation
+ permissionof = getattr(self.entity, 'reverse_require_permission', ())
+ if len(permissionof) == 1:
+ return permissionof[0]
+
# misc: facets, actions ########################################################
--- a/web/views/startup.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/startup.py Wed Jun 02 16:15:05 2010 +0200
@@ -42,7 +42,7 @@
def call(self, **kwargs):
"""The default view representing the instance's management"""
self._cw.add_css('cubicweb.manageview.css')
- self.w(u'<div>\n')
+ self.w(u'<h1>%s</h1>' % self._cw.property_value('ui.site-title'))
if not self.display_folders():
self._main_index()
else:
@@ -53,7 +53,6 @@
self.folders()
self.w(u'</td>')
self.w(u'</tr></table>\n')
- self.w(u'</div>\n')
def _main_index(self):
req = self._cw
@@ -79,7 +78,7 @@
self.w(u'<br/><a href="%s">%s</a>\n' % (xml_escape(href), label))
def folders(self):
- self.w(u'<h4>%s</h4>\n' % self._cw._('Browse by category'))
+ self.w(u'<h2>%s</h2>\n' % self._cw._('Browse by category'))
self._cw.vreg['views'].select('tree', self._cw).render(w=self.w)
def create_links(self):
@@ -93,20 +92,24 @@
self.w(u'</ul>')
def startup_views(self):
- self.w(u'<h4>%s</h4>\n' % self._cw._('Startup views'))
+ self.w(u'<h2>%s</h2>\n' % self._cw._('Startup views'))
self.startupviews_table()
def startupviews_table(self):
views = self._cw.vreg['views'].possible_views(self._cw, None)
+ if not views:
+ return
+ self.w(u'<ul>')
for v in sorted(views, key=lambda x: self._cw._(x.title)):
if v.category != 'startupview' or v.__regid__ in ('index', 'tree', 'manage'):
continue
- self.w('<p><a href="%s">%s</a></p>' % (
+ self.w('<li><a href="%s">%s</a></li>' % (
xml_escape(v.url()), xml_escape(self._cw._(v.title).capitalize())))
+ self.w(u'</ul>')
def entities(self):
schema = self._cw.vreg.schema
- self.w(u'<h4>%s</h4>\n' % self._cw._('The repository holds the following entities'))
+ self.w(u'<h2>%s</h2>\n' % self._cw._('Browse by entity type'))
manager = self._cw.user.matching_groups('managers')
self.w(u'<table class="startup">')
if manager:
--- a/web/views/tableview.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/tableview.py Wed Jun 02 16:15:05 2010 +0200
@@ -215,7 +215,7 @@
def render_actions(self, divid, actions):
box = MenuWidget('', 'tableActionsBox', _class='', islist=False)
- label = tags.img(src=self._cw.external_resource('PUCE_DOWN'),
+ label = tags.img(src=self._cw.uiprops['PUCE_DOWN'],
alt=xml_escape(self._cw._('action(s) on this selection')))
menu = PopupBoxMenu(label, isitem=False, link_class='actionsBox',
ident='%sActions' % divid)
@@ -387,9 +387,9 @@
self._cw.add_css(self.css_files)
_ = self._cw._
self.columns = columns or self.columns
- ecls = self._cw.vreg['etypes'].etype_class(self.cw_rset.description[0][0])
+ sample = self.cw_rset.get_entity(0, 0)
self.w(u'<table class="%s">' % self.table_css)
- self.table_header(ecls)
+ self.table_header(sample)
self.w(u'<tbody>')
for row in xrange(self.cw_rset.rowcount):
self.cell_call(row=row, col=0)
@@ -414,16 +414,15 @@
self.w(line % infos)
self.w(u'</tr>\n')
- def table_header(self, ecls):
+ def table_header(self, sample):
"""builds the table's header"""
self.w(u'<thead><tr>')
- _ = self._cw._
for column in self.columns:
meth = getattr(self, 'header_for_%s' % column, None)
if meth:
- colname = meth(ecls)
+ colname = meth(sample)
else:
- colname = _(column)
+ colname = self._cw._(column)
self.w(u'<th>%s</th>' % xml_escape(colname))
self.w(u'</tr></thead>\n')
--- a/web/views/timeline.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/timeline.py Wed Jun 02 16:15:05 2010 +0200
@@ -18,14 +18,13 @@
"""basic support for SIMILE's timline widgets
cf. http://code.google.com/p/simile-widgets/
+"""
-"""
__docformat__ = "restructuredtext en"
from logilab.mtconverter import xml_escape
-from cubicweb.interfaces import ICalendarable
-from cubicweb.selectors import implements
+from cubicweb.selectors import adaptable
from cubicweb.view import EntityView, StartupView
from cubicweb.web import json
@@ -37,11 +36,12 @@
should be properties of entity classes or subviews)
"""
__regid__ = 'timeline-json'
+ __select__ = adaptable('ICalendarable')
+
binary = True
templatable = False
content_type = 'application/json'
- __select__ = implements(ICalendarable)
date_fmt = '%Y/%m/%d'
def call(self):
@@ -74,8 +74,9 @@
'link': 'http://www.allposters.com/-sp/Portrait-of-Horace-Brodsky-Posters_i1584413_.htm'
}
"""
- start = entity.start
- stop = entity.stop
+ icalendarable = entity.cw_adapt_to('ICalendarable')
+ start = icalendarable.start
+ stop = icalendarable.stop
start = start or stop
if start is None and stop is None:
return None
@@ -116,7 +117,7 @@
"""builds a cubicweb timeline widget node"""
__regid__ = 'timeline'
title = _('timeline')
- __select__ = implements(ICalendarable)
+ __select__ = adaptable('ICalendarable')
paginable = False
def call(self, tlunit=None):
self._cw.html_headers.define_var('Timeline_urlPrefix', self._cw.datadir_url)
--- a/web/views/timetable.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/timetable.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,16 +15,16 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""html calendar views
+"""html timetable views"""
-"""
+__docformat__ = "restructuredtext en"
+_ = unicode
from logilab.mtconverter import xml_escape
-from logilab.common.date import date_range, todatetime
+from logilab.common.date import ONEDAY, date_range, todatetime
-from cubicweb.interfaces import ITimetableViews
-from cubicweb.selectors import implements
-from cubicweb.view import AnyRsetView
+from cubicweb.selectors import adaptable
+from cubicweb.view import EntityView
class _TaskEntry(object):
@@ -37,10 +37,10 @@
MIN_COLS = 3 # minimum number of task columns for a single user
ALL_USERS = object()
-class TimeTableView(AnyRsetView):
+class TimeTableView(EntityView):
__regid__ = 'timetable'
title = _('timetable')
- __select__ = implements(ITimetableViews)
+ __select__ = adaptable('ICalendarable')
paginable = False
def call(self, title=None):
@@ -53,20 +53,22 @@
# XXX: try refactoring with calendar.py:OneMonthCal
for row in xrange(self.cw_rset.rowcount):
task = self.cw_rset.get_entity(row, 0)
+ icalendarable = task.cw_adapt_to('ICalendarable')
if len(self.cw_rset[row]) > 1:
user = self.cw_rset.get_entity(row, 1)
else:
user = ALL_USERS
the_dates = []
- if task.start and task.stop:
- if task.start.toordinal() == task.stop.toordinal():
- the_dates.append(task.start)
+ if icalendarable.start and icalendarable.stop:
+ if icalendarable.start.toordinal() == icalendarable.stop.toordinal():
+ the_dates.append(icalendarable.start)
else:
- the_dates += date_range( task.start, task.stop )
- elif task.start:
- the_dates.append(task.start)
- elif task.stop:
- the_dates.append(task.stop)
+ the_dates += date_range(icalendarable.start,
+ icalendarable.stop + ONEDAY)
+ elif icalendarable.start:
+ the_dates.append(icalendarable.start)
+ elif icalendarable.stop:
+ the_dates.append(icalendarable.stop)
for d in the_dates:
d = todatetime(d)
d_users = dates.setdefault(d, {})
@@ -91,7 +93,7 @@
visited_tasks = {} # holds a description of a task for a user
task_colors = {} # remember a color assigned to a task
- for date in date_range(date_min, date_max):
+ for date in date_range(date_min, date_max + ONEDAY):
columns = [date]
d_users = dates.get(date, {})
for user in users:
--- a/web/views/treeview.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/treeview.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,22 +15,238 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""Set of tree-building widgets, based on jQuery treeview plugin
-
+"""Set of tree views / tree-building widgets, some based on jQuery treeview
+plugin.
"""
__docformat__ = "restructuredtext en"
+from warnings import warn
+
from logilab.mtconverter import xml_escape
+from logilab.common.decorators import cached
+
from cubicweb.utils import make_uid
+from cubicweb.selectors import implements, adaptable
+from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat
+from cubicweb.mixins import _done_init
+from cubicweb.web import json
from cubicweb.interfaces import ITree
-from cubicweb.selectors import implements
-from cubicweb.view import EntityView
-from cubicweb.web import json
+from cubicweb.web.views import baseviews
def treecookiename(treeid):
return str('%s-treestate' % treeid)
+
+class ITreeAdapter(EntityAdapter):
+ """This adapter has to be overriden to be configured using the
+ tree_relation, child_role and parent_role class attributes to
+ benefit from this default implementation
+ """
+ __regid__ = 'ITree'
+ __select__ = implements(ITree) # XXX for bw compat, else should be abstract
+
+ tree_relation = None
+ child_role = 'subject'
+ parent_role = 'object'
+
+ @implements_adapter_compat('ITree')
+ def children_rql(self):
+ """returns RQL to get children
+
+ XXX should be removed from the public interface
+ """
+ return self.entity.cw_related_rql(self.tree_relation, self.parent_role)
+
+ @implements_adapter_compat('ITree')
+ def different_type_children(self, entities=True):
+ """return children entities of different type as this entity.
+
+ according to the `entities` parameter, return entity objects or the
+ equivalent result set
+ """
+ res = self.entity.related(self.tree_relation, self.parent_role,
+ entities=entities)
+ eschema = self.entity.e_schema
+ if entities:
+ return [e for e in res if e.e_schema != eschema]
+ return res.filtered_rset(lambda x: x.e_schema != eschema, self.entity.cw_col)
+
+ @implements_adapter_compat('ITree')
+ def same_type_children(self, entities=True):
+ """return children entities of the same type as this entity.
+
+ according to the `entities` parameter, return entity objects or the
+ equivalent result set
+ """
+ res = self.entity.related(self.tree_relation, self.parent_role,
+ entities=entities)
+ eschema = self.entity.e_schema
+ if entities:
+ return [e for e in res if e.e_schema == eschema]
+ return res.filtered_rset(lambda x: x.e_schema is eschema, self.entity.cw_col)
+
+ @implements_adapter_compat('ITree')
+ def is_leaf(self):
+ """returns true if this node as no child"""
+ return len(self.children()) == 0
+
+ @implements_adapter_compat('ITree')
+ def is_root(self):
+ """returns true if this node has no parent"""
+ return self.parent() is None
+
+ @implements_adapter_compat('ITree')
+ def root(self):
+ """return the root object"""
+ return self._cw.entity_from_eid(self.path()[0])
+
+ @implements_adapter_compat('ITree')
+ def parent(self):
+ """return the parent entity if any, else None (e.g. if we are on the
+ root)
+ """
+ try:
+ return self.entity.related(self.tree_relation, self.child_role,
+ entities=True)[0]
+ except (KeyError, IndexError):
+ return None
+
+ @implements_adapter_compat('ITree')
+ def children(self, entities=True, sametype=False):
+ """return children entities
+
+ according to the `entities` parameter, return entity objects or the
+ equivalent result set
+ """
+ if sametype:
+ return self.same_type_children(entities)
+ else:
+ return self.entity.related(self.tree_relation, self.parent_role,
+ entities=entities)
+
+ @implements_adapter_compat('ITree')
+ def iterparents(self, strict=True):
+ def _uptoroot(self):
+ curr = self
+ while True:
+ curr = curr.parent()
+ if curr is None:
+ break
+ yield curr
+ curr = curr.cw_adapt_to('ITree')
+ if not strict:
+ return chain([self.entity], _uptoroot(self))
+ return _uptoroot(self)
+
+ @implements_adapter_compat('ITree')
+ def iterchildren(self, _done=None):
+ """iterates over the item's children"""
+ if _done is None:
+ _done = set()
+ for child in self.children():
+ if child.eid in _done:
+ self.error('loop in %s tree', child.__regid__.lower())
+ continue
+ yield child
+ _done.add(child.eid)
+
+ @implements_adapter_compat('ITree')
+ def prefixiter(self, _done=None):
+ if _done is None:
+ _done = set()
+ if self.entity.eid in _done:
+ return
+ _done.add(self.entity.eid)
+ yield self.entity
+ for child in self.same_type_children():
+ for entity in child.cw_adapt_to('ITree').prefixiter(_done):
+ yield entity
+
+ @cached
+ @implements_adapter_compat('ITree')
+ def path(self):
+ """returns the list of eids from the root object to this object"""
+ path = []
+ adapter = self
+ entity = adapter.entity
+ while entity is not None:
+ if entity.eid in path:
+ self.error('loop in %s tree', entity.__regid__.lower())
+ break
+ path.append(entity.eid)
+ try:
+ # check we are not jumping to another tree
+ if (adapter.tree_relation != self.tree_relation or
+ adapter.child_role != self.child_role):
+ break
+ entity = adapter.parent()
+ adapter = entity.cw_adapt_to('ITree')
+ except AttributeError:
+ break
+ path.reverse()
+ return path
+
+
+class BaseTreeView(baseviews.ListView):
+ """base tree view"""
+ __regid__ = 'tree'
+ __select__ = adaptable('ITree')
+ item_vid = 'treeitem'
+
+ def call(self, done=None, **kwargs):
+ if done is None:
+ done = set()
+ super(BaseTreeView, self).call(done=done, **kwargs)
+
+ def cell_call(self, row, col=0, vid=None, done=None, **kwargs):
+ done, entity = _done_init(done, self, row, col)
+ if done is None:
+ # entity is actually an error message
+ self.w(u'<li class="badcontent">%s</li>' % entity)
+ return
+ self.open_item(entity)
+ entity.view(vid or self.item_vid, w=self.w, **kwargs)
+ relatedrset = entity.cw_adapt_to('ITree').children(entities=False)
+ self.wview(self.__regid__, relatedrset, 'null', done=done, **kwargs)
+ self.close_item(entity)
+
+ def open_item(self, entity):
+ self.w(u'<li class="%s">\n' % entity.__regid__.lower())
+ def close_item(self, entity):
+ self.w(u'</li>\n')
+
+
+
+class TreePathView(EntityView):
+ """a recursive path view"""
+ __regid__ = 'path'
+ __select__ = adaptable('ITree')
+ item_vid = 'oneline'
+ separator = u' > '
+
+ def call(self, **kwargs):
+ self.w(u'<div class="pathbar">')
+ super(TreePathView, self).call(**kwargs)
+ self.w(u'</div>')
+
+ def cell_call(self, row, col=0, vid=None, done=None, **kwargs):
+ done, entity = _done_init(done, self, row, col)
+ if done is None:
+ # entity is actually an error message
+ self.w(u'<span class="badcontent">%s</span>' % entity)
+ return
+ parent = entity.cw_adapt_to('ITree').parent_entity()
+ if parent:
+ parent.view(self.__regid__, w=self.w, done=done)
+ self.w(self.separator)
+ entity.view(vid or self.item_vid, w=self.w)
+
+
+# XXX rename regid to ajaxtree/foldabletree or something like that (same for
+# treeitemview)
class TreeView(EntityView):
+ """ajax tree view, click to expand folder"""
+
__regid__ = 'treeview'
itemvid = 'treeitemview'
subvid = 'oneline'
@@ -112,7 +328,7 @@
def cell_call(self, row, col):
entity = self.cw_rset.get_entity(row, col)
- if ITree.is_implemented_by(entity.__class__) and not entity.is_leaf():
+ if entity.cw_adapt_to('ITree') and not entity.is_leaf():
self.w(u'<div class="folder">%s</div>\n' % entity.view('oneline'))
else:
# XXX define specific CSS classes according to mime types
@@ -120,7 +336,7 @@
class DefaultTreeViewItemView(EntityView):
- """default treeitem view for entities which don't implement ITree"""
+ """default treeitem view for entities which don't adapt to ITree"""
__regid__ = 'treeitemview'
def cell_call(self, row, col, vid='oneline', treeid=None, **morekwargs):
@@ -131,12 +347,12 @@
class TreeViewItemView(EntityView):
- """specific treeitem view for entities which implement ITree
+ """specific treeitem view for entities which adapt to ITree
(each item should be expandable if it's not a tree leaf)
"""
__regid__ = 'treeitemview'
- __select__ = implements(ITree)
+ __select__ = adaptable('ITree')
default_branch_state_is_open = False
def open_state(self, eeid, treeid):
@@ -150,15 +366,16 @@
is_last=False, **morekwargs):
w = self.w
entity = self.cw_rset.get_entity(row, col)
+ itree = entity.cw_adapt_to('ITree')
liclasses = []
is_open = self.open_state(entity.eid, treeid)
- is_leaf = not hasattr(entity, 'is_leaf') or entity.is_leaf()
+ is_leaf = not hasattr(entity, 'is_leaf') or itree.is_leaf()
if is_leaf:
if is_last:
liclasses.append('last')
w(u'<li class="%s">' % u' '.join(liclasses))
else:
- rql = entity.children_rql() % {'x': entity.eid}
+ rql = itree.children_rql() % {'x': entity.eid}
url = xml_escape(self._cw.build_url('json', rql=rql, vid=parentvid,
pageid=self._cw.pageid,
treeid=treeid,
@@ -197,7 +414,7 @@
# the local node info
self.wview(vid, self.cw_rset, row=row, col=col, **morekwargs)
if is_open and not is_leaf: # => rql is defined
- self.wview(parentvid, entity.children(entities=False), subvid=vid,
+ self.wview(parentvid, itree.children(entities=False), subvid=vid,
treeid=treeid, initial_load=False, **morekwargs)
w(u'</li>')
--- a/web/views/workflow.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/workflow.py Wed Jun 02 16:15:05 2010 +0200
@@ -33,13 +33,13 @@
from cubicweb import Unauthorized, view
from cubicweb.selectors import (implements, has_related_entities, one_line_rset,
relation_possible, match_form_params,
- implements, score_entity)
-from cubicweb.interfaces import IWorkflowable
+ implements, score_entity, adaptable)
from cubicweb.view import EntityView
from cubicweb.schema import display_name
from cubicweb.web import uicfg, stdmsgs, action, component, form, action
from cubicweb.web import formfields as ff, formwidgets as fwdgs
-from cubicweb.web.views import TmpFileViewMixin, forms, primary, autoform
+from cubicweb.web.views import TmpFileViewMixin
+from cubicweb.web.views import forms, primary, autoform, ibreadcrumbs
from cubicweb.web.views.tabs import TabbedPrimaryView, PrimaryTab
_pvs = uicfg.primaryview_section
@@ -89,8 +89,9 @@
class ChangeStateFormView(form.FormViewMixIn, view.EntityView):
__regid__ = 'statuschange'
title = _('status change')
- __select__ = (one_line_rset() & implements(IWorkflowable)
- & match_form_params('treid'))
+ __select__ = (one_line_rset()
+ & match_form_params('treid')
+ & adaptable('IWorkflowable'))
def cell_call(self, row, col):
entity = self.cw_rset.get_entity(row, col)
@@ -99,7 +100,7 @@
self.w(u'<h4>%s %s</h4>\n' % (self._cw._(transition.name),
entity.view('oneline')))
msg = self._cw._('status will change from %(st1)s to %(st2)s') % {
- 'st1': entity.printable_state,
+ 'st1': entity.cw_adapt_to('IWorkflowable').printable_state,
'st2': self._cw._(transition.destination(entity).name)}
self.w(u'<p>%s</p>\n' % msg)
self.w(form.render())
@@ -128,7 +129,7 @@
class WFHistoryView(EntityView):
__regid__ = 'wfhistory'
__select__ = relation_possible('wf_info_for', role='object') & \
- score_entity(lambda x: x.workflow_history)
+ score_entity(lambda x: x.cw_adapt_to('IWorkflowable').workflow_history)
title = _('Workflow history')
@@ -183,22 +184,24 @@
def fill_menu(self, box, menu):
entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
- menu.label = u'%s: %s' % (self._cw._('state'), entity.printable_state)
+ menu.label = u'%s: %s' % (self._cw._('state'),
+ entity.cw_adapt_to('IWorkflowable').printable_state)
menu.append_anyway = True
super(WorkflowActions, self).fill_menu(box, menu)
def actual_actions(self):
entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
+ iworkflowable = entity.cw_adapt_to('IWorkflowable')
hastr = False
- for tr in entity.possible_transitions():
+ for tr in iworkflowable.possible_transitions():
url = entity.absolute_url(vid='statuschange', treid=tr.eid)
yield self.build_action(self._cw._(tr.name), url)
hastr = True
# don't propose to see wf if user can't pass any transition
if hastr:
- wfurl = entity.current_workflow.absolute_url()
+ wfurl = iworkflowable.current_workflow.absolute_url()
yield self.build_action(self._cw._('view workflow'), wfurl)
- if entity.workflow_history:
+ if iworkflowable.workflow_history:
wfurl = entity.absolute_url(vid='wfhistory')
yield self.build_action(self._cw._('view history'), wfurl)
@@ -346,6 +349,27 @@
'allowed_transition')
return []
+class WorkflowIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+ __select__ = implements('Workflow')
+ # XXX what if workflow of multiple types?
+ def parent_entity(self):
+ return self.entity.workflow_of and self.entity.workflow_of[0] or None
+
+class WorkflowItemIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+ __select__ = implements('BaseTransition', 'State')
+ def parent_entity(self):
+ return self.entity.workflow
+
+class TransitionItemIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+ __select__ = implements('SubWorkflowExitPoint')
+ def parent_entity(self):
+ return self.entity.reverse_subworkflow_exit[0]
+
+class TrInfoIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
+ __select__ = implements('TrInfo')
+ def parent_entity(self):
+ return self.entity.for_entity
+
# workflow images ##############################################################
--- a/web/views/xmlrss.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/views/xmlrss.py Wed Jun 02 16:15:05 2010 +0200
@@ -15,9 +15,8 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""base xml and rss views
+"""base xml and rss views"""
-"""
__docformat__ = "restructuredtext en"
_ = unicode
@@ -25,8 +24,10 @@
from logilab.mtconverter import xml_escape
-from cubicweb.selectors import non_final_entity, one_line_rset, appobject_selectable
-from cubicweb.view import EntityView, AnyRsetView, Component
+from cubicweb.selectors import (implements, non_final_entity, one_line_rset,
+ appobject_selectable, adaptable)
+from cubicweb.view import EntityView, EntityAdapter, AnyRsetView, Component
+from cubicweb.view import implements_adapter_compat
from cubicweb.uilib import simple_sgml_tag
from cubicweb.web import httpcache, box
@@ -120,6 +121,16 @@
# RSS stuff ###################################################################
+class IFeedAdapter(EntityAdapter):
+ __regid__ = 'IFeed'
+ __select__ = implements('Any')
+
+ @implements_adapter_compat('IFeed')
+ def rss_feed_url(self):
+ """return an url to the rss feed for this entity"""
+ return self.entity.absolute_url(vid='rss')
+
+
class RSSFeedURL(Component):
__regid__ = 'rss_feed_url'
__select__ = non_final_entity()
@@ -130,10 +141,11 @@
class RSSEntityFeedURL(Component):
__regid__ = 'rss_feed_url'
- __select__ = non_final_entity() & one_line_rset()
+ __select__ = one_line_rset() & adaptable('IFeed')
def feed_url(self):
- return self.cw_rset.get_entity(0, 0).rss_feed_url()
+ entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
+ return entity.cw_adapt_to('IFeed').rss_feed_url()
class RSSIconBox(box.BoxTemplate):
@@ -147,7 +159,7 @@
def call(self, **kwargs):
try:
- rss = self._cw.external_resource('RSS_LOGO')
+ rss = self._cw.uiprops['RSS_LOGO']
except KeyError:
self.error('missing RSS_LOGO external resource')
return
--- a/web/webconfig.py Wed Jun 02 16:13:28 2010 +0200
+++ b/web/webconfig.py Wed Jun 02 16:15:05 2010 +0200
@@ -23,8 +23,10 @@
import os
from os.path import join, exists, split
+from warnings import warn
from logilab.common.decorators import cached
+from logilab.common.deprecation import deprecated
from cubicweb.toolsutils import read_config
from cubicweb.cwconfig import CubicWebConfiguration, register_persistent_options, merge_options
@@ -77,6 +79,7 @@
"""
cubicweb_appobject_path = CubicWebConfiguration.cubicweb_appobject_path | set([join('web', 'views')])
cube_appobject_path = CubicWebConfiguration.cube_appobject_path | set(['views'])
+ uiprops = {'FCKEDITOR_PATH': ''}
options = merge_options(CubicWebConfiguration.options + (
('anonymous-user',
@@ -205,10 +208,18 @@
'group': 'web', 'level': 3,
}),
+ ('use-old-css',
+ {'type' : 'yn',
+ 'default': True,
+ 'help': 'use cubicweb.old.css instead of 3.9 cubicweb.css',
+ 'group': 'web', 'level': 2,
+ }),
+
+
))
def fckeditor_installed(self):
- return exists(self.ext_resources['FCKEDITOR_PATH'])
+ return exists(self.uiprops['FCKEDITOR_PATH'])
def eproperty_definitions(self):
for key, pdef in super(WebConfiguration, self).eproperty_definitions():
@@ -239,30 +250,6 @@
def vc_config(self):
return self.repository().get_versions()
- # mapping to external resources (id -> path) (`external_resources` file) ##
- ext_resources = {
- 'FAVICON': 'DATADIR/favicon.ico',
- 'LOGO': 'DATADIR/logo.png',
- 'RSS_LOGO': 'DATADIR/rss.png',
- 'HELP': 'DATADIR/help.png',
- 'CALENDAR_ICON': 'DATADIR/calendar.gif',
- 'SEARCH_GO':'DATADIR/go.png',
-
- 'FCKEDITOR_PATH': '/usr/share/fckeditor/',
-
- 'IE_STYLESHEETS': ['DATADIR/cubicweb.ie.css'],
- 'STYLESHEETS': ['DATADIR/cubicweb.css'],
- 'STYLESHEETS_PRINT': ['DATADIR/cubicweb.print.css'],
-
- 'JAVASCRIPTS': ['DATADIR/jquery.js',
- 'DATADIR/jquery.corner.js',
- 'DATADIR/jquery.json.js',
- 'DATADIR/cubicweb.compat.js',
- 'DATADIR/cubicweb.python.js',
- 'DATADIR/cubicweb.htmlhelpers.js'],
- }
-
-
def anonymous_user(self):
"""return a login and password to use for anonymous users. None
may be returned for both if anonymous connections are not allowed
@@ -276,26 +263,30 @@
user = unicode(user)
return user, passwd
- def has_resource(self, rid):
- """return true if an external resource is defined"""
- return bool(self.ext_resources.get(rid))
-
- @cached
def locate_resource(self, rid):
"""return the directory where the given resource may be found"""
return self._fs_locate(rid, 'data')
- @cached
def locate_doc_file(self, fname):
"""return the directory where the given resource may be found"""
return self._fs_locate(fname, 'wdoc')
- def _fs_locate(self, rid, rdirectory):
+ @cached
+ def _fs_path_locate(self, rid, rdirectory):
"""return the directory where the given resource may be found"""
path = [self.apphome] + self.cubes_path() + [join(self.shared_dir())]
for directory in path:
if exists(join(directory, rdirectory, rid)):
- return join(directory, rdirectory)
+ return directory
+
+ def _fs_locate(self, rid, rdirectory):
+ """return the directory where the given resource may be found"""
+ directory = self._fs_path_locate(rid, rdirectory)
+ if directory is None:
+ return None
+ if rdirectory == 'data' and rid.endswith('.css'):
+ return self.uiprops.process_resource(join(directory, rdirectory), rid)
+ return join(directory, rdirectory)
def locate_all_files(self, rid, rdirectory='wdoc'):
"""return all files corresponding to the given resource"""
@@ -309,8 +300,8 @@
"""load instance's configuration files"""
super(WebConfiguration, self).load_configuration()
# load external resources definition
- self._build_ext_resources()
self._init_base_url()
+ self._build_ui_properties()
def _init_base_url(self):
# normalize base url(s)
@@ -320,29 +311,70 @@
if not self.repairing:
self.global_set_option('base-url', baseurl)
httpsurl = self['https-url']
- if httpsurl and httpsurl[-1] != '/':
- httpsurl += '/'
- if not self.repairing:
- self.global_set_option('https-url', httpsurl)
+ 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()
- def _build_ext_resources(self):
- libresourcesfile = join(self.shared_dir(), 'data', 'external_resources')
- self.ext_resources.update(read_config(libresourcesfile))
+ def _build_ui_properties(self):
+ # self.datadir_url[:-1] to remove trailing /
+ from cubicweb.web.propertysheet import PropertySheet
+ self.uiprops = PropertySheet(
+ join(self.appdatahome, 'uicache'),
+ data=lambda x: self.datadir_url + x,
+ datadir_url=self.datadir_url[:-1])
+ self._init_uiprops(self.uiprops)
+ if self['https-url']:
+ self.https_uiprops = PropertySheet(
+ join(self.appdatahome, 'uicache'),
+ data=lambda x: self.https_datadir_url + x,
+ datadir_url=self.https_datadir_url[:-1])
+ self._init_uiprops(self.https_uiprops)
+
+ def _init_uiprops(self, uiprops):
+ libuiprops = join(self.shared_dir(), 'data', 'uiprops.py')
+ uiprops.load(libuiprops)
for path in reversed([self.apphome] + self.cubes_path()):
- resourcesfile = join(path, 'data', 'external_resources')
- if exists(resourcesfile):
- self.debug('loading %s', resourcesfile)
- self.ext_resources.update(read_config(resourcesfile))
- resourcesfile = join(self.apphome, 'external_resources')
+ self._load_ui_properties_file(uiprops, path)
+ self._load_ui_properties_file(uiprops, self.apphome)
+ # XXX pre 3.9 css compat
+ if self['use-old-css']:
+ datadir_url = uiprops.context['datadir_url']
+ if (datadir_url+'/cubicweb.css') in uiprops['STYLESHEETS']:
+ idx = uiprops['STYLESHEETS'].index(datadir_url+'/cubicweb.css')
+ uiprops['STYLESHEETS'][idx] = datadir_url+'/cubicweb.old.css'
+ if datadir_url+'/cubicweb.reset.css' in uiprops['STYLESHEETS']:
+ uiprops['STYLESHEETS'].remove(datadir_url+'/cubicweb.reset.css')
+
+ def _load_ui_properties_file(self, uiprops, path):
+ resourcesfile = join(path, 'data', 'external_resources')
if exists(resourcesfile):
- self.debug('loading %s', resourcesfile)
- self.ext_resources.update(read_config(resourcesfile))
- for resource in ('STYLESHEETS', 'STYLESHEETS_PRINT',
- 'IE_STYLESHEETS', 'JAVASCRIPTS'):
- val = self.ext_resources[resource]
- if isinstance(val, str):
- files = [w.strip() for w in val.split(',') if w.strip()]
- self.ext_resources[resource] = files
+ warn('[3.9] %s file is deprecated, use an uiprops.py file'
+ % resourcesfile, DeprecationWarning)
+ datadir_url = uiprops.context['datadir_url']
+ for rid, val in read_config(resourcesfile).iteritems():
+ if rid in ('STYLESHEETS', 'STYLESHEETS_PRINT',
+ 'IE_STYLESHEETS', 'JAVASCRIPTS'):
+ val = [w.strip().replace('DATADIR', datadir_url)
+ for w in val.split(',') if w.strip()]
+ if rid == 'IE_STYLESHEETS':
+ rid = 'STYLESHEETS_IE'
+ else:
+ val = val.strip().replace('DATADIR', datadir_url)
+ uiprops[rid] = val
+ uipropsfile = join(path, 'uiprops.py')
+ if exists(uipropsfile):
+ self.debug('loading %s', uipropsfile)
+ uiprops.load(uipropsfile)
# static files handling ###################################################
@@ -369,3 +401,8 @@
def static_file_del(self, rpath):
if self.static_file_exists(rpath):
os.remove(join(self.static_directory, rpath))
+
+ @deprecated('[3.9] use _cw.uiprops.get(rid)')
+ def has_resource(self, rid):
+ """return true if an external resource is defined"""
+ return bool(self.uiprops.get(rid))
--- a/wsgi/handler.py Wed Jun 02 16:13:28 2010 +0200
+++ b/wsgi/handler.py Wed Jun 02 16:15:05 2010 +0200
@@ -100,9 +100,8 @@
NOTE: no pyro
"""
- def __init__(self, config, debug=None, vreg=None):
- self.appli = CubicWebPublisher(config, debug=debug, vreg=vreg)
- self.debugmode = debug
+ def __init__(self, config, vreg=None):
+ self.appli = CubicWebPublisher(config, vreg=vreg)
self.config = config
self.base_url = None
# self.base_url = config['base-url'] or config.default_base_url()