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 |
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';">¶</a>''' |
|
414 u' %(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' ') |
|
428 |
|
429 class LineRenderer(pyviews.PyValTableColRenderer): |
|
430 def render_cell(self, w, rownum): |
|
431 line = self.data[rownum][2] |
|
432 w(line or u' ') |
|
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] |