# HG changeset patch # User Sylvain Thénault # Date 1275488105 -7200 # Node ID 8138d9c86ac8c44cd5dd7996691546c68d128dba # Parent 879590c52679b4f698a8db81e67aef787b03dd94# Parent 56784e46509f01ebb3d9d603fd1797d1f128a776 backport stable diff -r 56784e46509f -r 8138d9c86ac8 __pkginfo__.py diff -r 56784e46509f -r 8138d9c86ac8 appobject.py --- 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 + + 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) diff -r 56784e46509f -r 8138d9c86ac8 cwconfig.py --- 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') diff -r 56784e46509f -r 8138d9c86ac8 cwctl.py --- 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. + the identifier of the instance to connect. """ name = 'shell' - arguments = ' [batch command file]' + arguments = ' [batch command file(s)] [-- * - *================================================== + * */ 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"] = [ diff -r 56784e46509f -r 8138d9c86ac8 web/data/cubicweb.widgets.js --- 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'); diff -r 56784e46509f -r 8138d9c86ac8 web/data/external_resources --- 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 diff -r 56784e46509f -r 8138d9c86ac8 web/data/logo.png Binary file web/data/logo.png has changed diff -r 56784e46509f -r 8138d9c86ac8 web/data/mail.gif Binary file web/data/mail.gif has changed diff -r 56784e46509f -r 8138d9c86ac8 web/data/nomail.gif Binary file web/data/nomail.gif has changed diff -r 56784e46509f -r 8138d9c86ac8 web/data/rhythm15.png Binary file web/data/rhythm15.png has changed diff -r 56784e46509f -r 8138d9c86ac8 web/data/rhythm18.png Binary file web/data/rhythm18.png has changed diff -r 56784e46509f -r 8138d9c86ac8 web/data/rhythm20.png Binary file web/data/rhythm20.png has changed diff -r 56784e46509f -r 8138d9c86ac8 web/data/rhythm22.png Binary file web/data/rhythm22.png has changed diff -r 56784e46509f -r 8138d9c86ac8 web/data/rhythm24.png Binary file web/data/rhythm24.png has changed diff -r 56784e46509f -r 8138d9c86ac8 web/data/rhythm26.png Binary file web/data/rhythm26.png has changed diff -r 56784e46509f -r 8138d9c86ac8 web/data/uiprops.py --- /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' diff -r 56784e46509f -r 8138d9c86ac8 web/formfields.py --- 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): diff -r 56784e46509f -r 8138d9c86ac8 web/formwidgets.py --- 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""" """ % (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''' -
- -  
-''' - # 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 ''\ '%(label)s%(label)s' % { 'label': label, 'imgsrc': imgsrc, diff -r 56784e46509f -r 8138d9c86ac8 web/propertysheet.py --- /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 . +"""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) diff -r 56784e46509f -r 8138d9c86ac8 web/request.py --- 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'
') return u'
' + @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) diff -r 56784e46509f -r 8138d9c86ac8 web/test/data/pouet.css --- /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 diff -r 56784e46509f -r 8138d9c86ac8 web/test/data/sheet1.py --- /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 diff -r 56784e46509f -r 8138d9c86ac8 web/test/data/sheet2.py --- /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] diff -r 56784e46509f -r 8138d9c86ac8 web/test/unittest_breadcrumbs.py --- 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 . + from cubicweb.devtools.testlib import CubicWebTC + class BreadCrumbsTC(CubicWebTC): def test_base(self): diff -r 56784e46509f -r 8138d9c86ac8 web/test/unittest_propertysheet.py --- /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() diff -r 56784e46509f -r 8138d9c86ac8 web/test/unittest_views_basecontrollers.py --- 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'])) diff -r 56784e46509f -r 8138d9c86ac8 web/test/unittest_viewselector.py --- 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')) diff -r 56784e46509f -r 8138d9c86ac8 web/test/unittest_webconfig.py --- 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 diff -r 56784e46509f -r 8138d9c86ac8 web/views/actions.py --- 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 . -"""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 ########################################### diff -r 56784e46509f -r 8138d9c86ac8 web/views/autoform.py --- 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, diff -r 56784e46509f -r 8138d9c86ac8 web/views/basecomponents.py --- 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'' - % (self._cw.base_url(), self._cw.external_resource('LOGO'))) + self.w(u'' + % (self._cw.base_url(), self._cw.uiprops['LOGO'])) class ApplHelp(component.Component): diff -r 56784e46509f -r 8138d9c86ac8 web/views/basecontrollers.py --- 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 """""" % (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'
') @@ -359,12 +356,12 @@ if divid == 'pageContent': stream.write(u'
') view.render(**kwargs) - extresources = req.html_headers.getvalue(skiphead=True) + extresources = self._cw.html_headers.getvalue(skiphead=True) if extresources: stream.write(u'
\n') # XXX use a widget ? stream.write(extresources) stream.write(u'
\n') - if req.form.get('paginate') and divid == 'pageContent': + if paginate and divid == 'pageContent': stream.write(u'
') 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'
\n') - stream.write(extresources) - stream.write(u'
\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'
\n') - stream.write(extresources) - stream.write(u'
\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') diff -r 56784e46509f -r 8138d9c86ac8 web/views/basetemplates.py --- 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'
\n') self.nav_column(view, 'left') - w(u'\n') @@ -254,7 +254,7 @@ w(u'\n') w(u'
') w(u'
\n') + w(u'\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'
\n') - w(u'""" % cssclass) line = u''.join(u'' % 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
' u'' % (xml_escape(short_title), cid)) diff -r 56784e46509f -r 8138d9c86ac8 web/views/isioc.py --- 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 . -"""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'\n' - % (sioct, xml_escape(entity.absolute_url()))) + % (isioct, xml_escape(entity.absolute_url()))) self.w(u'%s' % xml_escape(entity.dc_title())) self.w(u'%s' - % entity.creation_date) + % entity.creation_date) # XXX format self.w(u'%s' - % entity.modification_date) + % entity.modification_date) # XXX format self.w(u'')#entity.isioc_items() - self.w(u'\n' % sioct) + self.w(u'\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'\n' - % (sioct, xml_escape(entity.absolute_url()))) + % (isioct, xml_escape(entity.absolute_url()))) self.w(u'%s' % xml_escape(entity.dc_title())) self.w(u'%s' - % entity.creation_date) + % entity.creation_date) # XXX format self.w(u'%s' - % entity.modification_date) - if entity.content: - self.w(u'%s''' - % xml_escape(entity.isioc_content())) - if entity.related('entry_of'): + % entity.modification_date) # XXX format + content = isioc.isioc_content() + if content: + self.w(u'%s' % xml_escape(content)) + container = isioc.isioc_container() + if container: self.w(u'\n' - % xml_escape(entity.isioc_container().absolute_url())) + % xml_escape(container.absolute_url())) if entity.creator: self.w(u'\n') self.w(u'\n' @@ -103,5 +156,5 @@ self.w(u'\n') self.w(u'')#entity.isioc_topics() self.w(u'')#entity.isioc_replies() - self.w(u' \n' % sioct) + self.w(u' \n' % isioct) diff -r 56784e46509f -r 8138d9c86ac8 web/views/management.py --- 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"Package %s version: %s
\n" % (cube, cubeversion)) + w(u"Cube %s version: %s
\n" % (cube, cubeversion)) cversions.append((cube, cubeversion)) w(u"") # 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 diff -r 56784e46509f -r 8138d9c86ac8 web/views/massmailing.py --- 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 . -"""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) diff -r 56784e46509f -r 8138d9c86ac8 web/views/navigation.py --- 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'') +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'
') diff -r 56784e46509f -r 8138d9c86ac8 web/views/old_calendar.py --- 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 . -"""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'
' infos += self._cw.view(itemvid, self.cw_rset, row=row) infos += u'
' - 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 = '%s' % (xml_escape(url), umonth) self.w(u'
' \ % (_('week'), monday.isocalendar()[1], monthlink)) - for day in date_range(monday, sunday): + for day in date_range(monday, sunday+ONEDAY): self.w(u'') self.w(u'' % _(WEEKDAYS[day.weekday()])) self.w(u'' % (day.strftime('%Y-%m-%d'))) @@ -478,7 +493,7 @@ w(u'%s' % ( WEEK_TITLE % (_('week'), monday.isocalendar()[1], monthlink))) w(u''% _(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'' % style) diff -r 56784e46509f -r 8138d9c86ac8 web/views/schema.py --- 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'%s' % ( - url, self._cw.external_resource('UP_ICON'), _('up'))) + url, self._cw.uiprops['UP_ICON'], _('up'))) self.w(u'') self.w(u'
') 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'%s' % ( - url, self._cw.external_resource('UP_ICON'), _('up'))) + url, self._cw.uiprops['UP_ICON'], _('up'))) self.w(u'') 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 ######################################################## diff -r 56784e46509f -r 8138d9c86ac8 web/views/startup.py --- 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'
\n') + self.w(u'

