Improve status tracking for upgrades; add package version diff

This commit is contained in:
Lance Edgar 2017-08-07 22:23:07 -05:00
parent 430a1416c6
commit e14b5a89c3
7 changed files with 209 additions and 11 deletions

71
tailbone/diffs.py Normal file
View file

@ -0,0 +1,71 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2017 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# Rattail 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 General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Tools for displaying data diffs
"""
from __future__ import unicode_literals, absolute_import
from pyramid.renderers import render
from webhelpers2.html import HTML
class Diff(object):
"""
Core diff class. In sore need of documentation.
"""
def __init__(self, old_data, new_data, columns=None, fields=None, render_value=None):
self.old_data = old_data
self.new_data = new_data
self.columns = columns or ["field name", "old value", "new value"]
self.fields = fields or self.make_fields()
self.render_value = render_value or self.render_value_default
def make_fields(self):
return sorted(set(self.old_data) | set(self.new_data), key=lambda x: x.lower())
def old_value(self, field):
return self.old_data[field]
def new_value(self, field):
return self.new_data[field]
def values_differ(self, field):
return self.new_value(field) != self.old_value(field)
def render_html(self, template='/diff.mako', **kwargs):
context = kwargs
context['diff'] = self
return HTML.literal(render(template, context))
def render_value_default(self, field, value):
return repr(value)
def render_old_value(self, field):
value = self.old_value(field)
return self.render_value(field, value)
def render_new_value(self, field):
value = self.new_value(field)
return self.render_value(field, value)

View file

@ -175,7 +175,7 @@ class Form(object):
""" """
def __init__(self, fields=None, schema=None, request=None, readonly=False, readonly_fields=[], def __init__(self, fields=None, schema=None, request=None, readonly=False, readonly_fields=[],
model_instance=None, model_class=None, labels={}, renderers={}, widgets={}, model_instance=None, model_class=None, enums={}, labels={}, renderers={}, widgets={},
action_url=None, cancel_url=None): action_url=None, cancel_url=None):
self.fields = list(fields) if fields is not None else None self.fields = list(fields) if fields is not None else None
@ -189,6 +189,7 @@ class Form(object):
self.model_class = type(self.model_instance) self.model_class = type(self.model_instance)
if self.model_class and self.fields is None: if self.model_class and self.fields is None:
self.fields = self.make_fields() self.fields = self.make_fields()
self.enums = enums or {}
self.labels = labels or {} self.labels = labels or {}
self.renderers = renderers or {} self.renderers = renderers or {}
self.widgets = widgets or {} self.widgets = widgets or {}
@ -280,12 +281,21 @@ class Form(object):
self.set_renderer(key, self.render_datetime) self.set_renderer(key, self.render_datetime)
elif type_ == 'boolean': elif type_ == 'boolean':
self.set_renderer(key, self.render_boolean) self.set_renderer(key, self.render_boolean)
elif type_ == 'enum':
self.set_renderer(key, self.render_enum)
elif type_ == 'codeblock': elif type_ == 'codeblock':
self.set_renderer(key, self.render_codeblock) self.set_renderer(key, self.render_codeblock)
self.set_widget(key, dfwidget.TextAreaWidget(cols=80, rows=8)) self.set_widget(key, dfwidget.TextAreaWidget(cols=80, rows=8))
else: else:
raise ValueError("unknown type for '{}' field: {}".format(key, type_)) raise ValueError("unknown type for '{}' field: {}".format(key, type_))
def set_enum(self, key, enum):
if enum:
self.enums[key] = enum
self.set_type(key, 'enum')
else:
self.enums.pop(key, None)
def set_renderer(self, key, renderer): def set_renderer(self, key, renderer):
self.renderers[key] = renderer self.renderers[key] = renderer
@ -372,6 +382,15 @@ class Form(object):
value = self.obtain_value(record, field_name) value = self.obtain_value(record, field_name)
return pretty_boolean(value) return pretty_boolean(value)
def render_enum(self, record, field_name):
value = self.obtain_value(record, field_name)
if value is None:
return ""
enum = self.enums.get(field_name)
if enum and value in enum:
return six.text_type(enum[value])
return six.text_type(value)
def render_codeblock(self, record, field_name): def render_codeblock(self, record, field_name):
value = self.obtain_value(record, field_name) value = self.obtain_value(record, field_name)
if value is None: if value is None:

View file

@ -0,0 +1,31 @@
table.diff {
background-color: White;
border-collapse: collapse;
border-left: 1px solid Black;
border-top: 1px solid Black;
}
table.diff th,
table.diff td {
border-bottom: 1px solid Black;
border-right: 1px solid Black;
}
table.diff td {
padding: 2px 5px;
}
table.diff td.old-value,
table.diff td.new-value{
font-family: monospace;
white-space: pre;
}
table.diff tr.diff td.new-value {
background-color: #cfc;
}
table.diff tr.diff td.old-value {
background-color: #fcc;
}

View file

@ -149,6 +149,7 @@
${h.stylesheet_link(request.static_url('tailbone:static/css/grids.css'))} ${h.stylesheet_link(request.static_url('tailbone:static/css/grids.css'))}
${h.stylesheet_link(request.static_url('tailbone:static/css/filters.css'))} ${h.stylesheet_link(request.static_url('tailbone:static/css/filters.css'))}
${h.stylesheet_link(request.static_url('tailbone:static/css/forms.css'))} ${h.stylesheet_link(request.static_url('tailbone:static/css/forms.css'))}
${h.stylesheet_link(request.static_url('tailbone:static/css/diffs.css'))}
</%def> </%def>
<%def name="jquery_smoothness_theme()"> <%def name="jquery_smoothness_theme()">

View file

@ -0,0 +1,19 @@
## -*- coding: utf-8; -*-
<table class="diff">
<thead>
<tr>
% for column in diff.columns:
<th>${column}</th>
% endfor
</tr>
</thead>
<tbody>
% for field in diff.fields:
<tr${' class="diff"' if diff.values_differ(field) else ''|n}>
<td class="field">${field}</td>
<td class="old-value">${diff.render_old_value(field)}</td>
<td class="new-value">${diff.render_new_value(field)}</td>
</tr>
% endfor
</tbody>
</table>

View file

@ -47,7 +47,7 @@ from pyramid.renderers import get_renderer, render_to_response, render
from pyramid.response import FileResponse from pyramid.response import FileResponse
from webhelpers2.html import HTML, tags from webhelpers2.html import HTML, tags
from tailbone import forms, grids from tailbone import forms, grids, diffs
from tailbone.views import View from tailbone.views import View
from tailbone.progress import SessionProgress from tailbone.progress import SessionProgress
@ -1685,6 +1685,9 @@ class MasterView(View):
# TODO: make this smarter? # TODO: make this smarter?
return {'uuid': row.uuid} return {'uuid': row.uuid}
def make_diff(self, old_data, new_data, **kwargs):
return diffs.Diff(old_data, new_data, **kwargs)
############################## ##############################
# Config Stuff # Config Stuff
############################## ##############################

View file

@ -27,7 +27,11 @@ Views for app upgrades
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
import os import os
import re
import six
from pip.download import PipSession
from pip.req import parse_requirements
from sqlalchemy import orm from sqlalchemy import orm
from rattail.db import model, Session as RattailSession from rattail.db import model, Session as RattailSession
@ -49,13 +53,19 @@ class UpgradeView(MasterView):
model_class = model.Upgrade model_class = model.Upgrade
executable = True executable = True
downloadable = True downloadable = True
labels = {
'executed_by': "Executed by",
'status_code': "Status",
'stdout_file': "STDOUT",
'stderr_file': "STDERR",
}
grid_columns = [ grid_columns = [
'created', 'created',
'description', 'description',
# 'not_until', # 'not_until',
'enabled', 'enabled',
'executing', 'status_code',
'executed', 'executed',
'executed_by', 'executed_by',
] ]
@ -68,11 +78,12 @@ class UpgradeView(MasterView):
'created', 'created',
'created_by', 'created_by',
'enabled', 'enabled',
'executing',
'executed', 'executed',
'executed_by', 'executed_by',
'status_code',
'stdout_file', 'stdout_file',
'stderr_file', 'stderr_file',
'package_diff',
] ]
def __init__(self, request): def __init__(self, request):
@ -95,11 +106,11 @@ class UpgradeView(MasterView):
super(UpgradeView, self).configure_grid(g) super(UpgradeView, self).configure_grid(g)
g.set_joiner('executed_by', lambda q: q.join(model.User).outerjoin(model.Person)) g.set_joiner('executed_by', lambda q: q.join(model.User).outerjoin(model.Person))
g.set_sorter('executed_by', model.Person.display_name) g.set_sorter('executed_by', model.Person.display_name)
g.set_enum('status_code', self.enum.UPGRADE_STATUS)
g.set_type('created', 'datetime') g.set_type('created', 'datetime')
g.set_type('executed', 'datetime') g.set_type('executed', 'datetime')
g.default_sortkey = 'created' g.default_sortkey = 'created'
g.default_sortdir = 'desc' g.default_sortdir = 'desc'
g.set_label('executed_by', "Executed by")
g.set_link('created') g.set_link('created')
g.set_link('description') g.set_link('description')
# g.set_link('not_until') # g.set_link('not_until')
@ -107,29 +118,28 @@ class UpgradeView(MasterView):
def configure_form(self, f): def configure_form(self, f):
super(UpgradeView, self).configure_form(f) super(UpgradeView, self).configure_form(f)
f.set_enum('status_code', self.enum.UPGRADE_STATUS)
f.set_type('created', 'datetime') f.set_type('created', 'datetime')
f.set_type('enabled', 'boolean') f.set_type('enabled', 'boolean')
f.set_type('executing', 'boolean')
f.set_type('executed', 'datetime') f.set_type('executed', 'datetime')
# f.set_widget('not_until', dfwidget.DateInputWidget()) # f.set_widget('not_until', dfwidget.DateInputWidget())
f.set_widget('notes', dfwidget.TextAreaWidget(cols=80, rows=8)) f.set_widget('notes', dfwidget.TextAreaWidget(cols=80, rows=8))
f.set_renderer('stdout_file', self.render_stdout_file) f.set_renderer('stdout_file', self.render_stdout_file)
f.set_renderer('stderr_file', self.render_stdout_file) f.set_renderer('stderr_file', self.render_stdout_file)
f.set_renderer('package_diff', self.render_package_diff)
# f.set_readonly('created') # f.set_readonly('created')
# f.set_readonly('created_by') # f.set_readonly('created_by')
f.set_readonly('executing')
f.set_readonly('executed') f.set_readonly('executed')
f.set_readonly('executed_by') f.set_readonly('executed_by')
f.set_label('stdout_file', "STDOUT")
f.set_label('stderr_file', "STDERR")
upgrade = f.model_instance upgrade = f.model_instance
if self.creating or self.editing: if self.creating or self.editing:
f.remove_field('created') f.remove_field('created')
f.remove_field('created_by') f.remove_field('created_by')
f.remove_field('stdout_file') f.remove_field('stdout_file')
f.remove_field('stderr_file') f.remove_field('stderr_file')
if self.creating:
f.remove_field('status_code')
if self.creating or not upgrade.executed: if self.creating or not upgrade.executed:
f.remove_field('executing')
f.remove_field('executed') f.remove_field('executed')
f.remove_field('executed_by') f.remove_field('executed_by')
if self.editing and upgrade.executed: if self.editing and upgrade.executed:
@ -137,7 +147,6 @@ class UpgradeView(MasterView):
elif f.model_instance.executed: elif f.model_instance.executed:
f.remove_field('enabled') f.remove_field('enabled')
f.remove_field('executing')
else: else:
f.remove_field('executed') f.remove_field('executed')
@ -145,6 +154,9 @@ class UpgradeView(MasterView):
f.remove_field('stdout_file') f.remove_field('stdout_file')
f.remove_field('stderr_file') f.remove_field('stderr_file')
if not self.viewing or not upgrade.executed:
f.remove_field('package_diff')
def render_stdout_file(self, upgrade, fieldname): def render_stdout_file(self, upgrade, fieldname):
if fieldname.startswith('stderr'): if fieldname.startswith('stderr'):
filename = 'stderr.log' filename = 'stderr.log'
@ -157,6 +169,45 @@ class UpgradeView(MasterView):
return tags.link_to(content, url) return tags.link_to(content, url)
return filename return filename
def render_package_diff(self, upgrade, fieldname):
try:
before = self.parse_requirements(upgrade, 'before')
after = self.parse_requirements(upgrade, 'after')
diff = self.make_diff(before, after,
columns=["package", "old version", "new version"],
render_value=self.render_diff_value,
)
return diff.render_html()
except:
return "(not available for this upgrade)"
def render_diff_value(self, field, value):
if value.startswith("u'") and value.endswith("'"):
return value[2:1]
return value
def parse_requirements(self, upgrade, type_):
packages = {}
path = self.rattail_config.upgrade_filepath(upgrade.uuid, filename='requirements.{}.txt'.format(type_))
session = PipSession()
for req in parse_requirements(path, session=session):
version = self.version_from_requirement(req)
packages[req.name] = version
return packages
def version_from_requirement(self, req):
if req.specifier:
match = re.match(r'^==(.*)$', six.text_type(req.specifier))
if match:
return match.group(1)
return six.text_type(req.specifier)
elif req.link:
match = re.match(r'^.*@(.*)#egg=.*$', six.text_type(req.link))
if match:
return match.group(1)
return six.text_type(req.link)
return ""
def get_size(self, path): def get_size(self, path):
try: try:
return os.path.getsize(path) return os.path.getsize(path)
@ -182,6 +233,7 @@ class UpgradeView(MasterView):
def before_create_flush(self, upgrade): def before_create_flush(self, upgrade):
upgrade.created_by = self.request.user upgrade.created_by = self.request.user
upgrade.status_code = self.enum.UPGRADE_STATUS_PENDING
# TODO: this was an attempt to make the progress bar survive Apache restart, # TODO: this was an attempt to make the progress bar survive Apache restart,
# but it didn't work... need to "fork" instead of waiting for execution? # but it didn't work... need to "fork" instead of waiting for execution?
@ -192,9 +244,11 @@ class UpgradeView(MasterView):
def execute_instance(self, upgrade, user, **kwargs): def execute_instance(self, upgrade, user, **kwargs):
session = orm.object_session(upgrade) session = orm.object_session(upgrade)
upgrade.executing = True upgrade.executing = True
upgrade.status_code = self.enum.UPGRADE_STATUS_EXECUTING
session.commit() session.commit()
self.handler.execute(upgrade, user, **kwargs) self.handler.execute(upgrade, user, **kwargs)
upgrade.executing = False upgrade.executing = False
upgrade.status_code = self.enum.UPGRADE_STATUS_SUCCEEDED
upgrade.executed = make_utc() upgrade.executed = make_utc()
upgrade.executed_by = user upgrade.executed_by = user