view.py 19.6 KB
Newer Older
1
# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
2
3
4
5
6
7
8
9
10
# 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.
#
11
# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
12
13
14
15
16
17
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
Sylvain Thénault's avatar
cleanup    
Sylvain Thénault committed
18
"""abstract views and templates classes for CubicWeb web client"""
Adrien Di Mascio's avatar
Adrien Di Mascio committed
19

Sylvain Thénault's avatar
Sylvain Thénault committed
20

21
from cubicweb import _
Adrien Di Mascio's avatar
Adrien Di Mascio committed
22

23
import re
24
from io import BytesIO
25
from warnings import warn
26
from functools import partial
27
from inspect import getframeinfo, stack
Adrien Di Mascio's avatar
Adrien Di Mascio committed
28

29
from logilab.common.registry import yes
Sylvain Thénault's avatar
Sylvain Thénault committed
30
from logilab.mtconverter import xml_escape
31

Sylvain Thénault's avatar
Sylvain Thénault committed
32
from rql import nodes
Adrien Di Mascio's avatar
Adrien Di Mascio committed
33

34
from cubicweb import NotAnEntity
35
from cubicweb.predicates import non_final_entity, nonempty_rset, none_rset
36
from cubicweb.appobject import AppObject
37
from cubicweb.utils import UStringIO, HTMLStream
38
from cubicweb.uilib import domid, js
39
from cubicweb.schema import display_name
40

Adrien Di Mascio's avatar
Adrien Di Mascio committed
41
42
43
44
# robots control
NOINDEX = u'<meta name="ROBOTS" content="NOINDEX" />'
NOFOLLOW = u'<meta name="ROBOTS" content="NOFOLLOW" />'

45
TRANSITIONAL_DOCTYPE_NOEXT = u'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n'
46
47
TRANSITIONAL_DOCTYPE = TRANSITIONAL_DOCTYPE_NOEXT # bw compat

48
STRICT_DOCTYPE_NOEXT = u'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'
49
STRICT_DOCTYPE = STRICT_DOCTYPE_NOEXT # bw compat
Adrien Di Mascio's avatar
Adrien Di Mascio committed
50

51
52
53
54
55

def inject_html_generating_call_on_w():
    View.debug_html_rendering = True


56
57
# base view object ############################################################

58
class View(AppObject):
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
    """This class is an abstraction of a view class, used as a base class for
    every renderable object such as views, templates and other user interface
    components.

    A `View` is instantiated to render a result set or part of a result
    set. `View` subclasses may be parametrized using the following class
    attributes:

    :py:attr:`templatable` indicates if the view may be embedded in a main
      template or if it has to be rendered standalone (i.e. pure XML views must
      not be embedded in the main template of HTML pages)
    :py:attr:`content_type` if the view is not templatable, it should set the
      `content_type` class attribute to the correct MIME type (text/xhtml being
      the default)
    :py:attr:`category` this attribute may be used in the interface to regroup
      related objects (view kinds) together

    :py:attr:`paginable`

    :py:attr:`binary`


    A view writes to its output stream thanks to its attribute `w` (the
    append method of an `UStreamIO`, except for binary views).
Adrien Di Mascio's avatar
Adrien Di Mascio committed
83

Sylvain Thénault's avatar
Sylvain Thénault committed
84
85
86
    At instantiation time, the standard `_cw`, and `cw_rset` attributes are
    added and the `w` attribute will be set at rendering time to a write
    function to use.
Adrien Di Mascio's avatar
Adrien Di Mascio committed
87
88
    """
    __registry__ = 'views'
89

Adrien Di Mascio's avatar
Adrien Di Mascio committed
90
91
92
93
94
    templatable = True
    # content_type = 'application/xhtml+xml' # text/xhtml'
    binary = False
    add_to_breadcrumbs = True
    category = 'view'
95
    paginable = True
96
    debug_html_rendering = False
97

98
    def __init__(self, req=None, rset=None, **kwargs):
99
        super(View, self).__init__(req, rset=rset, **kwargs)
100
        self._w = None
Adrien Di Mascio's avatar
Adrien Di Mascio committed
101
102
103

    @property
    def content_type(self):
104
        return self._cw.html_content_type()
105

