20 """ |
20 """ |
21 |
21 |
22 __docformat__ = "restructuredtext en" |
22 __docformat__ = "restructuredtext en" |
23 _ = unicode |
23 _ = unicode |
24 |
24 |
|
25 import logging |
25 from itertools import repeat, chain |
26 from itertools import repeat, chain |
26 |
27 from logilab.mtconverter import xml_escape |
27 from cubicweb import Unauthorized |
28 from logilab.common.decorators import cachedproperty |
28 from cubicweb.selectors import is_instance, score_entity, match_user_groups |
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, tableview, 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 |
42 _afs.tag_object_of(('*', 'cw_for_source', 'CWSource'), 'main', 'hidden') |
50 _afs.tag_object_of(('*', 'cw_for_source', 'CWSource'), 'main', 'hidden') |
43 |
51 |
44 _affk = uicfg.autoform_field_kwargs |
52 _affk = uicfg.autoform_field_kwargs |
270 |
302 |
271 |
303 |
272 class CWSourcesTable(tableview.EntityTableView): |
304 class CWSourcesTable(tableview.EntityTableView): |
273 __regid__ = 'cw.sources-table' |
305 __regid__ = 'cw.sources-table' |
274 __select__ = is_instance('CWSource') |
306 __select__ = is_instance('CWSource') |
275 columns = ['source', 'type', 'parser', 'latest_retrieval'] |
307 columns = ['source', 'type', 'parser', 'latest_retrieval', 'latest_import'] |
276 |
308 |
277 # @tableview.etable_header_title('CWSource_plural', addcount=True) |
309 class LatestImportColRenderer(tableview.EntityTableColRenderer): |
278 # @tableview.etable_entity_sortvalue() |
310 def render_cell(self, w, rownum): |
279 # def source_cell(self, w, entity): |
311 entity = self.entity(rownum) |
280 # w(entity.view('incontext')) |
312 rset = self._cw.execute('Any X,XS,XST ORDERBY XST DESC LIMIT 1 WHERE ' |
|
313 'X cw_import_of S, S eid %(s)s, X status XS, ' |
|
314 'X start_timestamp XST', {'s': entity.eid}) |
|
315 if rset: |
|
316 self._cw.view('incontext', rset, row=0, w=w) |
|
317 else: |
|
318 w(self.empty_cell_content) |
|
319 |
|
320 column_renderers = { |
|
321 'source': tableview.MainEntityColRenderer(), |
|
322 'latest_import': LatestImportColRenderer(header=_('latest import'), |
|
323 sortable=False) |
|
324 } |
|
325 |
|
326 # datafeed source import ####################################################### |
|
327 |
|
328 REVERSE_SEVERITIES = { |
|
329 logging.DEBUG : _('DEBUG'), |
|
330 logging.INFO : _('INFO'), |
|
331 logging.WARNING : _('WARNING'), |
|
332 logging.ERROR : _('ERROR'), |
|
333 logging.FATAL : _('FATAL') |
|
334 } |
|
335 |
|
336 |
|
337 def log_to_table(req, rawdata): |
|
338 data = [] |
|
339 for msg_idx, msg in enumerate(rawdata.split('<br/>')): |
|
340 record = msg.strip() |
|
341 if not record: |
|
342 continue |
|
343 try: |
|
344 severity, url, line, msg = record.split('\t', 3) |
|
345 except ValueError: |
|
346 req.warning('badly formated log %s' % record) |
|
347 url = line = u'' |
|
348 severity = logging.DEBUG |
|
349 msg = record |
|
350 data.append( (severity, url, line, msg) ) |
|
351 return data |
|
352 |
|
353 |
|
354 class LogTableLayout(tableview.TableLayout): |
|
355 __select__ = match_view('cw.log.table') |
|
356 needs_js = tableview.TableLayout.needs_js + ('cubicweb.log.js',) |
|
357 needs_css = tableview.TableLayout.needs_css + ('cubicweb.log.css',) |
|
358 columns_css = { |
|
359 0: 'logSeverity', |
|
360 1: 'logPath', |
|
361 2: 'logLine', |
|
362 3: 'logMsg', |
|
363 } |
|
364 |
|
365 def render_table(self, w, actions, paginate): |
|
366 default_level = self.view.cw_extra_kwargs['default_level'] |
|
367 if default_level != 'Debug': |
|
368 self._cw.add_onload('$("select.logFilter").val("%s").change();' |
|
369 % self._cw.form.get('logLevel', default_level)) |
|
370 w(u'\n<form action="#"><fieldset>') |
|
371 w(u'<label>%s</label>' % self._cw._(u'Message threshold')) |
|
372 w(u'<select class="log_filter" onchange="filterLog(\'%s\', this.options[this.selectedIndex].value)">' |
|
373 % self.view.domid) |
|
374 for level in ('Debug', 'Info', 'Warning', 'Error', 'Fatal'): |
|
375 w('<option value="%s">%s</option>' % (level, self._cw._(level))) |
|
376 w(u'</select>') |
|
377 w(u'</fieldset></form>') |
|
378 super(LogTableLayout, self).render_table(w, actions, paginate) |
|
379 |
|
380 def table_attributes(self): |
|
381 attrs = super(LogTableLayout, self).table_attributes() |
|
382 attrs['id'] = 'table'+self.view.domid |
|
383 return attrs |
|
384 |
|
385 def row_attributes(self, rownum): |
|
386 attrs = super(LogTableLayout, self).row_attributes(rownum) |
|
387 attrs['id'] = 'log_msg_%i' % rownum |
|
388 severityname = REVERSE_SEVERITIES[int(self.view.pyvalue[rownum][0])] |
|
389 attrs['class'] = 'log%s' % severityname.capitalize() |
|
390 return attrs |
|
391 |
|
392 def cell_attributes(self, rownum, colnum, colid): |
|
393 attrs = super(LogTableLayout, self).cell_attributes(rownum, colnum, colid) |
|
394 attrs['class'] = self.columns_css[colnum] |
|
395 return attrs |
|
396 |
|
397 |
|
398 class LogTable(pyviews.PyValTableView): |
|
399 __regid__ = 'cw.log.table' |
|
400 headers = [_('severity'), _('url'), _('line'), _('message')] |
|
401 |
|
402 @cachedproperty |
|
403 def domid(self): |
|
404 return make_uid('logTable') |
|
405 |
|
406 class SeverityRenderer(pyviews.PyValTableColRenderer): |
|
407 def render_cell(self, w, rownum): |
|
408 severity = self.data[rownum][0] |
|
409 w(u'<a class="internallink" href="javascript:;" title="%(title)s" ' |
|
410 u'''onclick="document.location.hash='%(msg_id)s';">¶</a>''' |
|
411 u' %(severity)s' % { |
|
412 'severity': self._cw._(REVERSE_SEVERITIES[int(severity)]), |
|
413 'title': self._cw._('permalink to this message'), |
|
414 'msg_id': 'log_msg_%i' % rownum, |
|
415 }) |
|
416 def sortvalue(self, rownum): |
|
417 return int(self.data[rownum][0]) |
|
418 |
|
419 class URLRenderer(pyviews.PyValTableColRenderer): |
|
420 def render_cell(self, w, rownum): |
|
421 url = self.data[rownum][1] |
|
422 w(url and tags.a(url, href=url) or u' ') |
|
423 |
|
424 class LineRenderer(pyviews.PyValTableColRenderer): |
|
425 def render_cell(self, w, rownum): |
|
426 line = self.data[rownum][2] |
|
427 w(line or u' ') |
|
428 |
|
429 class MessageRenderer(pyviews.PyValTableColRenderer): |
|
430 snip_over = 7 |
|
431 def render_cell(self, w, rownum): |
|
432 msg = self.data[rownum][3] |
|
433 lines = msg.splitlines() |
|
434 if len(lines) <= self.snip_over: |
|
435 w(u'<pre class="rawtext">%s</pre>' % msg) |
|
436 else: |
|
437 # The make_uid argument has no specific meaning here. |
|
438 div_snip_id = make_uid(u'log_snip_') |
|
439 div_full_id = make_uid(u'log_full_') |
|
440 divs_id = (div_snip_id, div_full_id) |
|
441 snip = u'\n'.join((lines[0], lines[1], |
|
442 u' ...', |
|
443 u' %i more lines [double click to expand]' % (len(lines)-4), |
|
444 u' ...', |
|
445 lines[-2], lines[-1])) |
|
446 divs = ( |
|
447 (div_snip_id, snip, u'expand', "class='collapsed'"), |
|
448 (div_full_id, msg, u'collapse', "class='hidden'") |
|
449 ) |
|
450 for div_id, content, button, h_class in divs: |
|
451 text = self._cw._(button) |
|
452 js = u"toggleVisibility('%s'); toggleVisibility('%s');" % divs_id |
|
453 w(u'<div id="%s" %s>' % (div_id, h_class)) |
|
454 w(u'<pre class="raw_test" ondblclick="javascript: %s" ' |
|
455 u'title="%s" style="display: block;">' % (js, text)) |
|
456 w(content) |
|
457 w(u'</pre>') |
|
458 w(u'</div>') |
|
459 |
|
460 column_renderers = {0: SeverityRenderer(), |
|
461 1: URLRenderer(), |
|
462 2: LineRenderer(), |
|
463 3: MessageRenderer(), |
|
464 } |
|
465 |
|
466 |
|
467 class DataFeedSourceDataImport(EntityView): |
|
468 __select__ = EntityView.__select__ & match_kwargs('rtype') |
|
469 __regid__ = 'cw.formated_log' |
|
470 |
|
471 def cell_call(self, row, col, rtype, loglevel='Info', **kwargs): |
|
472 if 'dispctrl' in self.cw_extra_kwargs: |
|
473 loglevel = self.cw_extra_kwargs['dispctrl'].get('loglevel', loglevel) |
|
474 entity = self.cw_rset.get_entity(row, col) |
|
475 value = getattr(entity, rtype) |
|
476 if value: |
|
477 self._cw.view('cw.log.table', pyvalue=log_to_table(self._cw, value), |
|
478 default_level=loglevel, w=self.w) |
|
479 else: |
|
480 self.w(self._cw._('no log to display')) |
|
481 |
|
482 |
|
483 _pvs.tag_attribute(('CWDataImport', 'log'), 'relations') |
|
484 _pvdc.tag_attribute(('CWDataImport', 'log'), {'vid': 'cw.formated_log'}) |
|
485 _pvs.tag_subject_of(('CWDataImport', 'cw_import_of', '*'), 'hidden') # in breadcrumbs |
|
486 _pvs.tag_object_of(('*', 'cw_import_of', 'CWSource'), 'hidden') # in dedicated tab |
|
487 |
|
488 |
|
489 class CWDataImportIPrevNextAdapter(navigation.IPrevNextAdapter): |
|
490 __select__ = is_instance('CWDataImport') |
|
491 |
|
492 def next_entity(self): |
|
493 if self.entity.start_timestamp is not None: |
|
494 # add NOT X eid %(e)s because > may not be enough |
|
495 rset = self._cw.execute( |
|
496 'Any X,XSTS ORDERBY 2 LIMIT 1 WHERE X is CWDataImport, ' |
|
497 'X cw_import_of S, S eid %(s)s, NOT X eid %(e)s, ' |
|
498 'X start_timestamp XSTS, X start_timestamp > %(sts)s', |
|
499 {'sts': self.entity.start_timestamp, |
|
500 'e': self.entity.eid, |
|
501 's': self.entity.cwsource.eid}) |
|
502 if rset: |
|
503 return rset.get_entity(0, 0) |
|
504 |
|
505 def previous_entity(self): |
|
506 if self.entity.start_timestamp is not None: |
|
507 # add NOT X eid %(e)s because < may not be enough |
|
508 rset = self._cw.execute( |
|
509 'Any X,XSTS ORDERBY 2 DESC LIMIT 1 WHERE X is CWDataImport, ' |
|
510 'X cw_import_of S, S eid %(s)s, NOT X eid %(e)s, ' |
|
511 'X start_timestamp XSTS, X start_timestamp < %(sts)s', |
|
512 {'sts': self.entity.start_timestamp, |
|
513 'e': self.entity.eid, |
|
514 's': self.entity.cwsource.eid}) |
|
515 if rset: |
|
516 return rset.get_entity(0, 0) |
|
517 |
|
518 class CWDataImportStatusFacet(facet.AttributeFacet): |
|
519 __regid__ = 'datafeed.dataimport.status' |
|
520 __select__ = is_instance('CWDataImport') |
|
521 rtype = 'status' |
281 |
522 |
282 |
523 |
283 # breadcrumbs configuration #################################################### |
524 # breadcrumbs configuration #################################################### |
284 |
525 |
285 class CWsourceConfigIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter): |
526 class CWsourceConfigIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter): |
286 __select__ = is_instance('CWSourceHostConfig', 'CWSourceSchemaConfig') |
527 __select__ = is_instance('CWSourceHostConfig', 'CWSourceSchemaConfig') |
287 def parent_entity(self): |
528 def parent_entity(self): |
288 return self.entity.cwsource |
529 return self.entity.cwsource |
|
530 |
|
531 class CWDataImportIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter): |
|
532 __select__ = is_instance('CWDataImport') |
|
533 def parent_entity(self): |
|
534 return self.entity.cw_import_of[0] |