diff -r 000000000000 -r b97547f5f1fa common/view.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/view.py Wed Nov 05 15:52:50 2008 +0100 @@ -0,0 +1,462 @@ +"""abstract views and templates classes for CubicWeb web client + + +:organization: Logilab +:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +""" +__docformat__ = "restructuredtext en" + +from cStringIO import StringIO + +from logilab.mtconverter import html_escape + +from cubicweb import NotAnEntity, NoSelectableObject +from cubicweb.common.registerers import accepts_registerer, priority_registerer +from cubicweb.common.selectors import (in_group_selector, anyrset_selector, + emptyrset_selector, accept_selector, + norset_selector, chainfirst) +from cubicweb.common.appobject import AppRsetObject, ComponentMixIn +from cubicweb.common.utils import UStringIO, HTMLStream + +_ = unicode + +# robots control +NOINDEX = u'' +NOFOLLOW = u'' + +CW_XHTML_EXTENSIONS = '''[ + + + ] ''' + +TRANSITIONAL_DOCTYPE = u'\n' + +STRICT_DOCTYPE = u'\n' + +class View(AppRsetObject): + """abstract view class, used as base for every renderable object such + as views, templates, some components...web + + A view is instantiated to render a [part of a] result set. View + subclasses may be parametred using the following class attributes: + + * `templatable` indicates if the view may be embeded in a main + template or if it has to be rendered standalone (i.e. XML for + instance) + * if the view is not templatable, it should set the `content_type` class + attribute to the correct MIME type (text/xhtml by default) + * the `category` attribute may be used in the interface to regroup related + objects together + + At instantiation time, the standard `req`, `rset`, and `cursor` + attributes are added and the `w` attribute will be set at rendering + time to a write function to use. + """ + __registry__ = 'views' + + templatable = True + need_navigation = True + # content_type = 'application/xhtml+xml' # text/xhtml' + binary = False + add_to_breadcrumbs = True + category = 'view' + + def __init__(self, req, rset): + super(View, self).__init__(req, rset) + self.w = None + + @property + def content_type(self): + if self.req.xhtml_browser(): + return 'application/xhtml+xml' + return 'text/html' + + def set_stream(self, w=None): + if self.w is not None: + return + if w is None: + if self.binary: + self._stream = stream = StringIO() + else: + self._stream = stream = UStringIO() + w = stream.write + else: + stream = None + self.w = w + return stream + + # main view interface ##################################################### + + def dispatch(self, w=None, **context): + """called to render a view object for a result set. + + This method is a dispatched to an actual method selected + according to optional row and col parameters, which are locating + a particular row or cell in the result set: + + * if row [and col] are specified, `cell_call` is called + * if none of them is supplied, the view is considered to apply on + the whole result set (which may be None in this case), `call` is + called + """ + row, col = context.get('row'), context.get('col') + if row is not None: + context.setdefault('col', 0) + view_func = self.cell_call + else: + view_func = self.call + stream = self.set_stream(w) + # stream = self.set_stream(context) + view_func(**context) + # return stream content if we have created it + if stream is not None: + return self._stream.getvalue() + + # should default .call() method add a
around each + # rset item + add_div_section = True + + def call(self, **kwargs): + """the view is called for an entire result set, by default loop + other rows of the result set and call the same view on the + particular row + + Views applicable on None result sets have to override this method + """ + rset = self.rset + if rset is None: + raise NotImplementedError, self + wrap = self.templatable and len(rset) > 1 and self.add_div_section + for i in xrange(len(rset)): + if wrap: + self.w(u'
') + self.wview(self.id, rset, row=i, **kwargs) + if wrap: + self.w(u"
") + + def cell_call(self, row, col, **kwargs): + """the view is called for a particular result set cell""" + raise NotImplementedError, self + + def linkable(self): + """return True if the view may be linked in a menu + + by default views without title are not meant to be displayed + """ + if not getattr(self, 'title', None): + return False + return True + + def is_primary(self): + return self.id == 'primary' + + def url(self): + """return the url associated with this view. Should not be + necessary for non linkable views, but a default implementation + is provided anyway. + """ + try: + return self.build_url(vid=self.id, rql=self.req.form['rql']) + except KeyError: + return self.build_url(vid=self.id) + + def set_request_content_type(self): + """set the content type returned by this view""" + self.req.set_content_type(self.content_type) + + # view utilities ########################################################## + + def view(self, __vid, rset, __fallback_vid=None, **kwargs): + """shortcut to self.vreg.render method avoiding to pass self.req""" + try: + view = self.vreg.select_view(__vid, self.req, rset, **kwargs) + except NoSelectableObject: + if __fallback_vid is None: + raise + view = self.vreg.select_view(__fallback_vid, self.req, rset, **kwargs) + return view.dispatch(**kwargs) + + def wview(self, __vid, rset, __fallback_vid=None, **kwargs): + """shortcut to self.view method automatically passing self.w as argument + """ + self.view(__vid, rset, __fallback_vid, w=self.w, **kwargs) + + def whead(self, data): + self.req.html_headers.write(data) + + def wdata(self, data): + """simple helper that escapes `data` and writes into `self.w`""" + self.w(html_escape(data)) + + def action(self, actionid, row=0): + """shortcut to get action object with id `actionid`""" + return self.vreg.select_action(actionid, self.req, self.rset, + row=row) + + def action_url(self, actionid, label=None, row=0): + """simple method to be able to display `actionid` as a link anywhere + """ + action = self.vreg.select_action(actionid, self.req, self.rset, + row=row) + if action: + label = label or self.req._(action.title) + return u'%s' % (html_escape(action.url()), label) + return u'' + + def html_headers(self): + """return a list of html headers (eg something to be inserted between + and of the returned page + + by default return a meta tag to disable robot indexation of the page + """ + return [NOINDEX] + + def page_title(self): + """returns a title according to the result set - used for the + title in the HTML header + """ + vtitle = self.req.form.get('vtitle') + if vtitle: + return self.req._(vtitle) + # class defined title will only be used if the resulting title doesn't + # seem clear enough + vtitle = getattr(self, 'title', None) or u'' + if vtitle: + vtitle = self.req._(vtitle) + rset = self.rset + if rset and rset.rowcount: + if rset.rowcount == 1: + try: + entity = self.complete_entity(0) + # use long_title to get context information if any + clabel = entity.dc_long_title() + except NotAnEntity: + clabel = display_name(self.req, rset.description[0][0]) + clabel = u'%s (%s)' % (clabel, vtitle) + else : + etypes = rset.column_types(0) + if len(etypes) == 1: + etype = iter(etypes).next() + clabel = display_name(self.req, etype, 'plural') + else : + clabel = u'#[*] (%s)' % vtitle + else: + clabel = vtitle + return u'%s (%s)' % (clabel, self.req.property_value('ui.site-title')) + + def output_url_builder( self, name, url, args ): + self.w(u'\n') + + def create_url(self, etype, **kwargs): + """ return the url of the entity creation form for a given entity type""" + return self.req.build_url('add/%s'%etype, **kwargs) + + +# concrete views base classes ################################################# + +class EntityView(View): + """base class for views applying on an entity (i.e. uniform result set) + """ + __registerer__ = accepts_registerer + __selectors__ = (accept_selector,) + category = 'entityview' + + def field(self, label, value, row=True, show_label=True, w=None, tr=True): + """ read-only field """ + if w is None: + w = self.w + if row: + w(u'
') + if show_label: + if tr: + label = display_name(self.req, label) + w(u'%s' % label) + w(u'
%s
' % value) + if row: + w(u'
') + + +class StartupView(View): + """base class for views which doesn't need a particular result set + to be displayed (so they can always be displayed !) + """ + __registerer__ = priority_registerer + __selectors__ = (in_group_selector, norset_selector) + require_groups = () + category = 'startupview' + + def url(self): + """return the url associated with this view. We can omit rql here""" + return self.build_url('view', vid=self.id) + + def html_headers(self): + """return a list of html headers (eg something to be inserted between + and of the returned page + + by default startup views are indexed + """ + return [] + + +class EntityStartupView(EntityView): + """base class for entity views which may also be applied to None + result set (usually a default rql is provided by the view class) + """ + __registerer__ = accepts_registerer + __selectors__ = (chainfirst(norset_selector, accept_selector),) + + default_rql = None + + def __init__(self, req, rset): + super(EntityStartupView, self).__init__(req, rset) + if rset is None: + # this instance is not in the "entityview" category + self.category = 'startupview' + + def startup_rql(self): + """return some rql to be executedif the result set is None""" + return self.default_rql + + def call(self, **kwargs): + """override call to execute rql returned by the .startup_rql + method if necessary + """ + if self.rset is None: + self.rset = self.req.execute(self.startup_rql()) + rset = self.rset + for i in xrange(len(rset)): + self.wview(self.id, rset, row=i, **kwargs) + + def url(self): + """return the url associated with this view. We can omit rql if we + are on a result set on which we do not apply. + """ + if not self.__select__(self.req, self.rset): + return self.build_url(vid=self.id) + return super(EntityStartupView, self).url() + + +class AnyRsetView(View): + """base class for views applying on any non empty result sets""" + __registerer__ = priority_registerer + __selectors__ = (anyrset_selector,) + + category = 'anyrsetview' + + +class EmptyRsetView(View): + """base class for views applying on any empty result sets""" + __registerer__ = priority_registerer + __selectors__ = (emptyrset_selector,) + + +# concrete template base classes ############################################## + +class Template(View): + """a template is almost like a view, except that by default a template + is only used globally (i.e. no result set adaptation) + """ + __registry__ = 'templates' + __registerer__ = priority_registerer + __selectors__ = (in_group_selector,) + + require_groups = () + + def template(self, oid, **kwargs): + """shortcut to self.registry.render method on the templates registry""" + w = kwargs.pop('w', self.w) + self.vreg.render('templates', oid, self.req, w=w, **kwargs) + + +class MainTemplate(Template): + """main template are primary access point to render a full HTML page. + There is usually at least a regular main template and a simple fallback + one to display error if the first one failed + """ + + base_doctype = STRICT_DOCTYPE + + @property + def doctype(self): + if self.req.xhtml_browser(): + return self.base_doctype % CW_XHTML_EXTENSIONS + return self.base_doctype % '' + + def set_stream(self, w=None, templatable=True): + if templatable and self.w is not None: + return + + if w is None: + if self.binary: + self._stream = stream = StringIO() + elif not templatable: + # not templatable means we're using a non-html view, we don't + # want the HTMLStream stuff to interfere during data generation + self._stream = stream = UStringIO() + else: + self._stream = stream = HTMLStream(self.req) + w = stream.write + else: + stream = None + self.w = w + return stream + + def write_doctype(self, xmldecl=True): + assert isinstance(self._stream, HTMLStream) + self._stream.doctype = self.doctype + if not xmldecl: + self._stream.xmldecl = u'' + +# viewable components base classes ############################################ + +class VComponent(ComponentMixIn, View): + """base class for displayable components""" + property_defs = { + 'visible': dict(type='Boolean', default=True, + help=_('display the component or not')),} + +class SingletonVComponent(VComponent): + """base class for displayable unique components""" + __registerer__ = priority_registerer