%s

' % 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'') self.w(u'
%%(%s)s
%s %s (%s)
%s%s
%s 
\n') - self.w(u'
\n') def _main_index(self): req = self._cw @@ -79,7 +78,7 @@ self.w(u'
%s\n' % (xml_escape(href), label)) def folders(self): - self.w(u'

%s

\n' % self._cw._('Browse by category')) + self.w(u'

%s

\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'') def startup_views(self): - self.w(u'

%s

\n' % self._cw._('Startup views')) + self.w(u'

%s

\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'
    ') 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('

    %s

    ' % ( + self.w('
  • %s
  • ' % ( xml_escape(v.url()), xml_escape(self._cw._(v.title).capitalize()))) + self.w(u'
') def entities(self): schema = self._cw.vreg.schema - self.w(u'

%s

\n' % self._cw._('The repository holds the following entities')) + self.w(u'

%s

\n' % self._cw._('Browse by entity type')) manager = self._cw.user.matching_groups('managers') self.w(u'') if manager: diff -r 56784e46509f -r 8138d9c86ac8 web/views/tableview.py --- 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'
' % self.table_css) - self.table_header(ecls) + self.table_header(sample) self.w(u'') 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'\n') - def table_header(self, ecls): + def table_header(self, sample): """builds the table's header""" self.w(u'') - _ = 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'' % xml_escape(colname)) self.w(u'\n') diff -r 56784e46509f -r 8138d9c86ac8 web/views/timeline.py --- 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) diff -r 56784e46509f -r 8138d9c86ac8 web/views/timetable.py --- 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 . -"""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: diff -r 56784e46509f -r 8138d9c86ac8 web/views/treeview.py --- 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 . -"""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'
  • %s
  • ' % 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'
  • \n' % entity.__regid__.lower()) + def close_item(self, entity): + self.w(u'
  • \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'
    ') + super(TreePathView, self).call(**kwargs) + self.w(u'
    ') + + 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'%s' % 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'
    %s
    \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'
  • ' % 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'
  • ') diff -r 56784e46509f -r 8138d9c86ac8 web/views/workflow.py --- 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'

    %s %s

    \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'

    %s

    \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 ############################################################## diff -r 56784e46509f -r 8138d9c86ac8 web/views/xmlrss.py --- 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 . -"""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 diff -r 56784e46509f -r 8138d9c86ac8 web/webconfig.py --- 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)) diff -r 56784e46509f -r 8138d9c86ac8 wsgi/handler.py --- 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()
    %s