cubicweb/web/views/xmlrss.py
author Nicolas Chauvat <nicolas.chauvat@logilab.fr>
Sat, 08 Feb 2020 22:57:59 +0100
changeset 12880 59d4ad7e7df3
parent 12567 26744ad37953
permissions -rw-r--r--
[adapters] move class EntityAdapter from cubicweb.view to cubicweb.entity The adapter mechanism is not specific to the use of views. Moving the class the cubicweb.entity makes this clearer.

# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
#
# CubicWeb is free software: you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
"""base xml and rss views"""

from cubicweb import _

from base64 import b64encode
from time import timezone

from logilab.mtconverter import xml_escape

from cubicweb.predicates import (is_instance, non_final_entity, one_line_rset,
                                 appobject_selectable, adaptable)
from cubicweb.view import EntityView, AnyRsetView, Component
from cubicweb.entity import EntityAdapter
from cubicweb.uilib import simple_sgml_tag
from cubicweb.web import httpcache, component


def encode_bytes(value):
    return '<![CDATA[%s]]>' % b64encode(value.getvalue())

# see cubicweb.sobjects.parser.DEFAULT_CONVERTERS
SERIALIZERS = {
    'String': xml_escape,
    'Bytes': encode_bytes,
    'Date': lambda x: x.strftime('%Y-%m-%d'),
    'Datetime': lambda x: x.strftime('%Y-%m-%d %H:%M:%S'),
    'Time': lambda x: x.strftime('%H:%M:%S'),
    'TZDatetime': lambda x: x.strftime('%Y-%m-%d %H:%M:%S'), # XXX TZ
    'TZTime': lambda x: x.strftime('%H:%M:%S'),
    'Interval': lambda x: x.days * 60*60*24 + x.seconds,
    }

# base xml views ##############################################################

class XMLView(EntityView):
    """xml view for entities"""
    __regid__ = 'xml'
    title = _('xml export (entities)')
    templatable = False
    content_type = 'text/xml'
    xml_root = 'rset'
    item_vid = 'xmlitem'

    def cell_call(self, row, col):
        self.wview(self.item_vid, self.cw_rset, row=row, col=col)

    def call(self):
        """display a list of entities by calling their <item_vid> view"""
        self.w(u'<?xml version="1.0" encoding="%s"?>\n' % self._cw.encoding)
        self.w(u'<%s size="%s">\n' % (self.xml_root, len(self.cw_rset)))
        for i in range(self.cw_rset.rowcount):
            self.cell_call(i, 0)
        self.w(u'</%s>\n' % self.xml_root)


class XMLItemView(EntityView):
    __regid__ = 'xmlitem'

    def entity_call(self, entity):
        """element as an item for an xml feed"""
        entity.complete()
        source = entity.cw_source[0].name
        self.w(u'<%s eid="%s" cwuri="%s" cwsource="%s">\n'
               % (entity.cw_etype, entity.eid, xml_escape(entity.cwuri),
                  xml_escape(source)))
        for rschema, attrschema in sorted(entity.e_schema.attribute_definitions()):
            attr = rschema.type
            if attr in ('eid', 'cwuri'):
                continue
            else:
                try:
                    value = entity.cw_attr_cache[attr]
                except KeyError:
                    # Bytes
                    continue
            if value is None:
                self.w(u'  <%s/>\n' % attr)
            else:
                if attrschema in SERIALIZERS:
                    value = SERIALIZERS[attrschema](value)
                self.w(u'  <%s>%s</%s>\n' % (attr, value, attr))
        for relstr in self._cw.list_form_param('relation'):
            try:
                rtype, role = relstr.split('-')
            except ValueError:
                self.error('badly formated relation name %r', relstr)
                continue
            if role == 'subject':
                getrschema = entity.e_schema.subjrels
            elif role == 'object':
                getrschema = entity.e_schema.objrels
            else:
                self.error('badly formated relation name %r', relstr)
                continue
            if rtype not in getrschema:
                self.error('unexisting relation %r', relstr)
                continue
            self.w(u'  <%s role="%s">\n' % (rtype, role))
            self.wview('xmlrelateditem', entity.related(rtype, role, safe=True), 'null')
            self.w(u'  </%s>\n' % rtype)
        self.w(u'</%s>\n' % (entity.e_schema))


class XMLRelatedItemView(EntityView):
    __regid__ = 'xmlrelateditem'
    add_div_section = False

    def entity_call(self, entity):
        # XXX put unique attributes as xml attribute, they are much probably
        # used to search existing entities in client data feed, and putting it
        # here may avoid an extra request to get those attributes values
        self.w(u'    <%s eid="%s" cwuri="%s"/>\n'
               % (entity.e_schema, entity.eid, xml_escape(entity.cwuri)))


class XMLRelatedItemStateView(XMLRelatedItemView):
    __select__ = is_instance('State')

    def entity_call(self, entity):
        self.w(u'    <%s eid="%s" cwuri="%s" name="%s"/>\n'
               % (entity.e_schema, entity.eid, xml_escape(entity.cwuri),
                  xml_escape(entity.name)))


