view.py 20.1 KB
Newer Older
1
# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
2
# contact https://www.logilab.fr/ -- mailto:contact@logilab.fr
3
4
5
6
7
8
9
10
#
# 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
# 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
17
# with CubicWeb.  If not, see <https://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, format_and_escape_string
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
# robots control
42
43
NOINDEX = '<meta name="ROBOTS" content="NOINDEX" />'
NOFOLLOW = '<meta name="ROBOTS" content="NOFOLLOW" />'
Adrien Di Mascio's avatar
Adrien Di Mascio committed
44

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

48
49
STRICT_DOCTYPE_NOEXT = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'
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

59
class View(AppObject):
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
    """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
84

Sylvain Thénault's avatar
Sylvain Thénault committed
85
86
87
    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
88
    """
89
90

    __registry__ = "views"
91

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

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

    @property
    def content_type(self):
106
        return self._cw.html_content_type()
107

108
    def w(self, text, *args, escape=True):
109
        if self._w is None:
110
            raise Exception("Error: call to %s.w before it has been set" % self)
111
112
113
114
115

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

            if isinstance(text, str) and re.match(r"^ *<[a-zA-Z0-9]+( .*>|>)", text):
116
117
118
119
120
121
122
123
                to_add = (
                    'cubicweb-generated-by="%s.%s" cubicweb-from-source="%s:%s"'
                    % (
                        self.__module__,
                        self.__class__.__name__,
                        caller.filename,
                        caller.lineno,
                    )
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
                )

                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,
                }

143
        text = format_and_escape_string(text, *args, escape=escape)
144

145
146
        return self._w(text)

Adrien Di Mascio's avatar
Adrien Di Mascio committed
147
    def set_stream(self, w=None):
148
        if self._w is not None:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
149
150
151
            return
        if w is None:
            if self.binary:
152
                self._stream = stream = BytesIO()
Adrien Di Mascio's avatar
Adrien Di Mascio committed
153
154
155
156
157
            else:
                self._stream = stream = UStringIO()
            w = stream.write
        else:
            stream = None
158
        self._w = w
Adrien Di Mascio's avatar
Adrien Di Mascio committed
159
160
161
        return stream

    # main view interface #####################################################
162

163
    def render(self, w=None, **context):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
164
165
166
167
168
        """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:
169

sylvain.thenault@logilab.fr's avatar
sylvain.thenault@logilab.fr committed
170
        * if row is specified, `cell_call` is called
Adrien Di Mascio's avatar
Adrien Di Mascio committed
171
172
173
174
        * 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
        """
175
        # XXX use .cw_row/.cw_col
176
        row = context.get("row")
Adrien Di Mascio's avatar
Adrien Di Mascio committed
177
        if row is not None:
178
            context.setdefault("col", 0)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
179
180
181
182
            view_func = self.cell_call
        else:
            view_func = self.call
        stream = self.set_stream(w)
183
184
        try:
            view_func(**context)
185
        except Exception:
186
            self.debug("view call %s failed (context=%s)", view_func, context)
187
            raise
Adrien Di Mascio's avatar
Adrien Di Mascio committed
188
189
190
191
        # 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
192
193
194
195
196
    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
197

Sylvain Thénault's avatar
Sylvain Thénault committed
198
        context = CubicWebContext()
199
200
201
202
203
204
205
206
207
        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
208
209
210
211
212
        context.update(variables)
        output = UStringIO()
        template.expand(context, output)
        return output.getvalue()

Adrien Di Mascio's avatar
Adrien Di Mascio committed
213
214
215
    # should default .call() method add a <div classs="section"> around each
    # rset item
    add_div_section = True
216

