web/views/cwsources.py
brancholdstable
changeset 8462 a14b6562082b
parent 8407 6874eb7a08e8
child 8408 41461b2e9854
equal deleted inserted replaced
8231:1bb43e31032d 8462:a14b6562082b
    20 """
    20 """
    21 
    21 
    22 __docformat__ = "restructuredtext en"
    22 __docformat__ = "restructuredtext en"
    23 _ = unicode
    23 _ = unicode
    24 
    24 
    25 from itertools import repeat, chain
    25 import logging
    26 
    26 from itertools import repeat
    27 from cubicweb import Unauthorized
    27 from logilab.mtconverter import xml_escape
    28 from cubicweb.selectors import is_instance, score_entity, match_user_groups
    28 from logilab.common.decorators import cachedproperty
       
    29 
       
    30 from cubicweb import Unauthorized, tags
       
    31 from cubicweb.utils import make_uid
       
    32 from cubicweb.selectors import (is_instance, score_entity, has_related_entities,
       
    33                                 match_user_groups, match_kwargs, match_view)
    29 from cubicweb.view import EntityView, StartupView
    34 from cubicweb.view import EntityView, StartupView
    30 from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, display_name
    35 from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, display_name
    31 from cubicweb.web import uicfg, formwidgets as wdgs
    36 from cubicweb.web import uicfg, formwidgets as wdgs, facet
    32 from cubicweb.web.views import tabs, actions, ibreadcrumbs, add_etype_button
    37 from cubicweb.web.views import add_etype_button
       
    38 from cubicweb.web.views import (tabs, actions, ibreadcrumbs, navigation,
       
    39                                 tableview, pyviews)
    33 
    40 
    34 
    41 
    35 _abaa = uicfg.actionbox_appearsin_addmenu
    42 _abaa = uicfg.actionbox_appearsin_addmenu
    36 # there are explicit 'add' buttons for those
    43 # there are explicit 'add' buttons for those
    37 _abaa.tag_object_of(('CWSourceSchemaConfig', 'cw_schema', '*'), False)
    44 _abaa.tag_object_of(('CWSourceSchemaConfig', 'cw_schema', '*'), False)
    38 _abaa.tag_object_of(('CWSourceSchemaConfig', 'cw_for_source', '*'), False)
    45 _abaa.tag_object_of(('CWSourceSchemaConfig', 'cw_for_source', '*'), False)
    39 _abaa.tag_object_of(('CWSourceSchemaConfig', 'cw_host_config_of', '*'), False)
    46 _abaa.tag_object_of(('CWSourceSchemaConfig', 'cw_host_config_of', '*'), False)
       
    47 _abaa.tag_object_of(('CWDataImport', 'cw_import_of', '*'), False)
    40 
    48 
    41 _afs = uicfg.autoform_section
    49 _afs = uicfg.autoform_section
       
    50 _afs.tag_attribute(('CWSource', 'latest_retrieval'), 'main', 'hidden')
       
    51 _afs.tag_attribute(('CWSource', 'in_synchronization'), 'main', 'hidden')
    42 _afs.tag_object_of(('*', 'cw_for_source', 'CWSource'), 'main', 'hidden')
    52 _afs.tag_object_of(('*', 'cw_for_source', 'CWSource'), 'main', 'hidden')
    43 
    53 
    44 _affk = uicfg.autoform_field_kwargs
    54 _affk = uicfg.autoform_field_kwargs
    45 _affk.tag_attribute(('CWSource', 'parser'), {'widget': wdgs.TextInput})
    55 _affk.tag_attribute(('CWSource', 'parser'), {'widget': wdgs.TextInput})
    46 
    56 
    60 _rc.tag_attribute(('CWSourceSchemaConfig', 'options'), {'rvid': 'verbatimattr'})
    70 _rc.tag_attribute(('CWSourceSchemaConfig', 'options'), {'rvid': 'verbatimattr'})
    61 
    71 
    62 
    72 
    63 class CWSourcePrimaryView(tabs.TabbedPrimaryView):
    73 class CWSourcePrimaryView(tabs.TabbedPrimaryView):
    64     __select__ = is_instance('CWSource')
    74     __select__ = is_instance('CWSource')
    65     tabs = [_('cwsource-main'), _('cwsource-mapping')]
    75     tabs = [_('cwsource-main'), _('cwsource-mapping'), _('cwsource-imports')]
    66     default_tab = 'cwsource-main'
    76     default_tab = 'cwsource-main'
    67 
    77 
    68 
    78 
    69 class CWSourceMainTab(tabs.PrimaryTab):
    79 class CWSourceMainTab(tabs.PrimaryTab):
    70     __regid__ = 'cwsource-main'
    80     __regid__ = 'cwsource-main'
    71     __select__ = tabs.PrimaryTab.__select__ & is_instance('CWSource')
    81     __select__ = is_instance('CWSource')
    72 
    82 
    73     def render_entity_attributes(self, entity):
    83     def render_entity_attributes(self, entity):
    74         super(CWSourceMainTab, self).render_entity_attributes(entity)
    84         super(CWSourceMainTab, self).render_entity_attributes(entity)
    75         self.w(add_etype_button(self._cw, 'CWSourceHostConfig',
    85         self.w(add_etype_button(self._cw, 'CWSourceHostConfig',
    76                                 __linkto='cw_host_config_of:%s:subject' % entity.eid,
    86                                 __linkto='cw_host_config_of:%s:subject' % entity.eid,
    82         except Unauthorized:
    92         except Unauthorized:
    83             pass
    93             pass
    84         else:
    94         else:
    85             if hostconfig:
    95             if hostconfig:
    86                 self.w(u'<h3>%s</h3>' % self._cw._('CWSourceHostConfig_plural'))
    96                 self.w(u'<h3>%s</h3>' % self._cw._('CWSourceHostConfig_plural'))
    87                 self._cw.view('editable-table', hostconfig,
    97                 self._cw.view('table', hostconfig, w=self.w,
    88                               displaycols=range(2), w=self.w)
    98                               displaycols=range(2),
       
    99                               cellvids={1: 'editable-final'})
    89 
   100 
    90 
   101 
    91 MAPPED_SOURCE_TYPES = set( ('pyrorql', 'datafeed') )
   102 MAPPED_SOURCE_TYPES = set( ('pyrorql', 'datafeed') )
    92 
   103 
    93 class CWSourceMappingTab(EntityView):
   104 class CWSourceMappingTab(EntityView):
    94     __regid__ = 'cwsource-mapping'
   105     __regid__ = 'cwsource-mapping'
    95     __select__ = (tabs.PrimaryTab.__select__ & is_instance('CWSource')
   106     __select__ = (is_instance('CWSource')
    96                   & match_user_groups('managers')
   107                   & match_user_groups('managers')
    97                   & score_entity(lambda x:x.type in MAPPED_SOURCE_TYPES))
   108                   & score_entity(lambda x:x.type in MAPPED_SOURCE_TYPES))
    98 
   109 
    99     def entity_call(self, entity):
   110     def entity_call(self, entity):
   100         _ = self._cw._
   111         _ = self._cw._
   125         if (checker.errors or checker.warnings or checker.infos):
   136         if (checker.errors or checker.warnings or checker.infos):
   126             self.w('<h2>%s</h2>' % _('Detected problems'))
   137             self.w('<h2>%s</h2>' % _('Detected problems'))
   127             errors = zip(repeat(_('error')), checker.errors)
   138             errors = zip(repeat(_('error')), checker.errors)
   128             warnings = zip(repeat(_('warning')), checker.warnings)
   139             warnings = zip(repeat(_('warning')), checker.warnings)
   129             infos = zip(repeat(_('warning')), checker.infos)
   140             infos = zip(repeat(_('warning')), checker.infos)
   130             self.wview('pyvaltable', pyvalue=chain(errors, warnings, infos))
   141             self.wview('pyvaltable', pyvalue=errors + warnings + infos)
   131 
   142 
   132 
   143 
   133 class MappingChecker(object):
   144 class MappingChecker(object):
   134     def __init__(self, cwsource):
   145     def __init__(self, cwsource):
   135         self.cwsource = cwsource
   146         self.cwsource = cwsource
   245 
   256 
   246 MAPPING_CHECKERS = {
   257 MAPPING_CHECKERS = {
   247     'pyrorql': PyroRQLMappingChecker,
   258     'pyrorql': PyroRQLMappingChecker,
   248     }
   259     }
   249 
   260 
       
   261 
       
   262 class CWSourceImportsTab(EntityView):
       
   263     __regid__ = 'cwsource-imports'
       
   264     __select__ = (is_instance('CWSource')
       
   265                   & has_related_entities('cw_import_of', 'object'))
       
   266 
       
   267     def entity_call(self, entity):
       
   268         rset = self._cw.execute('Any X, XST, XET, XS ORDERBY XST DESC WHERE '
       
   269                                 'X cw_import_of S, S eid %(s)s, X status XS, '
       
   270                                 'X start_timestamp XST, X end_timestamp XET',
       
   271                                 {'s': entity.eid})
       
   272         self._cw.view('cw.imports-table', rset, w=self.w)
       
   273 
       
   274 
       
   275 class CWImportsTable(tableview.EntityTableView):
       
   276     __regid__ = 'cw.imports-table'
       
   277     __select__ = is_instance('CWDataImport')
       
   278     columns = ['import', 'start_timestamp', 'end_timestamp']
       
   279     column_renderers = {'import': tableview.MainEntityColRenderer()}
       
   280     layout_args = {'display_filter': 'top'}
       
   281 
       
   282 
       
   283 
       
   284 
   250 # sources management view ######################################################
   285 # sources management view ######################################################
   251 
   286 
   252 class ManageSourcesAction(actions.ManagersAction):
   287 class ManageSourcesAction(actions.ManagersAction):
   253     __regid__ = 'cwsource'
   288     __regid__ = 'cwsource'
   254     title = _('data sources')
   289     title = _('data sources')
   255     category = 'manage'
   290     category = 'manage'
   256 
   291 
   257 class CWSourceManagementView(StartupView):
   292 
   258     __regid__ = 'cw.source-management'
   293 class CWSourcesManagementView(StartupView):
   259     rql = ('Any S, ST, SP, SD, SN ORDERBY SN WHERE S is CWSource, S name SN, S type ST, '
   294     __regid__ = 'cw.sources-management'
       
   295     rql = ('Any S,ST,SP,SD,SN ORDERBY SN WHERE S is CWSource, S name SN, S type ST, '
   260            'S latest_retrieval SD, S parser SP')
   296            'S latest_retrieval SD, S parser SP')
   261     title = _('data sources management')
   297     title = _('data sources management')
   262 
   298 
   263     def call(self, **kwargs):
   299     def call(self, **kwargs):
   264         self.w('<h1>%s</h1>' % self._cw._(self.title))
   300         self.w('<h1>%s</h1>' % self._cw._(self.title))
   265         self.w(add_etype_button(self._cw, 'CWSource'))
   301         self.w(add_etype_button(self._cw, 'CWSource'))
   266         self.w(u'<div class="clear"></div>')
   302         self.w(u'<div class="clear"></div>')
   267         self.wview('table', self._cw.execute(self.rql), displaycols=range(4))
   303         self.wview('cw.sources-table', self._cw.execute(self.rql))
       
   304 
       
   305 
       
   306 class CWSourcesTable(tableview.EntityTableView):
       
   307     __regid__ = 'cw.sources-table'
       
   308     __select__ = is_instance('CWSource')
       
   309     columns = ['source', 'type', 'parser', 'latest_retrieval', 'latest_import']
       
   310 
       
   311     class LatestImportColRenderer(tableview.EntityTableColRenderer):
       
   312         def render_cell(self, w, rownum):
       
   313             entity = self.entity(rownum)
       
   314             rset = self._cw.execute('Any X,XS,XST ORDERBY XST DESC LIMIT 1 WHERE '
       
   315                                     'X cw_import_of S, S eid %(s)s, X status XS, '
       
   316                                     'X start_timestamp XST', {'s': entity.eid})
       
   317             if rset:
       
   318                 self._cw.view('incontext', rset, row=0, w=w)
       
   319             else:
       
   320                 w(self.empty_cell_content)
       
   321 
       
   322     column_renderers = {
       
   323         'source': tableview.MainEntityColRenderer(),
       
   324         'latest_import': LatestImportColRenderer(header=_('latest import'),
       
   325                                                  sortable=False)
       
   326         }
       
   327 
       
   328 # datafeed source import #######################################################
       
   329 
       
   330 REVERSE_SEVERITIES = {
       
   331     logging.DEBUG :   _('DEBUG'),
       
   332     logging.INFO :    _('INFO'),
       
   333     logging.WARNING : _('WARNING'),
       
   334     logging.ERROR :   _('ERROR'),
       
   335     logging.FATAL :   _('FATAL')
       
   336 }
       
   337 
       
   338 
       
   339 def log_to_table(req, rawdata):
       
   340     data = []
       
   341     for msg_idx, msg in enumerate(rawdata.split('<br/>')):
       
   342         record = msg.strip()
       
   343         if not record:
       
   344             continue
       
   345         try:
       
   346             severity, url, line, msg = record.split('\t', 3)
       
   347         except ValueError:
       
   348             req.warning('badly formated log %s' % record)
       
   349             url = line = u''
       
   350             severity = logging.DEBUG
       
   351             msg = record
       
   352         data.append( (severity, url, line, msg) )
       
   353     return data
       
   354 
       
   355 
       
   356 class LogTableLayout(tableview.TableLayout):
       
   357     __select__ = match_view('cw.log.table')
       
   358     needs_js = tableview.TableLayout.needs_js + ('cubicweb.log.js',)
       
   359     needs_css = tableview.TableLayout.needs_css + ('cubicweb.log.css',)
       
   360     columns_css = {
       
   361         0: 'logSeverity',
       
   362         1: 'logPath',
       
   363         2: 'logLine',
       
   364         3: 'logMsg',
       
   365         }
       
   366 
       
   367     def render_table(self, w, actions, paginate):
       
   368         default_level = self.view.cw_extra_kwargs['default_level']
       
   369         if default_level != 'Debug':
       
   370             self._cw.add_onload('$("select.logFilter").val("%s").change();'
       
   371                            % self._cw.form.get('logLevel', default_level))
       
   372         w(u'\n<form action="#"><fieldset>')
       
   373         w(u'<label>%s</label>' % self._cw._(u'Message threshold'))
       
   374         w(u'<select class="log_filter" onchange="filterLog(\'%s\', this.options[this.selectedIndex].value)">'
       
   375           % self.view.domid)
       
   376         for level in ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'FATAL'):
       
   377             w('<option value="%s">%s</option>' % (level.capitalize(),
       
   378                                                   self._cw._(level)))
       
   379         w(u'</select>')
       
   380         w(u'</fieldset></form>')
       
   381         super(LogTableLayout, self).render_table(w, actions, paginate)
       
   382 
       
   383     def table_attributes(self):
       
   384         attrs = super(LogTableLayout, self).table_attributes()
       
   385         attrs['id'] = 'table'+self.view.domid
       
   386         return attrs
       
   387 
       
   388     def row_attributes(self, rownum):
       
   389         attrs = super(LogTableLayout, self).row_attributes(rownum)
       
   390         attrs['id'] = 'log_msg_%i' % rownum
       
   391         severityname = REVERSE_SEVERITIES[int(self.view.pyvalue[rownum][0])]
       
   392         attrs['class'] = 'log%s' % severityname.capitalize()
       
   393         return attrs
       
   394 
       
   395     def cell_attributes(self, rownum, colnum, colid):
       
   396         attrs = super(LogTableLayout, self).cell_attributes(rownum, colnum, colid)
       
   397         attrs['class'] = self.columns_css[colnum]
       
   398         return attrs
       
   399 
       
   400 
       
   401 class LogTable(pyviews.PyValTableView):
       
   402     __regid__ = 'cw.log.table'
       
   403     headers = [_('severity'), _('url'), _('line'), _('message')]
       
   404 
       
   405     @cachedproperty
       
   406     def domid(self):
       
   407         return make_uid('logTable')
       
   408 
       
   409     class SeverityRenderer(pyviews.PyValTableColRenderer):
       
   410         def render_cell(self, w, rownum):
       
   411             severity = self.data[rownum][0]
       
   412             w(u'<a class="internallink" href="javascript:;" title="%(title)s" '
       
   413               u'''onclick="document.location.hash='%(msg_id)s';">&#182;</a>'''
       
   414               u'&#160;%(severity)s' % {
       
   415                 'severity': self._cw._(REVERSE_SEVERITIES[int(severity)]),
       
   416                 'title': self._cw._('permalink to this message'),
       
   417                 'msg_id': 'log_msg_%i' % rownum,
       
   418             })
       
   419         def sortvalue(self, rownum):
       
   420             return int(self.data[rownum][0])
       
   421 
       
   422     class URLRenderer(pyviews.PyValTableColRenderer):
       
   423         def render_cell(self, w, rownum):
       
   424             url = self.data[rownum][1]
       
   425             if url and url.startswith('http'):
       
   426                 url = tags.a(url, href=url)
       
   427             w(url or u'&#160;')
       
   428 
       
   429     class LineRenderer(pyviews.PyValTableColRenderer):
       
   430         def render_cell(self, w, rownum):
       
   431             line = self.data[rownum][2]
       
   432             w(line or u'&#160;')
       
   433 
       
   434     class MessageRenderer(pyviews.PyValTableColRenderer):
       
   435         snip_over = 7
       
   436         def render_cell(self, w, rownum):
       
   437             msg = self.data[rownum][3]
       
   438             lines = msg.splitlines()
       
   439             if len(lines) <= self.snip_over:
       
   440                 w(u'<pre class="rawtext">%s</pre>' % msg)
       
   441             else:
       
   442                 # The make_uid argument has no specific meaning here.
       
   443                 div_snip_id = make_uid(u'log_snip_')
       
   444                 div_full_id = make_uid(u'log_full_')
       
   445                 divs_id = (div_snip_id, div_full_id)
       
   446                 snip = u'\n'.join((lines[0], lines[1],
       
   447                                    u'  ...',
       
   448                                    u'    %i more lines [double click to expand]' % (len(lines)-4),
       
   449                                    u'  ...',
       
   450                                    lines[-2], lines[-1]))
       
   451                 divs = (
       
   452                         (div_snip_id, snip, u'expand', "class='collapsed'"),
       
   453                         (div_full_id, msg,  u'collapse', "class='hidden'")
       
   454                 )
       
   455                 for div_id, content, button, h_class in divs:
       
   456                     text = self._cw._(button)
       
   457                     js = u"toggleVisibility('%s'); toggleVisibility('%s');" % divs_id
       
   458                     w(u'<div id="%s" %s>' % (div_id, h_class))
       
   459                     w(u'<pre class="raw_test" ondblclick="javascript: %s" '
       
   460                       u'title="%s" style="display: block;">' % (js, text))
       
   461                     w(content)
       
   462                     w(u'</pre>')
       
   463                     w(u'</div>')
       
   464 
       
   465     column_renderers = {0: SeverityRenderer(),
       
   466                         1: URLRenderer(sortable=False),
       
   467                         2: LineRenderer(sortable=False),
       
   468                         3: MessageRenderer(sortable=False),
       
   469                         }
       
   470 
       
   471 
       
   472 class DataFeedSourceDataImport(EntityView):
       
   473     __select__ = EntityView.__select__ & match_kwargs('rtype')
       
   474     __regid__ = 'cw.formated_log'
       
   475 
       
   476     def cell_call(self, row, col, rtype, loglevel='Info', **kwargs):
       
   477         if 'dispctrl' in self.cw_extra_kwargs:
       
   478             loglevel = self.cw_extra_kwargs['dispctrl'].get('loglevel', loglevel)
       
   479         entity = self.cw_rset.get_entity(row, col)
       
   480         value = getattr(entity, rtype)
       
   481         if value:
       
   482             self._cw.view('cw.log.table', pyvalue=log_to_table(self._cw, value),
       
   483                           default_level=loglevel, w=self.w)
       
   484         else:
       
   485             self.w(self._cw._('no log to display'))
       
   486 
       
   487 
       
   488 _pvs.tag_attribute(('CWDataImport', 'log'), 'relations')
       
   489 _pvdc.tag_attribute(('CWDataImport', 'log'), {'vid': 'cw.formated_log'})
       
   490 _pvs.tag_subject_of(('CWDataImport', 'cw_import_of', '*'), 'hidden') # in breadcrumbs
       
   491 _pvs.tag_object_of(('*', 'cw_import_of', 'CWSource'), 'hidden') # in dedicated tab
       
   492 
       
   493 
       
   494 class CWDataImportIPrevNextAdapter(navigation.IPrevNextAdapter):
       
   495     __select__ = is_instance('CWDataImport')
       
   496 
       
   497     def next_entity(self):
       
   498         if self.entity.start_timestamp is not None:
       
   499             # add NOT X eid %(e)s because > may not be enough
       
   500             rset = self._cw.execute(
       
   501                 'Any X,XSTS ORDERBY 2 LIMIT 1 WHERE X is CWDataImport, '
       
   502                 'X cw_import_of S, S eid %(s)s, NOT X eid %(e)s, '
       
   503                 'X start_timestamp XSTS, X start_timestamp > %(sts)s',
       
   504                 {'sts': self.entity.start_timestamp,
       
   505                  'e': self.entity.eid,
       
   506                  's': self.entity.cwsource.eid})
       
   507             if rset:
       
   508                 return rset.get_entity(0, 0)
       
   509 
       
   510     def previous_entity(self):
       
   511         if self.entity.start_timestamp is not None:
       
   512             # add NOT X eid %(e)s because < may not be enough
       
   513             rset = self._cw.execute(
       
   514                 'Any X,XSTS ORDERBY 2 DESC LIMIT 1 WHERE X is CWDataImport, '
       
   515                 'X cw_import_of S, S eid %(s)s, NOT X eid %(e)s, '
       
   516                 'X start_timestamp XSTS, X start_timestamp < %(sts)s',
       
   517                 {'sts': self.entity.start_timestamp,
       
   518                  'e': self.entity.eid,
       
   519                  's': self.entity.cwsource.eid})
       
   520             if rset:
       
   521                 return rset.get_entity(0, 0)
       
   522 
       
   523 class CWDataImportStatusFacet(facet.AttributeFacet):
       
   524     __regid__ = 'datafeed.dataimport.status'
       
   525     __select__ = is_instance('CWDataImport')
       
   526     rtype = 'status'
   268 
   527 
   269 
   528 
   270 # breadcrumbs configuration ####################################################
   529 # breadcrumbs configuration ####################################################
   271 
   530 
   272 class CWsourceConfigIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
   531 class CWsourceConfigIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
   273     __select__ = is_instance('CWSourceHostConfig', 'CWSourceSchemaConfig')
   532     __select__ = is_instance('CWSourceHostConfig', 'CWSourceSchemaConfig')
   274     def parent_entity(self):
   533     def parent_entity(self):
   275         return self.entity.cwsource
   534         return self.entity.cwsource
       
   535 
       
   536 class CWDataImportIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter):
       
   537     __select__ = is_instance('CWDataImport')
       
   538     def parent_entity(self):
       
   539         return self.entity.cw_import_of[0]