106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
    def w(self, text):
        if self._w is None:
            raise Exception('Error: call to %s.w before it has been set' % self)

        if self.debug_html_rendering:
            caller = getframeinfo(stack()[1][0])

            if isinstance(text, str) and re.match(r"^ *<[a-zA-Z0-9]+( .*>|>)", text):
                to_add = 'cubicweb-generated-by="%s.%s" cubicweb-from-source="%s:%s"' % (
                    self.__module__, self.__class__.__name__,
                    caller.filename, caller.lineno,
                )

                before_space, beginning_of_html = text.split("<", 1)

                # when it's a tag without attribues like "<b>"
                if re.match(r"^ *<[a-zA-Z0-9]+>", text):
                    tag_name, rest = beginning_of_html.split(">", 1)
                    rest = ">" + rest
                else:
                    tag_name, rest = beginning_of_html.split(" ", 1)
                    rest = " " + rest

                text = "%(before_space)s<%(tag_name)s %(to_add)s%(rest)s" % {
                    "before_space": before_space,
                    "tag_name": tag_name,
                    "to_add": to_add,
                    "rest": rest,
                }

        return self._w(text)

Adrien Di Mascio's avatar
Adrien Di Mascio committed
138
    def set_stream(self, w=None):
139
        if self._w is not None:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
140
141
142
            return
        if w is None:
            if self.binary:
143
                self._stream = stream = BytesIO()
Adrien Di Mascio's avatar
Adrien Di Mascio committed
144
145
146
147
148
            else:
                self._stream = stream = UStringIO()
            w = stream.write
        else:
            stream = None
149
        self._w = w
Adrien Di Mascio's avatar
Adrien Di Mascio committed
150
151
152
        return stream

    # main view interface #####################################################
153

154
    def render(self, w=None, **context):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
155
156
157
158
159
        """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:
160

sylvain.thenault@logilab.fr's avatar
sylvain.thenault@logilab.fr committed
161
        * if row is specified, `cell_call` is called
Adrien Di Mascio's avatar
Adrien Di Mascio committed
162
163
164
165
        * 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
        """
166
        # XXX use .cw_row/.cw_col
sylvain.thenault@logilab.fr's avatar
sylvain.thenault@logilab.fr committed
167
        row = context.get('row')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
168
169
170
171
172
173
        if row is not None:
            context.setdefault('col', 0)
            view_func = self.cell_call
        else:
            view_func = self.call
        stream = self.set_stream(w)
174
175
        try:
            view_func(**context)
176
        except Exception:
177
178
            self.debug('view call %s failed (context=%s)', view_func, context)
            raise
Adrien Di Mascio's avatar
Adrien Di Mascio committed
179
180
181
182
        # return stream content if we have created it
        if stream is not None:
            return self._stream.getvalue()

Sylvain Thénault's avatar
Sylvain Thénault committed
183
184
185
186
187
188
    def tal_render(self, template, variables):
        """render a precompiled page template with variables in the given
        dictionary as context
        """
        from cubicweb.ext.tal import CubicWebContext
        context = CubicWebContext()
189
190
        context.update({'self': self, 'rset': self.cw_rset, '_' : self._cw._,
                        'req': self._cw, 'user': self._cw.user})
Sylvain Thénault's avatar
Sylvain Thénault committed
191
192
193
194
195
        context.update(variables)
        output = UStringIO()
        template.expand(context, output)
        return output.getvalue()

Adrien Di Mascio's avatar
Adrien Di Mascio committed
196
197
198
    # should default .call() method add a <div classs="section"> around each
    # rset item
    add_div_section = True
199

Adrien Di Mascio's avatar
Adrien Di Mascio committed
200
201
202
203
204
205
206
    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
        """
207
        rset = self.cw_rset
Adrien Di Mascio's avatar
Adrien Di Mascio committed
208
        if rset is None:
209
            raise NotImplementedError("%r an rset is required" % self)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
210
        wrap = self.templatable and len(rset) > 1 and self.add_div_section
211
212
213
214
        # avoid re-selection if rset of size 1, we already have the most
        # specific view
        if rset.rowcount != 1:
            kwargs.setdefault('initargs', self.cw_extra_kwargs)
215
            for i in range(len(rset)):
216
217
218
219
220
221
                if wrap:
                    self.w(u'<div class="section">')
                self.wview(self.__regid__, rset, row=i, **kwargs)
                if wrap:
                    self.w(u"</div>")
        else:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
222
223
            if wrap:
                self.w(u'<div class="section">')
224
225
            kwargs.setdefault('col', 0)
            self.cell_call(row=0, **kwargs)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
226
227
228
229
230
            if wrap:
                self.w(u"</div>")

    def cell_call(self, row, col, **kwargs):
        """the view is called for a particular result set cell"""
231
        raise NotImplementedError(repr(self))
232

Adrien Di Mascio's avatar
Adrien Di Mascio committed
233
234
    def linkable(self):
        """return True if the view may be linked in a menu