Adrien Di Mascio's avatar
Adrien Di Mascio committed
217
218
219
220
221
222
223
    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
        """
224
        rset = self.cw_rset
Adrien Di Mascio's avatar
Adrien Di Mascio committed
225
        if rset is None:
226
            raise NotImplementedError("%r an rset is required" % self)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
227
        wrap = self.templatable and len(rset) > 1 and self.add_div_section
228
229
230
        # avoid re-selection if rset of size 1, we already have the most
        # specific view
        if rset.rowcount != 1:
231
            kwargs.setdefault("initargs", self.cw_extra_kwargs)
232
            for i in range(len(rset)):
233
                if wrap:
234
                    self.w('<div class="section">')
235
236
                self.wview(self.__regid__, rset, row=i, **kwargs)
                if wrap:
237
                    self.w("</div>")
238
        else:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
239
            if wrap:
240
241
                self.w('<div class="section">')
            kwargs.setdefault("col", 0)
242
            self.cell_call(row=0, **kwargs)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
243
            if wrap:
244
                self.w("</div>")
Adrien Di Mascio's avatar
Adrien Di Mascio committed
245
246
247

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

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

Adrien Di Mascio's avatar
Adrien Di Mascio committed
253
254
        by default views without title are not meant to be displayed
        """
255
        if not getattr(self, "title", None):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
256
257
258
259
            return False
        return True

    def is_primary(self):
260
        return self.cw_extra_kwargs.get("is_primary", self.__regid__ == "primary")
261

Adrien Di Mascio's avatar
Adrien Di Mascio committed
262
263
264
265
266
    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.
        """
267
        rset = self.cw_rset
268
        if rset is None:
269
            return self._cw.build_url("view", vid=self.__regid__)
270
271
        coltypes = rset.column_types(0)
        if len(coltypes) == 1:
272
            etype = next(iter(coltypes))
Sandrine Ribeau's avatar
Sandrine Ribeau committed
273
            if not self._cw.vreg.schema.eschema(etype).final:
274
275
                if len(rset) == 1:
                    entity = rset.get_entity(0, 0)
276
                    return entity.absolute_url(vid=self.__regid__)
277
278
279
280
281
282
            # 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
283
284
285
            norestriction = (
                isinstance(restr, nodes.Relation) and restr.is_types_restriction()
            )
286
            if norestriction:
287
                return self._cw.build_url(etype.lower(), vid=self.__regid__)
288
        return self._cw.build_url("view", rql=rset.printable_rql(), vid=self.__regid__)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
289
290
291

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

    # view utilities ##########################################################
295

296
    def wview(self, __vid, rset=None, __fallback_vid=None, **kwargs):
297
        """shortcut to self.view method automatically passing self.w as argument"""
298
        self._cw.view(__vid, rset, __fallback_vid, w=self._w, **kwargs)
299

300
301
    def whead(self, data, *args, **kwargs):
        self._cw.html_headers.write(data, *args, **kwargs)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
302
303

    def wdata(self, data):
304
305
306
        """
        Simple helper that escapes `data` and writes into `self.w`
        """
Sylvain Thénault's avatar
Sylvain Thénault committed
307
        self.w(xml_escape(data))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
308
309
310
311
312
313
314
315

    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]
316

Adrien Di Mascio's avatar
Adrien Di Mascio committed
317
318
319
320
    def page_title(self):
        """returns a title according to the result set - used for the
        title in the HTML header
        """
321
        vtitle = self._cw.form.get("vtitle")
Adrien Di Mascio's avatar
Adrien Di Mascio committed
322
        if vtitle:
323
            return self._cw._(vtitle)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
324
325
        # class defined title will only be used if the resulting title doesn't
        # seem clear enough
326
        vtitle = getattr(self, "title", None) or ""
Adrien Di Mascio's avatar
Adrien Di Mascio committed
327
        if vtitle:
328
329
            vtitle = self._cw._(vtitle)
        rset = self.cw_rset
Adrien Di Mascio's avatar
Adrien Di Mascio committed
330
331
332
        if rset and rset.rowcount:
            if rset.rowcount == 1:
                try:
333
                    entity = rset.complete_entity(0, 0)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
334
335
336
                    # use long_title to get context information if any
                    clabel = entity.dc_long_title()
                except NotAnEntity:
337
                    clabel = display_name(self._cw, rset.description[0][0])
338
339
                    clabel = "%s (%s)" % (clabel, vtitle)
            else:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
340
341
                etypes = rset.column_types(0)
                if len(etypes) == 1:
342
                    etype = next(iter(etypes))
343
344
345
                    clabel = display_name(self._cw, etype, "plural")
                else:
                    clabel = "#[*] (%s)" % vtitle
Adrien Di Mascio's avatar
Adrien Di Mascio committed
346
347
        else:
            clabel = vtitle
348
        return "%s (%s)" % (clabel, self._cw.property_value("ui.site-title"))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
349

350
351
352
    def field(
        self, label, value, row=True, show_label=True, w=None, tr=True, table=False
    ):
353
        """read-only field"""
Adrien Di Mascio's avatar
Adrien Di Mascio committed
354
355
        if w is None:
            w = self.w
356
        if table:
357
            w('<tr class="entityfield">')
358
        else:
359
            w('<div class="entityfield">')
360
        if show_label and label:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
361
            if tr:
362
                label = display_name(self._cw, label)
363
            if table:
364
                w("<th>%s</th>", label, escape=False)
365
            else:
366
                w('<span class="label">%s</span> ', label, escape=False)
367
        if table:
368
            if not (show_label and label):
369
                w('<td colspan="2">%s</td></tr>', value, escape=False)
370
            else:
371
                w("<td>%s</td></tr>", value, escape=False)
372
        else:
373
            w("<span>%s</span></div>", value, escape=False)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
374

375

376
377
# concrete views base classes #################################################

378

379
class EntityView(View):
sylvain.thenault@logilab.fr's avatar
cleanup    
sylvain.thenault@logilab.fr committed
380
    """base class for views applying on an entity (i.e. uniform result set)"""
381

382
    __select__ = non_final_entity()
383
    category = _("entityview")
384

385
386
    def call(self, **kwargs):
        if self.cw_rset is None:
387
388
389
390
391
392
393
394
395
            # * 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
396
397
398
            entity = self.cw_extra_kwargs.pop("entity")
            if "entity" in kwargs:
                assert kwargs.pop("entity") is entity
399
            self.entity_call(entity, **kwargs)
400
401
402
403
404
405
406
        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):
407
        raise NotImplementedError("%r %r" % (self.__regid__, self.__class__))
408

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

Adrien Di Mascio's avatar
Adrien Di Mascio committed
410
class StartupView(View):
sylvain.thenault@logilab.fr's avatar
cleanup    
sylvain.thenault@logilab.fr committed
411
    """base class for views which doesn't need a particular result set to be
