WIP [web] add Link HTTP headers if alternate representations available

authorNicolas Chauvat <nicolas.chauvat@logilab.fr>
changesetc7843c3449f4
branchdefault
phasedraft
hiddenno
parent revision#2f3cb7f5a92f chore(flake8): update flake8-ok-files.txt with files that passes flake8 test
child revision<not specified>
files modified by this revision
cubicweb/web/views/__init__.py
cubicweb/web/views/basecontrollers.py
# HG changeset patch
# User Nicolas Chauvat <nicolas.chauvat@logilab.fr>
# Date 1581195890 -3600
# Sat Feb 08 22:04:50 2020 +0100
# Node ID c7843c3449f46d77cfa284c896ecf75ffde5fb0b
# Parent 2f3cb7f5a92f4c3a70a5abf30227d6228b614804
WIP [web] add Link HTTP headers if alternate representations available

diff --git a/cubicweb/web/views/__init__.py b/cubicweb/web/views/__init__.py
@@ -18,10 +18,11 @@
1  """Views, forms, actions... for the CubicWeb web client"""
2 
3 
4 
5  import sys
6 +import logging
7 
8  from rql import nodes
9  from logilab.mtconverter import xml_escape
10 
11 
@@ -63,26 +64,40 @@
12                  break
13          else:
14              return True
15      return False
16 
17 -# FIXME: VID_BY_MIMETYPE is unfortunately a bit too naive since
18 -#        some browsers (e.g. FF2) send a bunch of mimetypes in
19 -#        the Accept header, for instance:
20 -#          text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,
21 -#          text/plain;q=0.8,image/png,*/*;q=0.5
22 -VID_BY_MIMETYPE = {
23 -    #'text/xml': 'xml',
24 -    # XXX rss, owl...
25 -}
26 +
27 +# XXX test also Accept-Content-Type
28 +# XXX try recursively in the order of preference/quality?
29 +# if no success for q=0.9, try for q=0.8 etc
30  def vid_from_rset(req, rset, schema, check_table=True):
31      """given a result set, return a view id"""
32 +
33 +    # try to negotiate using the content type
34 +    # but if HTML is present in the Accept header,
35 +    # return HTML for safer backward compatibility
36 +    if False:
37 +        matching = set()
38 +        for mimetype in req.parse_accept_header('Accept'):
39 +            logging.debug('client is accepting %s', mimetype)
40 +            if 'html' in mimetype:
41 +                break
42 +            for vid, views in req.vreg['views'].items():
43 +                for view in views:
44 +                    if mimetype in ('*/*', str(view.content_type)):
45 +                        matching.add(vid)
46 +                        break
47 +        else:
48 +            logging.debug('found matching vids %s', matching)
49 +            if len(matching) > 0:
50 +                return matching
51 +
52 +    # now returning HTML to the client
53 +    logging.debug('looking for a vid returning html')
54      if rset is None:
55          return 'index'
56 -    for mimetype in req.parse_accept_header('Accept'):
57 -        if mimetype in VID_BY_MIMETYPE:
58 -            return VID_BY_MIMETYPE[mimetype]
59      nb_rows = len(rset)
60      # empty resultset
61      if nb_rows == 0:
62          return 'noresult'
63      # entity result set
@@ -96,11 +111,10 @@
64          if len(rset.column_types(0)) == 1:
65              return 'sameetypelist'
66          return 'list'
67      return 'table'
68 
69 -
70  def linksearch_select_url(req, rset):
71      """when searching an entity to create a relation, return a URL to select
72      entities in the given rset
73      """
74      req.add_js( ('cubicweb.ajax.js', 'cubicweb.edition.js') )
diff --git a/cubicweb/web/views/basecontrollers.py b/cubicweb/web/views/basecontrollers.py
@@ -73,10 +73,32 @@
75          #   we'll be redirected to the login form
76          msg = self._cw._('you have been logged out')
77          return self._cw.build_url('view', vid='loggedout')
78 
79 
80 +def possible_content_types(req, rset):
81 +    """Return set of all content types of views that can be selected for given rset
82 +    """
83 +    content_types = set()
84 +    for vid, views in req.vreg['views'].items():
85 +        print('vid',vid,'views',views)
86 +        for view in views:
87 +            try:
88 +                print(view,'score',view.__select__(view, req, rset=rset))
89 +                if view.__select__(view, req, rset=rset) > 0:
90 +                    content_type = view.content_type
91 +                    if content_type and not isinstance(content_type, str):
92 +                        print('content_type should be a string', content_type)
93 +                    if isinstance(content_type, str):
94 +                        content_types.add(content_type)
95 +                    else:
96 +                        print('content_type ignored %s' % content_type)
97 +            except Exception as ex: # XXX more specific exception
98 +                print(ex)
99 +    print('returning possible content_types %s for rset %s' % (content_types, rset))
100 +    return content_types
101 +
102  class ViewController(Controller):
103      """standard entry point :
104      - build result set
105      - select and call main template
106      """
@@ -93,10 +115,11 @@
107          return self._cw.vreg['views'].main_template(self._cw, template,
108                                                      rset=rset, view=view)
109 
110      def _select_view_and_rset(self, rset):
111          req = self._cw
112 +        # get rset
113          if rset is None and not hasattr(req, '_rql_processed'):
114              req._rql_processed = True
115              if req.cnx:
116                  rset = self.process_rql()
117              else:
@@ -108,25 +131,25 @@
118                      req.headers_out.addRawHeader(
119                          'Link', "<%s>;rel=alternate;type=%s" % (rset.one().cwuri, mimetype))
120                  except NotAnEntity:
121                      # was a string, or a number (but not an eid)
122                      break
123 +        # set headers for alternate content types
124 +        print('looking for alternate headers')
125 +        for ct in possible_content_types(req, rset):
126 +            req.headers_out.addRawHeader('Link', "<%s>;rel=alternate;type=%s" % (req.url(), ct))
127          try:
128 -            view = self._cw.vreg['views'].select(vid, req, rset=rset)
129 -        except ObjectNotFound:
130 -            self.warning("the view %s could not be found", vid)
131 -            req.set_message(req._("The view %s could not be found") % vid)
132 -            vid = vid_from_rset(req, rset, self._cw.vreg.schema)
133 -            view = self._cw.vreg['views'].select(vid, req, rset=rset)
134 +            view = select_among(self._cw.vreg['views'], vids, req, rset=rset)
135          except NoSelectableObject:
136              if rset:
137                  req.set_message(req._("The view %s can not be applied to this query") % vid)
138              else:
139                  req.set_message(req._("You have no access to this view or it can not "
140                                        "be used to display the current data."))
141              vid = req.form.get('fallbackvid') or vid_from_rset(req, rset, req.vreg.schema)
142              view = req.vreg['views'].select(vid, req, rset=rset)
143 +            # XXX what if this selection fails?
144          return view, rset
145 
146      def execute_linkto(self, eid=None):
147          """XXX __linkto parameter may cause security issue
148 
@@ -148,10 +171,19 @@
149                  rql = 'SET Y %s X WHERE X eid %%(x)s, Y eid %%(y)s' % rtype
150              for teid in eids:
151                  req.execute(rql, {'x': eid, 'y': int(teid)})
152 
153 
154 +def select_among(vreg, __oids, *args, **kwargs):
155 +    objects = []
156 +    for __oid in __oids:
157 +        objects.extend(vreg[__oid])
158 +    obj = vreg._select_best(objects, *args, **kwargs)
159 +    if obj is None:
160 +        raise NoSelectableObject(args, kwargs, objects)
161 +    return obj
162 +
163  def _validation_error(req, ex):
164      req.cnx.rollback()
165      ex.translate(req._) # translate messages using ui language
166      # XXX necessary to remove existant validation error?
167      # imo (syt), it's not necessary
obsoletes