235

Adrien Di Mascio's avatar
Adrien Di Mascio committed
236
237
238
239
240
241
242
        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):
Sandrine Ribeau's avatar
Sandrine Ribeau committed
243
        return self.cw_extra_kwargs.get('is_primary', self.__regid__ == 'primary')
244

Adrien Di Mascio's avatar
Adrien Di Mascio committed
245
246
247
248
249
    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.
        """
250
        rset = self.cw_rset
251
        if rset is None:
252
            return self._cw.build_url('view', vid=self.__regid__)
253
254
        coltypes = rset.column_types(0)
        if len(coltypes) == 1:
255
            etype = next(iter(coltypes))
Sandrine Ribeau's avatar
Sandrine Ribeau committed
256
            if not self._cw.vreg.schema.eschema(etype).final:
257
258
                if len(rset) == 1:
                    entity = rset.get_entity(0, 0)
259
                    return entity.absolute_url(vid=self.__regid__)
260
261
262
263
264
265
266
267
268
            # don't want to generate /<etype> url if there is some restriction
            # on something else than the entity type
            restr = rset.syntax_tree().children[0].where
            # XXX norestriction is not correct here. For instance, in cases like
            # "Any P,N WHERE P is Project, P name N" norestriction should equal
            # True
            norestriction = (isinstance(restr, nodes.Relation) and
                             restr.is_types_restriction())
            if norestriction:
269
270
                return self._cw.build_url(etype.lower(), vid=self.__regid__)
        return self._cw.build_url('view', rql=rset.printable_rql(), vid=self.__regid__)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
271
272
273

    def set_request_content_type(self):
        """set the content type returned by this view"""
274
        self._cw.set_content_type(self.content_type)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
275
276

    # view utilities ##########################################################
277

278
    def wview(self, __vid, rset=None, __fallback_vid=None, **kwargs):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
279
280
        """shortcut to self.view method automatically passing self.w as argument
        """
281
        self._cw.view(__vid, rset, __fallback_vid, w=self._w, **kwargs)
282

Adrien Di Mascio's avatar
Adrien Di Mascio committed
283
    def whead(self, data):
284
        self._cw.html_headers.write(data)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
285
286
287

    def wdata(self, data):
        """simple helper that escapes `data` and writes into `self.w`"""
Sylvain Thénault's avatar
Sylvain Thénault committed
288
        self.w(xml_escape(data))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
289
290
291
292
293
294
295
296

    def html_headers(self):
        """return a list of html headers (eg something to be inserted between
        <head> and </head> of the returned page

        by default return a meta tag to disable robot indexation of the page
        """
        return [NOINDEX]
297

Adrien Di Mascio's avatar
Adrien Di Mascio committed
298
299
300
301
    def page_title(self):
        """returns a title according to the result set - used for the
        title in the HTML header
        """
302
        vtitle = self._cw.form.get('vtitle')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
303
        if vtitle:
304
            return self._cw._(vtitle)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
305
306
307
308
        # 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:
309
310
            vtitle = self._cw._(vtitle)
        rset = self.cw_rset
Adrien Di Mascio's avatar
Adrien Di Mascio committed
311
312
313
        if rset and rset.rowcount:
            if rset.rowcount == 1:
                try:
314
                    entity = rset.complete_entity(0, 0)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
315
316
317
                    # use long_title to get context information if any
                    clabel = entity.dc_long_title()
                except NotAnEntity:
318
                    clabel = display_name(self._cw, rset.description[0][0])
Adrien Di Mascio's avatar
Adrien Di Mascio committed
319
320
321
322
                    clabel = u'%s (%s)' % (clabel, vtitle)
            else :
                etypes = rset.column_types(0)
                if len(etypes) == 1:
323
                    etype = next(iter(etypes))
324
                    clabel = display_name(self._cw, etype, 'plural')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
325
326
327
328
                else :
                    clabel = u'#[*] (%s)' % vtitle
        else:
            clabel = vtitle
329
        return u'%s (%s)' % (clabel, self._cw.property_value('ui.site-title'))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
330

Sylvain Thénault's avatar
Sylvain Thénault committed
331
332
    def field(self, label, value, row=True, show_label=True, w=None, tr=True,
              table=False):
333
        """read-only field"""
Adrien Di Mascio's avatar
Adrien Di Mascio committed
334
335
        if w is None:
            w = self.w
336
337
338
339
        if table:
            w(u'<tr class="entityfield">')
        else:
            w(u'<div class="entityfield">')
340
        if show_label and label:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
341
            if tr:
342
                label = display_name(self._cw, label)
343
344
345
            if table:
                w(u'<th>%s</th>' % label)
            else:
346
                w(u'<span class="label">%s</span> ' % label)
347
        if table:
348
349
350
351
            if not (show_label and label):
                w(u'<td colspan="2">%s</td></tr>' % value)
            else:
                w(u'<td>%s</td></tr>' % value)
352
353
        else:
            w(u'<span>%s</span></div>' % value)
354

Adrien Di Mascio's avatar
Adrien Di Mascio committed
355

356

357
358
359
# concrete views base classes #################################################

class EntityView(View):
sylvain.thenault@logilab.fr's avatar
cleanup    
sylvain.thenault@logilab.fr committed
360
    """base class for views applying on an entity (i.e. uniform result set)"""
361
    __select__ = non_final_entity()
362
    category = _('entityview')
363

364
365
    def call(self, **kwargs):
        if self.cw_rset is None:
366
367
368
369
370
371
372
373
374
375
376
377
378
            # * cw_extra_kwargs is the place where extra selection arguments are
            #   stored
            # * when calling req.view('somevid', entity=entity), 'entity' ends
            #   up in cw_extra_kwargs and kwargs
            #
            # handle that to avoid a TypeError with a sanity check
            #
            # Notice that could probably be avoided by handling entity_call in
            # .render
            entity = self.cw_extra_kwargs.pop('entity')
            if 'entity' in kwargs:
                assert kwargs.pop('entity') is entity
            self.entity_call(entity, **kwargs)
379
380
381
382
383
384
385
        else:
            super(EntityView, self).call(**kwargs)

    def cell_call(self, row, col, **kwargs):
        self.entity_call(self.cw_rset.get_entity(row, col), **kwargs)

    def entity_call(self, entity, **kwargs):
386
        raise NotImplementedError('%r %r' % (self.__regid__, self.__class__))
387

Sylvain Thénault's avatar
Sylvain Thénault committed
388

Adrien Di Mascio's avatar
Adrien Di Mascio committed
389
class StartupView(View):
sylvain.thenault@logilab.fr's avatar
cleanup    
sylvain.thenault@logilab.fr committed
390
    """base class for views which doesn't need a particular result set to be
Dimitri Papadopoulos's avatar
Dimitri Papadopoulos committed
391
    displayed (so they can always be displayed!)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
392
    """
393
    __select__ = none_rset()
394

395
    category = _('startupview')
396

Adrien Di Mascio's avatar
Adrien Di Mascio committed
397
398
399
400
401
402
403
404
405
406
407
408
409
    def html_headers(self):
        """return a list of html headers (eg something to be inserted between
        <head> and </head> 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)
    """
410
    __select__ = none_rset() | non_final_entity()
411

Adrien Di Mascio's avatar
Adrien Di Mascio committed
412
    default_rql = None
413

Sylvain Thénault's avatar
Sylvain Thénault committed
414
    def __init__(self, req, rset=None, **kwargs):
Sylvain Thénault's avatar
Sylvain Thénault committed
415
        super(EntityStartupView, self).__init__(req, rset=rset, **kwargs)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
416
417
418
419
420
        if rset is None:
            # this instance is not in the "entityview" category
            self.category = 'startupview'

    def startup_rql(self):
sylvain.thenault@logilab.fr's avatar
cleanup    
sylvain.thenault@logilab.fr committed
421
        """return some rql to be executed if the result set is None"""
Adrien Di Mascio's avatar
Adrien Di Mascio committed
422
        return self.default_rql
423

424
425
426
427
    def no_entities(self, **kwargs):
        """override to display something when no entities were found"""
        pass

Adrien Di Mascio's avatar
Adrien Di Mascio committed
428
    def call(self, **kwargs):
sylvain.thenault@logilab.fr's avatar
cleanup    
sylvain.thenault@logilab.fr committed
429
430
        """override call to execute rql returned by the .startup_rql method if
        necessary
Adrien Di Mascio's avatar
Adrien Di Mascio committed
431
        """
432
433
434
        rset = self.cw_rset
        if rset is None:
            rset = self.cw_rset = self._cw.execute(self.startup_rql())
435
        if rset:
436
            for i in range(len(rset)):
437
438
439
                self.wview(self.__regid__, rset, row=i, **kwargs)
        else:
            self.no_entities(**kwargs)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
440

441

Adrien Di Mascio's avatar
Adrien Di Mascio committed
442
443
class AnyRsetView(View):
    """base class for views applying on any non empty result sets"""
444
    __select__ = nonempty_rset()
445

446
    category = _('anyrsetview')
447

448
    def columns_labels(self, mainindex=0, tr=True):
449
450
451
452
453
454
455
456
457
458
459
        """compute the label of the rset colums

        The logic is based on :meth:`~rql.stmts.Union.get_description`.

        :param mainindex: The index of the main variable. This is an hint to get
                          more accurate label for various situation
        :type mainindex:  int

        :param tr: Should the label be translated ?
        :type tr: boolean
        """
Sylvain Thenault's avatar
Sylvain Thenault committed
460
        if tr:
461
            translate = partial(display_name, self._cw)
Sylvain Thenault's avatar
Sylvain Thenault committed
462
        else:
463
            translate = lambda val, *args,**kwargs: val
464
        # XXX [0] because of missing Union support
465
        rql_syntax_tree = self.cw_rset.syntax_tree()
466
        rqlstdescr = rql_syntax_tree.get_description(mainindex, translate)[0]
467
        labels = []
468
        for colidx, label in enumerate(rqlstdescr):
469
            labels.append(self.column_label(colidx, label, translate))
470
        return labels
471

472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
    def column_label(self, colidx, default, translate_func=None):
        """return the label of a specified columns index

        Overwrite me if you need to compute specific label.

        :param colidx: The index of the column the call computes a label for.
        :type colidx:  int

        :param default: Default value. If ``"Any"`` the default value will be
                        recomputed as coma separated list for all possible
                        etypes name.
        :type colidx:  string

        :param translate_func: A function used to translate name.
        :type colidx:  function
        """
        label = default
        if label == 'Any':
            etypes = self.cw_rset.column_types(colidx)
            if translate_func is not None:
                etypes = map(translate_func, etypes)
493
            label = u','.join(etypes)
494
495
496
        return label


497

Adrien Di Mascio's avatar
Adrien Di Mascio committed
498
499
# concrete template base classes ##############################################

500
class MainTemplate(View):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
501
502
503
504
505
    """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
    """

506
    doctype = '<!DOCTYPE html>'
Adrien Di Mascio's avatar
Adrien Di Mascio committed
507

508
    def set_stream(self, w=None):
509
        if self._w is not None:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
510
511
512
            return
        if w is None:
            if self.binary:
513
                self._stream = stream = BytesIO()
Adrien Di Mascio's avatar
Adrien Di Mascio committed
514
            else:
515
                self._stream = stream = HTMLStream(self._cw)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
516
517
518
            w = stream.write
        else:
            stream = None
519
        self._w = w
Adrien Di Mascio's avatar
Adrien Di Mascio committed
520
521
522
523
524
525
526
        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''
527

sylvain.thenault@logilab.fr's avatar
sylvain.thenault@logilab.fr committed
528
529
    def linkable(self):
        return False
530

531
532
533
534
# concrete component base classes #############################################

class ReloadableMixIn(object):
    """simple mixin for reloadable parts of UI"""
535

536
537
538
539
    @property
    def domid(self):
        return domid(self.__regid__)

540
541
542
543

class Component(ReloadableMixIn, View):
    """base class for components"""
    __registry__ = 'components'
544
    __select__ = yes()
545

546
    # XXX huummm, much probably useless (should be...)
547
    htmlclass = 'mainRelated'
548
549
550
    @property
    def cssclass(self):
        return '%s %s' % (self.htmlclass, domid(self.__regid__))
551

552
553
554
555
556
    # XXX should rely on ReloadableMixIn.domid
    @property
    def domid(self):
        return '%sComponent' % domid(self.__regid__)

557
558
559
560
# EntityAdapter moved to cubicweb.entity ######################################
from logilab.common.deprecation import class_moved
from cubicweb import entity
EntityAdapter = class_moved(entity.EntityAdapter) # cubicweb 3.28