Dimitri Papadopoulos's avatar
Dimitri Papadopoulos committed
412
    displayed (so they can always be displayed!)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
413
    """
414

415
    __select__ = none_rset()
416

417
    category = _("startupview")
418

Adrien Di Mascio's avatar
Adrien Di Mascio committed
419
420
421
422
423
424
425
426
427
428
429
430
431
    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)
    """
432

433
    __select__ = none_rset() | non_final_entity()
434

Adrien Di Mascio's avatar
Adrien Di Mascio committed
435
    default_rql = None
436

Sylvain Thénault's avatar
Sylvain Thénault committed
437
    def __init__(self, req, rset=None, **kwargs):
Sylvain Thénault's avatar
Sylvain Thénault committed
438
        super(EntityStartupView, self).__init__(req, rset=rset, **kwargs)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
439
440
        if rset is None:
            # this instance is not in the "entityview" category
441
            self.category = "startupview"
Adrien Di Mascio's avatar
Adrien Di Mascio committed
442
443

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

447
448
449
450
    def no_entities(self, **kwargs):
        """override to display something when no entities were found"""
        pass

Adrien Di Mascio's avatar
Adrien Di Mascio committed
451
    def call(self, **kwargs):
sylvain.thenault@logilab.fr's avatar
cleanup    
sylvain.thenault@logilab.fr committed
452
453
        """override call to execute rql returned by the .startup_rql method if
        necessary
Adrien Di Mascio's avatar
Adrien Di Mascio committed
454
        """