class XMLRsetView(AnyRsetView):
    """dumps raw rset as xml"""
    __regid__ = 'rsetxml'
    title = _('xml export')
    templatable = False
    content_type = 'text/xml'
    xml_root = 'rset'

    def call(self):
        w = self.w
        rset, descr = self.cw_rset, self.cw_rset.description
        eschema = self._cw.vreg.schema.eschema
        labels = self.columns_labels(tr=False)
        w(u'<?xml version="1.0" encoding="%s"?>\n' % self._cw.encoding)
        w(u'<%s query="%s">\n' % (self.xml_root, xml_escape(rset.printable_rql())))
        for rowindex, row in enumerate(self.cw_rset):
            w(u' <row>\n')
            for colindex, val in enumerate(row):
                etype = descr[rowindex][colindex]
                tag = labels[colindex]
                attrs = {}
                if '(' in tag:
                    attrs['expr'] = tag
                    tag = 'funccall'
                if val is not None and not eschema(etype).final:
                    attrs['eid'] = val
                    # csvrow.append(val) # val is eid in that case
                    val = self._cw.view('textincontext', rset,
                                        row=rowindex, col=colindex)
                else:
                    val = self._cw.view('final', rset, row=rowindex,
                                        col=colindex, format='text/plain')
                w(simple_sgml_tag(tag, val, **attrs))
            w(u'\n </row>\n')
        w(u'</%s>\n' % self.xml_root)


# RSS stuff ###################################################################

class IFeedAdapter(EntityAdapter):
    __needs_bw_compat__ = True
    __regid__ = 'IFeed'
    __select__ = is_instance('Any')

    def rss_feed_url(self):
        """return a 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()

    def feed_url(self):
        return self._cw.build_url(rql=self.cw_rset.limited_rql(), vid='rss')


class RSSEntityFeedURL(Component):
    __regid__ = 'rss_feed_url'
    __select__ = one_line_rset() & adaptable('IFeed')

    def feed_url(self):
        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(component.CtxComponent):
    """just display the RSS icon on uniform result set"""
    __regid__ = 'rss'
    __select__ = (component.CtxComponent.__select__
                  & appobject_selectable('components', 'rss_feed_url'))

    visible = False
    order = 999

    def render(self, w, **kwargs):
        try:
            rss = self._cw.uiprops['RSS_LOGO']
        except KeyError:
            self.error('missing RSS_LOGO external resource')
            return
        urlgetter = self._cw.vreg['components'].select('rss_feed_url', self._cw,
                                                       rset=self.cw_rset)
        url = urlgetter.feed_url()
        w(u'<a href="%s"><img src="%s" alt="rss"/></a>\n' % (xml_escape(url), rss))


class RSSView(XMLView):
    __regid__ = 'rss'
    title = _('rss export')
    templatable = False
    content_type = 'text/xml'
    http_cache_manager = httpcache.MaxAgeHTTPCacheManager
    cache_max_age = 60*60*2 # stay in http cache for 2 hours by default
    item_vid = 'rssitem'

    def _open(self):
        req = self._cw
        self.w(u'<?xml version="1.0" encoding="%s"?>\n' % req.encoding)
        self.w(u'<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">\n')
        self.w(u'  <channel>\n')
        self.w(u'    <title>%s RSS Feed</title>\n'
               % xml_escape(self.page_title()))
        self.w(u'    <description>%s</description>\n'
               % xml_escape(req.form.get('vtitle', '')))
        params = req.form.copy()
        params.pop('vid', None)
        self.w(u'    <link>%s</link>\n' % xml_escape(self._cw.build_url(**params)))

    def _close(self):
        self.w(u'  </channel>\n')
        self.w(u'</rss>')

    def call(self):
        """display a list of entities by calling their <item_vid> view"""
        self._open()
        for i in range(self.cw_rset.rowcount):
            self.cell_call(i, 0)
        self._close()

    def cell_call(self, row, col):
        self.wview(self.item_vid, self.cw_rset, row=row, col=col)


class RSSItemView(EntityView):
    __regid__ = 'rssitem'
    date_format = '%%Y-%%m-%%dT%%H:%%M%+03i:00' % (timezone / 3600)
    add_div_section = False

    def cell_call(self, row, col):
        entity = self.cw_rset.complete_entity(row, col)
        self.w(u'<item>\n')
        self.w(u'<guid isPermaLink="true">%s</guid>\n'
               % xml_escape(entity.absolute_url()))
        self.render_title_link(entity)
        self.render_description(entity)
        self._marker('dc:date', entity.dc_date(self.date_format))
        self.render_entity_creator(entity)
        self.w(u'</item>\n')

    def render_description(self, entity):
        self._marker('description', entity.dc_description(format='text/html'))

    def render_title_link(self, entity):
        self._marker('title', entity.dc_long_title())
        self._marker('link', entity.absolute_url())

    def render_entity_creator(self, entity):
        if entity.creator:
            self._marker('dc:creator', entity.dc_creator())

    def _marker(self, marker, value):
        if value:
            self.w(u'  <%s>%s</%s>\n' % (marker, xml_escape(value), marker))