455
456
457
        rset = self.cw_rset
        if rset is None:
            rset = self.cw_rset = self._cw.execute(self.startup_rql())
458
        if rset:
459
            for i in range(len(rset)):
460
461
462
                self.wview(self.__regid__, rset, row=i, **kwargs)
        else:
            self.no_entities(**kwargs)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
463

464

Adrien Di Mascio's avatar
Adrien Di Mascio committed
465
466
class AnyRsetView(View):
    """base class for views applying on any non empty result sets"""
467

468
    __select__ = nonempty_rset()
469

470
    category = _("anyrsetview")
471

472
    def columns_labels(self, mainindex=0, tr=True):
473
474
475
476
477
478
479
480
481
482
483
        """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
484
        if tr:
485
            translate = partial(display_name, self._cw)
Sylvain Thenault's avatar
Sylvain Thenault committed
486
        else:
487
            translate = lambda val, *args, **kwargs: val
488
        # XXX [0] because of missing Union support
489
        rql_syntax_tree = self.cw_rset.syntax_tree()
490
        rqlstdescr = rql_syntax_tree.get_description(mainindex, translate)[0]
491
        labels = []
492
        for colidx, label in enumerate(rqlstdescr):
493
            labels.append(self.column_label(colidx, label, translate))
494
        return labels
495

496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
    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
513
        if label == "Any":
514
515
516
            etypes = self.cw_rset.column_types(colidx)
            if translate_func is not None:
                etypes = map(translate_func, etypes)
517
            label = ",".join(etypes)
518
519
520
        return label


Adrien Di Mascio's avatar
Adrien Di Mascio committed
521
522
# concrete template base classes ##############################################

523

524
class MainTemplate(View):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
525
526
527
528
529
    """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
    """

530
    doctype = "<!DOCTYPE html>"
Adrien Di Mascio's avatar
Adrien Di Mascio committed
531

532
    def set_stream(self, w=None):
533
        if self._w is not None:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
534
535
536
            return
        if w is None:
            if self.binary:
537
                self._stream = stream = BytesIO()
Adrien Di Mascio's avatar
Adrien Di Mascio committed
538
            else:
539
                self._stream = stream = HTMLStream(self._cw)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
540
541
542
            w = stream.write
        else:
            stream = None
543
        self._w = w
Adrien Di Mascio's avatar
Adrien Di Mascio committed
544
545
546
547
548
549
        return stream

    def write_doctype(self, xmldecl=True):
        assert isinstance(self._stream, HTMLStream)
        self._stream.doctype = self.doctype
        if not xmldecl:
550
            self._stream.xmldecl = ""
551

sylvain.thenault@logilab.fr's avatar
sylvain.thenault@logilab.fr committed
552
553
    def linkable(self):
        return False
554

555

556
557
# concrete component base classes #############################################

558

559
560
class ReloadableMixIn(object):
    """simple mixin for reloadable parts of UI"""
561

562
563
564
565
    @property
    def domid(self):
        return domid(self.__regid__)

566
567
568

class Component(ReloadableMixIn, View):
    """base class for components"""
569
570

    __registry__ = "components"
571
    __select__ = yes()
572

573
    # XXX huummm, much probably useless (should be...)
574
575
    htmlclass = "mainRelated"

576
577
    @property
    def cssclass(self):
578
        return "%s %s" % (self.htmlclass, domid(self.__regid__))
579

580
581
582
    # XXX should rely on ReloadableMixIn.domid
    @property
    def domid(self):
583
584
        return "%sComponent" % domid(self.__regid__)

585

586
587
588
# EntityAdapter moved to cubicweb.entity ######################################
from logilab.common.deprecation import class_moved
from cubicweb import entity
589
590

EntityAdapter = class_moved(entity.EntityAdapter)  # cubicweb 3.28