Add websockets progress, "multi-system" support for upgrades
and related things to better support that
This commit is contained in:
parent
db3f215ebe
commit
18cec49a86
|
@ -195,22 +195,20 @@ def add_websocket(config, name, view, attr=None):
|
||||||
view_callable = rattail_app.load_object(view)
|
view_callable = rattail_app.load_object(view)
|
||||||
else:
|
else:
|
||||||
view_callable = view
|
view_callable = view
|
||||||
view_callable = view_callable(config.registry)
|
view_callable = view_callable(config)
|
||||||
if attr:
|
if attr:
|
||||||
view_callable = getattr(view_callable, attr)
|
view_callable = getattr(view_callable, attr)
|
||||||
|
|
||||||
path = '/ws/{}'.format(name)
|
|
||||||
|
|
||||||
# register route
|
# register route
|
||||||
config.add_route('ws.{}'.format(name),
|
path = '/ws/{}'.format(name)
|
||||||
path,
|
route_name = 'ws.{}'.format(name)
|
||||||
static=True)
|
config.add_route(route_name, path, static=True)
|
||||||
|
|
||||||
# register view callable
|
# register view callable
|
||||||
websockets = config.registry.setdefault('tailbone_websockets', {})
|
websockets = config.registry.setdefault('tailbone_websockets', {})
|
||||||
websockets[path] = view_callable
|
websockets[path] = view_callable
|
||||||
|
|
||||||
config.action('tailbone-add-websocket', action,
|
config.action('tailbone-add-websocket-{}'.format(name), action,
|
||||||
# nb. since this action adds routes, it must happen
|
# nb. since this action adds routes, it must happen
|
||||||
# sooner in the order than it normally would, hence
|
# sooner in the order than it normally would, hence
|
||||||
# we declare that
|
# we declare that
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2018 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -27,22 +27,33 @@ Progress Indicator
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import warnings
|
||||||
|
|
||||||
from rattail.progress import ProgressBase
|
from rattail.progress import ProgressBase
|
||||||
|
|
||||||
from beaker.session import Session
|
from beaker.session import Session
|
||||||
|
|
||||||
|
|
||||||
|
def get_basic_session(config, request={}, **kwargs):
|
||||||
|
"""
|
||||||
|
Create/get a "basic" Beaker session object.
|
||||||
|
"""
|
||||||
|
kwargs['use_cookies'] = False
|
||||||
|
session = Session(request, **kwargs)
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
def get_progress_session(request, key, **kwargs):
|
def get_progress_session(request, key, **kwargs):
|
||||||
"""
|
"""
|
||||||
Create/get a Beaker session object, to be used for progress.
|
Create/get a Beaker session object, to be used for progress.
|
||||||
"""
|
"""
|
||||||
id = '{}.progress.{}'.format(request.session.id, key)
|
kwargs['id'] = '{}.progress.{}'.format(request.session.id, key)
|
||||||
kwargs['use_cookies'] = False
|
|
||||||
if kwargs.get('type') == 'file':
|
if kwargs.get('type') == 'file':
|
||||||
|
warnings.warn("Passing a 'type' kwarg to get_progress_session() "
|
||||||
|
"is deprecated...i think",
|
||||||
|
DeprecationWarning, stacklevel=2)
|
||||||
kwargs['data_dir'] = os.path.join(request.rattail_config.appdir(), 'sessions')
|
kwargs['data_dir'] = os.path.join(request.rattail_config.appdir(), 'sessions')
|
||||||
session = Session(request, id, **kwargs)
|
return get_basic_session(request.rattail_config, request, **kwargs)
|
||||||
return session
|
|
||||||
|
|
||||||
|
|
||||||
class SessionProgress(ProgressBase):
|
class SessionProgress(ProgressBase):
|
||||||
|
@ -52,11 +63,20 @@ class SessionProgress(ProgressBase):
|
||||||
This class is only responsible for keeping the progress *data* current. It
|
This class is only responsible for keeping the progress *data* current. It
|
||||||
is the responsibility of some client-side AJAX (etc.) to consume the data
|
is the responsibility of some client-side AJAX (etc.) to consume the data
|
||||||
for display to the user.
|
for display to the user.
|
||||||
|
|
||||||
|
:param ws: If true, then websockets are assumed, and the progress will
|
||||||
|
behave accordingly. The default is false, "traditional" behavior.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, request, key, session_type=None):
|
def __init__(self, request, key, session_type=None, ws=False):
|
||||||
self.key = key
|
self.key = key
|
||||||
self.session = get_progress_session(request, key, type=session_type)
|
self.ws = ws
|
||||||
|
|
||||||
|
if self.ws:
|
||||||
|
self.session = get_basic_session(request.rattail_config, id=key)
|
||||||
|
else:
|
||||||
|
self.session = get_progress_session(request, key, type=session_type)
|
||||||
|
|
||||||
self.canceled = False
|
self.canceled = False
|
||||||
self.clear()
|
self.clear()
|
||||||
|
|
||||||
|
|
|
@ -73,6 +73,7 @@
|
||||||
|
|
||||||
let ${form.component_studly} = {
|
let ${form.component_studly} = {
|
||||||
template: '#${form.component}-template',
|
template: '#${form.component}-template',
|
||||||
|
mixins: [FormPosterMixin],
|
||||||
components: {},
|
components: {},
|
||||||
props: {},
|
props: {},
|
||||||
watch: {},
|
watch: {},
|
||||||
|
|
|
@ -682,20 +682,54 @@
|
||||||
% if show_prev_next is not Undefined and show_prev_next:
|
% if show_prev_next is not Undefined and show_prev_next:
|
||||||
% if prev_url:
|
% if prev_url:
|
||||||
<div class="level-item">
|
<div class="level-item">
|
||||||
${h.link_to(u"« Older", prev_url, class_='button autodisable')}
|
% if use_buefy:
|
||||||
|
<b-button tag="a" href="${prev_url}"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="arrow-left">
|
||||||
|
Older
|
||||||
|
</b-button>
|
||||||
|
% else:
|
||||||
|
${h.link_to(u"« Older", prev_url, class_='button autodisable')}
|
||||||
|
% endif
|
||||||
</div>
|
</div>
|
||||||
% else:
|
% else:
|
||||||
<div class="level-item">
|
<div class="level-item">
|
||||||
${h.link_to(u"« Older", '#', class_='button', disabled='disabled')}
|
% if use_buefy:
|
||||||
|
<b-button tag="a" href="#"
|
||||||
|
disabled
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="arrow-left">
|
||||||
|
Older
|
||||||
|
</b-button>
|
||||||
|
% else:
|
||||||
|
${h.link_to(u"« Older", '#', class_='button', disabled='disabled')}
|
||||||
|
% endif
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
% if next_url:
|
% if next_url:
|
||||||
<div class="level-item">
|
<div class="level-item">
|
||||||
${h.link_to(u"Newer »", next_url, class_='button autodisable')}
|
% if use_buefy:
|
||||||
|
<b-button tag="a" href="${next_url}"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="arrow-right">
|
||||||
|
Newer
|
||||||
|
</b-button>
|
||||||
|
% else:
|
||||||
|
${h.link_to(u"Newer »", next_url, class_='button autodisable')}
|
||||||
|
% endif
|
||||||
</div>
|
</div>
|
||||||
% else:
|
% else:
|
||||||
<div class="level-item">
|
<div class="level-item">
|
||||||
${h.link_to(u"Newer »", '#', class_='button', disabled='disabled')}
|
% if use_buefy:
|
||||||
|
<b-button tag="a" href="#"
|
||||||
|
disabled
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="arrow-right">
|
||||||
|
Newer
|
||||||
|
</b-button>
|
||||||
|
% else:
|
||||||
|
${h.link_to(u"Newer »", '#', class_='button', disabled='disabled')}
|
||||||
|
% endif
|
||||||
</div>
|
</div>
|
||||||
% endif
|
% endif
|
||||||
% endif
|
% endif
|
||||||
|
|
156
tailbone/templates/upgrades/configure.mako
Normal file
156
tailbone/templates/upgrades/configure.mako
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
## -*- coding: utf-8; -*-
|
||||||
|
<%inherit file="/configure.mako" />
|
||||||
|
|
||||||
|
<%def name="form_content()">
|
||||||
|
${h.hidden('upgrade_systems', **{':value': 'JSON.stringify(upgradeSystems)'})}
|
||||||
|
|
||||||
|
<h3 class="is-size-3">Upgradable Systems</h3>
|
||||||
|
<div class="block" style="padding-left: 2rem; display: flex;">
|
||||||
|
|
||||||
|
<b-table :data="upgradeSystems"
|
||||||
|
sortable>
|
||||||
|
<template slot-scope="props">
|
||||||
|
<b-table-column field="key"
|
||||||
|
label="Key"
|
||||||
|
sortable>
|
||||||
|
{{ props.row.key }}
|
||||||
|
</b-table-column>
|
||||||
|
<b-table-column field="label"
|
||||||
|
label="Label"
|
||||||
|
sortable>
|
||||||
|
{{ props.row.label }}
|
||||||
|
</b-table-column>
|
||||||
|
<b-table-column field="command"
|
||||||
|
label="Command"
|
||||||
|
sortable>
|
||||||
|
{{ props.row.command }}
|
||||||
|
</b-table-column>
|
||||||
|
<b-table-column label="Actions">
|
||||||
|
<a href="#"
|
||||||
|
@click.prevent="upgradeSystemEdit(props.row)">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="#"
|
||||||
|
v-if="props.row.key != 'rattail'"
|
||||||
|
class="has-text-danger"
|
||||||
|
@click.prevent="updateSystemDelete(props.row)">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
</b-table-column>
|
||||||
|
</template>
|
||||||
|
</b-table>
|
||||||
|
|
||||||
|
<div style="margin-left: 1rem;">
|
||||||
|
<b-button type="is-primary"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="plus"
|
||||||
|
@click="upgradeSystemCreate()">
|
||||||
|
New System
|
||||||
|
</b-button>
|
||||||
|
|
||||||
|
<b-modal has-modal-card
|
||||||
|
:active.sync="upgradeSystemShowDialog">
|
||||||
|
<div class="modal-card">
|
||||||
|
|
||||||
|
<header class="modal-card-head">
|
||||||
|
<p class="modal-card-title">Upgradable System</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="modal-card-body">
|
||||||
|
<b-field label="Key"
|
||||||
|
:type="upgradeSystemKey ? null : 'is-danger'">
|
||||||
|
<b-input v-model.trim="upgradeSystemKey"
|
||||||
|
ref="upgradeSystemKey"
|
||||||
|
:disabled="upgradeSystemKey == 'rattail'">
|
||||||
|
</b-input>
|
||||||
|
</b-field>
|
||||||
|
<b-field label="Label"
|
||||||
|
:type="upgradeSystemLabel ? null : 'is-danger'">
|
||||||
|
<b-input v-model.trim="upgradeSystemLabel"
|
||||||
|
ref="upgradeSystemLabel"
|
||||||
|
:disabled="upgradeSystemKey == 'rattail'">
|
||||||
|
</b-input>
|
||||||
|
</b-field>
|
||||||
|
<b-field label="Command"
|
||||||
|
:type="upgradeSystemCommand ? null : 'is-danger'">
|
||||||
|
<b-input v-model.trim="upgradeSystemCommand"
|
||||||
|
ref="upgradeSystemCommand">
|
||||||
|
</b-input>
|
||||||
|
</b-field>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class="modal-card-foot">
|
||||||
|
<b-button type="is-primary"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="save"
|
||||||
|
@click="upgradeSystemSave()"
|
||||||
|
:disabled="!upgradeSystemKey || !upgradeSystemLabel || !upgradeSystemCommand">
|
||||||
|
Save
|
||||||
|
</b-button>
|
||||||
|
<b-button @click="upgradeSystemShowDialog = false">
|
||||||
|
Cancel
|
||||||
|
</b-button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</b-modal>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="modify_this_page_vars()">
|
||||||
|
${parent.modify_this_page_vars()}
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
ThisPageData.upgradeSystems = ${json.dumps(upgrade_systems)|n}
|
||||||
|
ThisPageData.upgradeSystemShowDialog = false
|
||||||
|
ThisPageData.upgradeSystem = null
|
||||||
|
ThisPageData.upgradeSystemKey = null
|
||||||
|
ThisPageData.upgradeSystemLabel = null
|
||||||
|
ThisPageData.upgradeSystemCommand = null
|
||||||
|
|
||||||
|
ThisPage.methods.upgradeSystemCreate = function() {
|
||||||
|
this.upgradeSystem = null
|
||||||
|
this.upgradeSystemKey = null
|
||||||
|
this.upgradeSystemLabel = null
|
||||||
|
this.upgradeSystemCommand = null
|
||||||
|
this.upgradeSystemShowDialog = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.upgradeSystemKey.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ThisPage.methods.upgradeSystemEdit = function(system) {
|
||||||
|
this.upgradeSystem = system
|
||||||
|
this.upgradeSystemKey = system.key
|
||||||
|
this.upgradeSystemLabel = system.label
|
||||||
|
this.upgradeSystemCommand = system.command
|
||||||
|
this.upgradeSystemShowDialog = true
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.upgradeSystemCommand.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ThisPage.methods.upgradeSystemSave = function() {
|
||||||
|
if (this.upgradeSystem) {
|
||||||
|
this.upgradeSystem.key = this.upgradeSystemKey
|
||||||
|
this.upgradeSystem.label = this.upgradeSystemLabel
|
||||||
|
this.upgradeSystem.command = this.upgradeSystemCommand
|
||||||
|
} else {
|
||||||
|
let system = {key: this.upgradeSystemKey,
|
||||||
|
label: this.upgradeSystemLabel,
|
||||||
|
command: this.upgradeSystemCommand}
|
||||||
|
this.upgradeSystems.push(system)
|
||||||
|
}
|
||||||
|
this.upgradeSystemShowDialog = false
|
||||||
|
this.settingsNeedSaved = true
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
|
${parent.body()}
|
|
@ -38,6 +38,18 @@
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
<%def name="extra_styles()">
|
||||||
|
${parent.extra_styles()}
|
||||||
|
<style type="text/css">
|
||||||
|
.progress-with-textout {
|
||||||
|
border: 1px solid Black;
|
||||||
|
line-height: 1.2;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</%def>
|
||||||
|
|
||||||
<%def name="render_this_page()">
|
<%def name="render_this_page()">
|
||||||
${parent.render_this_page()}
|
${parent.render_this_page()}
|
||||||
|
|
||||||
|
@ -60,31 +72,86 @@
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="render_form_buttons()">
|
<%def name="render_form_buttons()">
|
||||||
% if not instance.executed and instance.status_code == enum.UPGRADE_STATUS_PENDING and request.has_perm('{}.execute'.format(permission_prefix)):
|
% if not instance.executed and instance.status_code == enum.UPGRADE_STATUS_PENDING and master.has_perm('execute'):
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
% if instance.enabled and not instance.executing:
|
% if instance.enabled and not instance.executing:
|
||||||
% if use_buefy:
|
% if use_buefy and expose_websockets:
|
||||||
${h.form(url('{}.execute'.format(route_prefix), uuid=instance.uuid), **{'@submit': 'submitForm'})}
|
<b-button type="is-primary"
|
||||||
% else:
|
icon-pack="fas"
|
||||||
${h.form(url('{}.execute'.format(route_prefix), uuid=instance.uuid), class_='autodisable')}
|
icon-left="arrow-circle-right"
|
||||||
% endif
|
:disabled="upgradeExecuting"
|
||||||
${h.csrf_token(request)}
|
@click="executeUpgrade()">
|
||||||
% if use_buefy:
|
{{ upgradeExecuting ? "Working, please wait..." : "Execute this upgrade" }}
|
||||||
|
</b-button>
|
||||||
|
% elif use_buefy:
|
||||||
|
${h.form(url('{}.execute'.format(route_prefix), uuid=instance.uuid), **{'@submit': 'submitForm'})}
|
||||||
|
${h.csrf_token(request)}
|
||||||
<b-button type="is-primary"
|
<b-button type="is-primary"
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="arrow-circle-right"
|
||||||
:disabled="formSubmitting">
|
:disabled="formSubmitting">
|
||||||
{{ formButtonText }}
|
{{ formSubmitting ? "Working, please wait..." : "Execute this upgrade" }}
|
||||||
</b-button>
|
</b-button>
|
||||||
|
${h.end_form()}
|
||||||
% else:
|
% else:
|
||||||
|
${h.form(url('{}.execute'.format(route_prefix), uuid=instance.uuid), class_='autodisable')}
|
||||||
|
${h.csrf_token(request)}
|
||||||
${h.submit('execute', "Execute this upgrade", class_='button is-primary')}
|
${h.submit('execute', "Execute this upgrade", class_='button is-primary')}
|
||||||
|
${h.end_form()}
|
||||||
% endif
|
% endif
|
||||||
${h.end_form()}
|
|
||||||
% elif instance.enabled:
|
% elif instance.enabled:
|
||||||
<button type="button" class="button is-primary" disabled="disabled" title="This upgrade is currently executing">Execute this upgrade</button>
|
<button type="button" class="button is-primary" disabled="disabled" title="This upgrade is currently executing">Execute this upgrade</button>
|
||||||
% else:
|
% else:
|
||||||
<button type="button" class="button is-primary" disabled="disabled" title="This upgrade is not enabled">Execute this upgrade</button>
|
<button type="button" class="button is-primary" disabled="disabled" title="This upgrade is not enabled">Execute this upgrade</button>
|
||||||
% endif
|
% endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<b-modal :active.sync="upgradeExecuting"
|
||||||
|
full-screen
|
||||||
|
:can-cancel="false">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content">
|
||||||
|
|
||||||
|
<div class="level">
|
||||||
|
<div class="level-item has-text-centered"
|
||||||
|
style="display: flex; flex-direction: column;">
|
||||||
|
<p class="block">Upgrading (please wait) ...</p>
|
||||||
|
<b-progress size="is-large"
|
||||||
|
style="width: 400px;"
|
||||||
|
## :value="80"
|
||||||
|
## show-value
|
||||||
|
## format="percent"
|
||||||
|
>
|
||||||
|
</b-progress>
|
||||||
|
</div>
|
||||||
|
<div class="level-right">
|
||||||
|
<div class="level-item">
|
||||||
|
<b-button type="is-warning"
|
||||||
|
icon-pack="fas"
|
||||||
|
icon-left="sad-tear"
|
||||||
|
@click="declareFailureClick()">
|
||||||
|
Declare Failure
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container progress-with-textout is-family-monospace is-size-7"
|
||||||
|
ref="textout">
|
||||||
|
<span v-for="line in progressOutput"
|
||||||
|
:key="line.key"
|
||||||
|
v-html="line.text">
|
||||||
|
</span>
|
||||||
|
|
||||||
|
## nb. we auto-scroll down to "see" this element
|
||||||
|
<div ref="seeme"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</b-modal>
|
||||||
|
|
||||||
% endif
|
% endif
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
@ -94,16 +161,81 @@
|
||||||
|
|
||||||
TailboneFormData.showingPackages = 'diffs'
|
TailboneFormData.showingPackages = 'diffs'
|
||||||
|
|
||||||
TailboneFormData.formButtonText = "Execute this upgrade"
|
|
||||||
TailboneFormData.formSubmitting = false
|
|
||||||
|
|
||||||
TailboneForm.methods.submitForm = function() {
|
|
||||||
this.formSubmitting = true
|
|
||||||
this.formButtonText = "Working, please wait..."
|
|
||||||
}
|
|
||||||
|
|
||||||
% if master.has_perm('execute'):
|
% if master.has_perm('execute'):
|
||||||
|
|
||||||
|
% if expose_websockets:
|
||||||
|
|
||||||
|
TailboneFormData.upgradeExecuting = ${json.dumps(instance.executing)|n}
|
||||||
|
TailboneFormData.progressOutput = []
|
||||||
|
TailboneFormData.progressOutputCounter = 0
|
||||||
|
|
||||||
|
TailboneForm.methods.executeUpgrade = function() {
|
||||||
|
this.upgradeExecuting = true
|
||||||
|
|
||||||
|
// grow the textout area to fill most of screen
|
||||||
|
this.$nextTick(() => {
|
||||||
|
let textout = this.$refs.textout
|
||||||
|
let height = window.innerHeight - textout.offsetTop - 50
|
||||||
|
textout.style.height = height + 'px'
|
||||||
|
})
|
||||||
|
|
||||||
|
let url = '${master.get_action_url('execute', instance)}'
|
||||||
|
this.submitForm(url, {ws: true}, response => {
|
||||||
|
|
||||||
|
## TODO: should be a cleaner way to get this url?
|
||||||
|
url = '${request.route_url('ws.upgrades.execution_progress', _query={'uuid': instance.uuid})}'
|
||||||
|
url = url.replace(/^https?:/, 'wss:')
|
||||||
|
|
||||||
|
this.ws = new WebSocket(url)
|
||||||
|
let that = this
|
||||||
|
|
||||||
|
## TODO: add support for this here?
|
||||||
|
// this.ws.onclose = (event) => {
|
||||||
|
// // websocket closing means 1 of 2 things:
|
||||||
|
// // - user navigated away from page intentionally
|
||||||
|
// // - server connection was broken somehow
|
||||||
|
// // only one of those is "bad" and we only want to
|
||||||
|
// // display warning in 2nd case. so we simply use a
|
||||||
|
// // brief delay to "rule out" the 1st scenario
|
||||||
|
// setTimeout(() => { that.websocketBroken = true },
|
||||||
|
// 3000)
|
||||||
|
// }
|
||||||
|
|
||||||
|
this.ws.onmessage = (event) => {
|
||||||
|
let data = JSON.parse(event.data)
|
||||||
|
|
||||||
|
if (data.complete) {
|
||||||
|
|
||||||
|
// upgrade has completed; reload page to view result
|
||||||
|
location.reload()
|
||||||
|
|
||||||
|
} else if (data.stdout) {
|
||||||
|
|
||||||
|
// add lines to textout area
|
||||||
|
that.progressOutput.push({
|
||||||
|
key: ++that.progressOutputCounter,
|
||||||
|
text: data.stdout})
|
||||||
|
|
||||||
|
// scroll down to end of textout area
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.seeme.scrollIntoView()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
% else:
|
||||||
|
## no websockets
|
||||||
|
|
||||||
|
TailboneFormData.formSubmitting = false
|
||||||
|
|
||||||
|
TailboneForm.methods.submitForm = function() {
|
||||||
|
this.formSubmitting = true
|
||||||
|
}
|
||||||
|
|
||||||
|
% endif
|
||||||
|
|
||||||
TailboneFormData.declareFailureSubmitting = false
|
TailboneFormData.declareFailureSubmitting = false
|
||||||
|
|
||||||
TailboneForm.methods.declareFailureClick = function() {
|
TailboneForm.methods.declareFailureClick = function() {
|
||||||
|
|
|
@ -24,26 +24,77 @@
|
||||||
ASGI Views
|
ASGI Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
from http.cookies import SimpleCookie
|
||||||
|
|
||||||
import http.cookies
|
from beaker.session import SignedCookie
|
||||||
|
from pyramid.interfaces import ISessionFactory
|
||||||
|
|
||||||
from beaker.cache import clsmap
|
|
||||||
from beaker.session import SessionObject, SignedCookie
|
class MockRequest(dict):
|
||||||
|
"""
|
||||||
|
Fake request class, needed for re-construction of the user's web
|
||||||
|
session.
|
||||||
|
"""
|
||||||
|
environ = {}
|
||||||
|
|
||||||
|
def add_response_callback(self, func):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class WebsocketView(object):
|
class WebsocketView(object):
|
||||||
|
|
||||||
def __init__(self, registry):
|
def __init__(self, pyramid_config):
|
||||||
self.registry = registry
|
self.pyramid_config = pyramid_config
|
||||||
|
self.registry = self.pyramid_config.registry
|
||||||
|
self.model = self.rattail_config.get_model()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rattail_config(self):
|
||||||
|
return self.registry['rattail_config']
|
||||||
|
|
||||||
|
def get_rattail_app(self):
|
||||||
|
return self.rattail_config.get_app()
|
||||||
|
|
||||||
|
async def authorize(self, scope, receive, send, permission):
|
||||||
|
|
||||||
|
# is user authorized for this socket?
|
||||||
|
authorized = await self.has_permission(scope, permission)
|
||||||
|
|
||||||
|
# wait for client to connect
|
||||||
|
message = await receive()
|
||||||
|
assert message['type'] == 'websocket.connect'
|
||||||
|
|
||||||
|
# allow or deny access, per authorization
|
||||||
|
if authorized:
|
||||||
|
await send({'type': 'websocket.accept'})
|
||||||
|
else: # forbidden
|
||||||
|
await send({'type': 'websocket.close'})
|
||||||
|
|
||||||
|
return authorized
|
||||||
|
|
||||||
|
async def get_user(self, scope, session=None):
|
||||||
|
app = self.get_rattail_app()
|
||||||
|
model = self.model
|
||||||
|
|
||||||
|
# load the user's web session
|
||||||
|
user_session = await self.get_user_session(scope)
|
||||||
|
if user_session:
|
||||||
|
|
||||||
|
# determine user uuid
|
||||||
|
user_uuid = user_session.get('auth.userid')
|
||||||
|
if user_uuid:
|
||||||
|
|
||||||
|
# use given db session, or make a new one
|
||||||
|
with app.short_session(config=self.rattail_config,
|
||||||
|
session=session):
|
||||||
|
|
||||||
|
# load user proper
|
||||||
|
return session.query(model.User).get(user_uuid)
|
||||||
|
|
||||||
async def get_user_session(self, scope):
|
async def get_user_session(self, scope):
|
||||||
settings = self.registry.settings
|
settings = self.registry.settings
|
||||||
beaker_key = settings['beaker.session.key']
|
beaker_key = settings['beaker.session.key']
|
||||||
beaker_secret = settings['beaker.session.secret']
|
beaker_secret = settings['beaker.session.secret']
|
||||||
beaker_type = settings['beaker.session.type']
|
|
||||||
beaker_data_dir = settings['beaker.session.data_dir']
|
|
||||||
beaker_lock_dir = settings['beaker.session.lock_dir']
|
|
||||||
|
|
||||||
# get ahold of session identifier cookie
|
# get ahold of session identifier cookie
|
||||||
headers = dict(scope['headers'])
|
headers = dict(scope['headers'])
|
||||||
|
@ -51,20 +102,31 @@ class WebsocketView(object):
|
||||||
if not cookie:
|
if not cookie:
|
||||||
return
|
return
|
||||||
cookie = cookie.decode('utf_8')
|
cookie = cookie.decode('utf_8')
|
||||||
cookie = http.cookies.SimpleCookie(cookie)
|
cookie = SimpleCookie(cookie)
|
||||||
morsel = cookie[beaker_key]
|
morsel = cookie[beaker_key]
|
||||||
|
|
||||||
# simulate pyramid_beaker logic to get at the session
|
# simulate pyramid_beaker logic to get at the actual session
|
||||||
cookieheader = morsel.output(header='')
|
cookieheader = morsel.output(header='')
|
||||||
cookie = SignedCookie(beaker_secret, input=cookieheader)
|
cookie = SignedCookie(beaker_secret, input=cookieheader)
|
||||||
session_id = cookie[beaker_key].value
|
session_id = cookie[beaker_key].value
|
||||||
request = {'cookie': cookieheader}
|
factory = self.registry.queryUtility(ISessionFactory)
|
||||||
session = SessionObject(
|
request = MockRequest()
|
||||||
request,
|
# nb. cannot pass 'id' to our factory, but things still work
|
||||||
id=session_id,
|
# if we assign it immediately, before load() is called
|
||||||
key=beaker_key,
|
session = factory(request)
|
||||||
namespace_class=clsmap[beaker_type],
|
session.id = session_id
|
||||||
data_dir=beaker_data_dir,
|
session.load()
|
||||||
lock_dir=beaker_lock_dir)
|
|
||||||
|
|
||||||
return session
|
return session
|
||||||
|
|
||||||
|
async def has_permission(self, scope, permission):
|
||||||
|
app = self.get_rattail_app()
|
||||||
|
auth_handler = app.get_auth_handler()
|
||||||
|
|
||||||
|
# figure out if user is authorized for this websocket
|
||||||
|
session = app.make_session()
|
||||||
|
user = await self.get_user(scope, session=session)
|
||||||
|
authorized = auth_handler.has_permission(session, user, permission)
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
return authorized
|
||||||
|
|
|
@ -24,8 +24,6 @@
|
||||||
DataSync Views
|
DataSync Views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
@ -35,36 +33,11 @@ from tailbone.views.asgi import WebsocketView
|
||||||
class DatasyncWS(WebsocketView):
|
class DatasyncWS(WebsocketView):
|
||||||
|
|
||||||
async def status(self, scope, receive, send):
|
async def status(self, scope, receive, send):
|
||||||
rattail_config = self.registry['rattail_config']
|
app = self.get_rattail_app()
|
||||||
app = rattail_config.get_app()
|
|
||||||
model = app.model
|
|
||||||
auth_handler = app.get_auth_handler()
|
|
||||||
datasync_handler = app.get_datasync_handler()
|
datasync_handler = app.get_datasync_handler()
|
||||||
|
|
||||||
authorized = False
|
# is user allowed to see this?
|
||||||
user_session = await self.get_user_session(scope)
|
if not await self.authorize(scope, receive, send, 'datasync.status'):
|
||||||
if user_session:
|
|
||||||
user_uuid = user_session.get('auth.userid')
|
|
||||||
session = app.make_session()
|
|
||||||
|
|
||||||
user = None
|
|
||||||
if user_uuid:
|
|
||||||
user = session.query(model.User).get(user_uuid)
|
|
||||||
|
|
||||||
# figure out if user is authorized for this websocket
|
|
||||||
permission = 'datasync.status'
|
|
||||||
authorized = auth_handler.has_permission(session, user, permission)
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
# wait for client to connect
|
|
||||||
message = await receive()
|
|
||||||
assert message['type'] == 'websocket.connect'
|
|
||||||
|
|
||||||
# allow or deny access, per authorization
|
|
||||||
if authorized:
|
|
||||||
await send({'type': 'websocket.accept'})
|
|
||||||
else: # forbidden
|
|
||||||
await send({'type': 'websocket.close'})
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# this tracks when client disconnects
|
# this tracks when client disconnects
|
||||||
|
|
131
tailbone/views/asgi/upgrades.py
Normal file
131
tailbone/views/asgi/upgrades.py
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2022 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/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Upgrade Views for ASGI
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from urllib.parse import parse_qs
|
||||||
|
|
||||||
|
from tailbone.views.asgi import WebsocketView
|
||||||
|
from tailbone.progress import get_basic_session
|
||||||
|
|
||||||
|
|
||||||
|
class UpgradeWS(WebsocketView):
|
||||||
|
|
||||||
|
async def execution_progress(self, scope, receive, send):
|
||||||
|
rattail_config = self.registry['rattail_config']
|
||||||
|
|
||||||
|
# is user allowed to see this?
|
||||||
|
if not await self.authorize(scope, receive, send, 'upgrades.execute'):
|
||||||
|
return
|
||||||
|
|
||||||
|
# this tracks when client disconnects
|
||||||
|
state = {'disconnected': False}
|
||||||
|
|
||||||
|
async def wait_for_disconnect():
|
||||||
|
message = await receive()
|
||||||
|
if message['type'] == 'websocket.disconnect':
|
||||||
|
state['disconnected'] = True
|
||||||
|
|
||||||
|
# watch for client disconnect, while we do other things
|
||||||
|
asyncio.create_task(wait_for_disconnect())
|
||||||
|
|
||||||
|
query = scope['query_string'].decode('utf_8')
|
||||||
|
query = parse_qs(query)
|
||||||
|
uuid = query['uuid'][0]
|
||||||
|
progress_session_id = 'upgrades.{}.execution_progress'.format(uuid)
|
||||||
|
progress_session = get_basic_session(rattail_config,
|
||||||
|
id=progress_session_id)
|
||||||
|
|
||||||
|
# do the rest forever, until client disconnects
|
||||||
|
while not state['disconnected']:
|
||||||
|
|
||||||
|
# load latest progress data
|
||||||
|
progress_session.load()
|
||||||
|
|
||||||
|
# when upgrade progress is complete...
|
||||||
|
if progress_session.get('complete'):
|
||||||
|
|
||||||
|
# maybe set success flash msg
|
||||||
|
msg = progress_session.get('success_msg')
|
||||||
|
if msg:
|
||||||
|
user_session = await self.get_user_session(scope)
|
||||||
|
user_session.flash(msg)
|
||||||
|
user_session.persist()
|
||||||
|
|
||||||
|
# tell client progress is complete
|
||||||
|
await send({'type': 'websocket.send',
|
||||||
|
'subtype': 'upgrades.execute_progress',
|
||||||
|
'text': json.dumps({'complete': True})})
|
||||||
|
|
||||||
|
# this websocket is done
|
||||||
|
break
|
||||||
|
|
||||||
|
# we will send this data down to client
|
||||||
|
data = dict(progress_session)
|
||||||
|
|
||||||
|
# maybe add more lines from command output
|
||||||
|
path = rattail_config.upgrade_filepath(uuid, filename='stdout.log')
|
||||||
|
offset = progress_session.get('stdout.offset', 0)
|
||||||
|
if os.path.exists(path):
|
||||||
|
size = os.path.getsize(path) - offset
|
||||||
|
if size > 0:
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
f.seek(offset)
|
||||||
|
chunk = f.read(size)
|
||||||
|
data['stdout'] = chunk.decode('utf8').replace('\n', '<br />')
|
||||||
|
progress_session['stdout.offset'] = offset + size
|
||||||
|
progress_session.save()
|
||||||
|
|
||||||
|
# send data to client
|
||||||
|
await send({'type': 'websocket.send',
|
||||||
|
'subtype': 'upgrades.execute_progress',
|
||||||
|
'text': json.dumps(data)})
|
||||||
|
|
||||||
|
# pause for 1 second
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def defaults(cls, config):
|
||||||
|
cls._defaults(config)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _defaults(cls, config):
|
||||||
|
|
||||||
|
# execution progress
|
||||||
|
config.add_tailbone_websocket('upgrades.execution_progress',
|
||||||
|
cls, attr='execution_progress')
|
||||||
|
|
||||||
|
|
||||||
|
def defaults(config, **kwargs):
|
||||||
|
base = globals()
|
||||||
|
|
||||||
|
UpgradeWS = kwargs.get('UpgradeWS', base['UpgradeWS'])
|
||||||
|
UpgradeWS.defaults(config)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
defaults(config)
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2021 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -134,12 +134,12 @@ class View(object):
|
||||||
def progress_loop(self, func, items, factory, *args, **kwargs):
|
def progress_loop(self, func, items, factory, *args, **kwargs):
|
||||||
return progress_loop(func, items, factory, *args, **kwargs)
|
return progress_loop(func, items, factory, *args, **kwargs)
|
||||||
|
|
||||||
def make_progress(self, key):
|
def make_progress(self, key, **kwargs):
|
||||||
"""
|
"""
|
||||||
Create and return a :class:`tailbone.progress.SessionProgress`
|
Create and return a :class:`tailbone.progress.SessionProgress`
|
||||||
instance, with the given key.
|
instance, with the given key.
|
||||||
"""
|
"""
|
||||||
return SessionProgress(self.request, key)
|
return SessionProgress(self.request, key, **kwargs)
|
||||||
|
|
||||||
# TODO: this signature seems wonky
|
# TODO: this signature seems wonky
|
||||||
def render_progress(self, progress, kwargs, template=None):
|
def render_progress(self, progress, kwargs, template=None):
|
||||||
|
|
|
@ -1790,14 +1790,28 @@ class MasterView(View):
|
||||||
"""
|
"""
|
||||||
obj = self.get_instance()
|
obj = self.get_instance()
|
||||||
model_title = self.get_model_title()
|
model_title = self.get_model_title()
|
||||||
progress = self.make_execute_progress(obj)
|
|
||||||
|
|
||||||
|
# caller must explicitly request websocket behavior; otherwise
|
||||||
|
# we will assume traditional behavior for progress
|
||||||
|
ws = self.request.is_xhr and self.request.json_body.get('ws')
|
||||||
|
|
||||||
|
# make our progress tracker
|
||||||
|
progress = self.make_execute_progress(obj, ws=ws)
|
||||||
|
|
||||||
|
# start execution in a separate thread
|
||||||
kwargs = {'progress': progress}
|
kwargs = {'progress': progress}
|
||||||
key = [self.request.matchdict[k]
|
key = [self.request.matchdict[k]
|
||||||
for k in self.get_model_key(as_tuple=True)]
|
for k in self.get_model_key(as_tuple=True)]
|
||||||
thread = Thread(target=self.execute_thread, args=(key, self.request.user.uuid), kwargs=kwargs)
|
thread = Thread(target=self.execute_thread,
|
||||||
|
args=(key, self.request.user.uuid),
|
||||||
|
kwargs=kwargs)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
|
# we're done here if using websockets
|
||||||
|
if ws:
|
||||||
|
return self.json_response({'ok': True})
|
||||||
|
|
||||||
|
# traditional behavior sends user to dedicated progress page
|
||||||
return self.render_progress(progress, {
|
return self.render_progress(progress, {
|
||||||
'instance': obj,
|
'instance': obj,
|
||||||
'initial_msg': self.execute_progress_initial_msg,
|
'initial_msg': self.execute_progress_initial_msg,
|
||||||
|
@ -1805,9 +1819,12 @@ class MasterView(View):
|
||||||
'cancel_msg': "{} execution was canceled".format(model_title),
|
'cancel_msg': "{} execution was canceled".format(model_title),
|
||||||
}, template=self.execute_progress_template)
|
}, template=self.execute_progress_template)
|
||||||
|
|
||||||
def make_execute_progress(self, obj):
|
def make_execute_progress(self, obj, ws=False):
|
||||||
key = '{}.execute'.format(self.get_grid_key())
|
if ws:
|
||||||
return self.make_progress(key)
|
key = '{}.{}.execution_progress'.format(self.get_route_prefix(), obj.uuid)
|
||||||
|
else:
|
||||||
|
key = '{}.execute'.format(self.get_grid_key())
|
||||||
|
return self.make_progress(key, ws=ws)
|
||||||
|
|
||||||
def get_instance_for_key(self, key, session):
|
def get_instance_for_key(self, key, session):
|
||||||
model_key = self.get_model_key(as_tuple=True)
|
model_key = self.get_model_key(as_tuple=True)
|
||||||
|
|
|
@ -26,24 +26,27 @@ Views for app upgrades
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
|
import warnings
|
||||||
|
|
||||||
import six
|
import six
|
||||||
from sqlalchemy import orm
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from rattail.core import Object
|
from rattail.core import Object
|
||||||
from rattail.db import model, Session as RattailSession
|
from rattail.db import model, Session as RattailSession
|
||||||
from rattail.time import make_utc
|
from rattail.time import make_utc
|
||||||
from rattail.threads import Thread
|
from rattail.threads import Thread
|
||||||
from rattail.upgrades import get_upgrade_handler
|
from rattail.util import OrderedDict
|
||||||
|
|
||||||
from deform import widget as dfwidget
|
from deform import widget as dfwidget
|
||||||
from webhelpers2.html import tags, HTML
|
from webhelpers2.html import tags, HTML
|
||||||
|
|
||||||
from tailbone.views import MasterView
|
from tailbone.views import MasterView
|
||||||
from tailbone.progress import get_progress_session #, SessionProgress
|
from tailbone.progress import get_progress_session #, SessionProgress
|
||||||
|
from tailbone.config import should_expose_websockets
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -56,6 +59,7 @@ class UpgradeView(MasterView):
|
||||||
model_class = model.Upgrade
|
model_class = model.Upgrade
|
||||||
downloadable = True
|
downloadable = True
|
||||||
cloneable = True
|
cloneable = True
|
||||||
|
configurable = True
|
||||||
executable = True
|
executable = True
|
||||||
execute_progress_template = '/upgrade.mako'
|
execute_progress_template = '/upgrade.mako'
|
||||||
execute_progress_initial_msg = "Upgrading"
|
execute_progress_initial_msg = "Upgrading"
|
||||||
|
@ -68,6 +72,7 @@ class UpgradeView(MasterView):
|
||||||
}
|
}
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
|
'system',
|
||||||
'created',
|
'created',
|
||||||
'description',
|
'description',
|
||||||
# 'not_until',
|
# 'not_until',
|
||||||
|
@ -78,6 +83,7 @@ class UpgradeView(MasterView):
|
||||||
]
|
]
|
||||||
|
|
||||||
form_fields = [
|
form_fields = [
|
||||||
|
'system',
|
||||||
'description',
|
'description',
|
||||||
# 'not_until',
|
# 'not_until',
|
||||||
# 'requirements',
|
# 'requirements',
|
||||||
|
@ -97,28 +103,40 @@ class UpgradeView(MasterView):
|
||||||
|
|
||||||
def __init__(self, request):
|
def __init__(self, request):
|
||||||
super(UpgradeView, self).__init__(request)
|
super(UpgradeView, self).__init__(request)
|
||||||
self.handler = self.get_handler()
|
|
||||||
|
|
||||||
def get_handler(self):
|
if hasattr(self, 'get_handler'):
|
||||||
"""
|
warnings.warn("defining get_handler() is deprecated. please "
|
||||||
Returns the ``UpgradeHandler`` instance for the view. The handler
|
"override AppHandler.get_upgrade_handler() instead",
|
||||||
factory for this may be defined by config, e.g.:
|
DeprecationWarning, stacklevel=2)
|
||||||
|
self.upgrade_handler = self.get_handler()
|
||||||
|
|
||||||
.. code-block:: ini
|
else:
|
||||||
|
app = self.get_rattail_app()
|
||||||
|
self.upgrade_handler = app.get_upgrade_handler()
|
||||||
|
|
||||||
[rattail.upgrades]
|
@property
|
||||||
handler = myapp.upgrades:CustomUpgradeHandler
|
def handler(self):
|
||||||
"""
|
warnings.warn("handler attribute is deprecated; "
|
||||||
return get_upgrade_handler(self.rattail_config)
|
"please use upgrade_handler instead",
|
||||||
|
DeprecationWarning, stacklevel=2)
|
||||||
|
return self.upgrade_handler
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
super(UpgradeView, self).configure_grid(g)
|
super(UpgradeView, self).configure_grid(g)
|
||||||
|
|
||||||
|
# system
|
||||||
|
systems = self.upgrade_handler.get_all_systems()
|
||||||
|
systems_enum = dict([(s['key'], s['label']) for s in systems])
|
||||||
|
g.set_enum('system', systems_enum)
|
||||||
|
|
||||||
g.set_joiner('executed_by', lambda q: q.join(model.User, model.User.uuid == model.Upgrade.executed_by_uuid).outerjoin(model.Person))
|
g.set_joiner('executed_by', lambda q: q.join(model.User, model.User.uuid == model.Upgrade.executed_by_uuid).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_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.set_sort_defaults('created', 'desc')
|
g.set_sort_defaults('created', 'desc')
|
||||||
|
|
||||||
|
g.set_link('system')
|
||||||
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')
|
||||||
|
@ -157,6 +175,16 @@ class UpgradeView(MasterView):
|
||||||
super(UpgradeView, self).configure_form(f)
|
super(UpgradeView, self).configure_form(f)
|
||||||
upgrade = f.model_instance
|
upgrade = f.model_instance
|
||||||
|
|
||||||
|
# system
|
||||||
|
systems = self.upgrade_handler.get_all_systems()
|
||||||
|
systems_enum = OrderedDict([(s['key'], s['label'])
|
||||||
|
for s in systems])
|
||||||
|
f.set_enum('system', systems_enum)
|
||||||
|
f.set_required('system')
|
||||||
|
if self.creating:
|
||||||
|
if len(systems) == 1:
|
||||||
|
f.set_default('system', list(systems_enum)[0])
|
||||||
|
|
||||||
# status_code
|
# status_code
|
||||||
if self.creating:
|
if self.creating:
|
||||||
f.remove_field('status_code')
|
f.remove_field('status_code')
|
||||||
|
@ -174,7 +202,15 @@ class UpgradeView(MasterView):
|
||||||
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)
|
|
||||||
|
# package_diff
|
||||||
|
if self.viewing and upgrade.executed and (
|
||||||
|
upgrade.system == 'rattail'
|
||||||
|
or not upgrade.system):
|
||||||
|
f.set_renderer('package_diff', self.render_package_diff)
|
||||||
|
else:
|
||||||
|
f.remove_field('package_diff')
|
||||||
|
|
||||||
# f.set_readonly('created')
|
# f.set_readonly('created')
|
||||||
# f.set_readonly('created_by')
|
# f.set_readonly('created_by')
|
||||||
f.set_readonly('executed')
|
f.set_readonly('executed')
|
||||||
|
@ -202,7 +238,6 @@ class UpgradeView(MasterView):
|
||||||
f.set_default('enabled', True)
|
f.set_default('enabled', True)
|
||||||
|
|
||||||
if not self.viewing or not upgrade.executed:
|
if not self.viewing or not upgrade.executed:
|
||||||
f.remove_field('package_diff')
|
|
||||||
f.remove_field('exit_code')
|
f.remove_field('exit_code')
|
||||||
|
|
||||||
def render_status_code(self, upgrade, field):
|
def render_status_code(self, upgrade, field):
|
||||||
|
@ -233,10 +268,11 @@ class UpgradeView(MasterView):
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def configure_clone_form(self, f):
|
def configure_clone_form(self, f):
|
||||||
f.fields = ['description', 'notes', 'enabled']
|
f.fields = ['system', 'description', 'notes', 'enabled']
|
||||||
|
|
||||||
def clone_instance(self, original):
|
def clone_instance(self, original):
|
||||||
cloned = self.model_class()
|
cloned = self.model_class()
|
||||||
|
cloned.system = original.system
|
||||||
cloned.created = make_utc()
|
cloned.created = make_utc()
|
||||||
cloned.created_by = self.request.user
|
cloned.created_by = self.request.user
|
||||||
cloned.description = original.description
|
cloned.description = original.description
|
||||||
|
@ -439,13 +475,22 @@ class UpgradeView(MasterView):
|
||||||
# key = '{}.execute'.format(self.get_grid_key())
|
# key = '{}.execute'.format(self.get_grid_key())
|
||||||
# return SessionProgress(self.request, key, session_type='file')
|
# return SessionProgress(self.request, key, session_type='file')
|
||||||
|
|
||||||
def execute_instance(self, upgrade, user, **kwargs):
|
def execute_instance(self, upgrade, user, progress=None, **kwargs):
|
||||||
session = orm.object_session(upgrade)
|
app = self.get_rattail_app()
|
||||||
self.handler.mark_executing(upgrade)
|
session = app.get_session(upgrade)
|
||||||
|
|
||||||
|
# record the fact that execution has begun for this ugprade
|
||||||
|
self.upgrade_handler.mark_executing(upgrade)
|
||||||
session.commit()
|
session.commit()
|
||||||
self.handler.do_execute(upgrade, user, **kwargs)
|
|
||||||
return ("Execution has finished, for better or worse. "
|
# let handler execute the upgrade
|
||||||
"You may need to restart your web app.")
|
self.upgrade_handler.do_execute(upgrade, user, **kwargs)
|
||||||
|
|
||||||
|
# success msg
|
||||||
|
msg = "Execution has finished, for better or worse."
|
||||||
|
if not upgrade.system or upgrade.system == 'rattail':
|
||||||
|
msg += " You may need to restart your web app."
|
||||||
|
return msg
|
||||||
|
|
||||||
def execute_progress(self):
|
def execute_progress(self):
|
||||||
upgrade = self.get_instance()
|
upgrade = self.get_instance()
|
||||||
|
@ -489,6 +534,50 @@ class UpgradeView(MasterView):
|
||||||
self.handler.delete_files(upgrade)
|
self.handler.delete_files(upgrade)
|
||||||
super(UpgradeView, self).delete_instance(upgrade)
|
super(UpgradeView, self).delete_instance(upgrade)
|
||||||
|
|
||||||
|
def configure_get_context(self, **kwargs):
|
||||||
|
context = super(UpgradeView, self).configure_get_context(**kwargs)
|
||||||
|
|
||||||
|
context['upgrade_systems'] = self.upgrade_handler.get_all_systems()
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def configure_gather_settings(self, data):
|
||||||
|
settings = super(UpgradeView, self).configure_gather_settings(data)
|
||||||
|
|
||||||
|
keys = []
|
||||||
|
for system in json.loads(data['upgrade_systems']):
|
||||||
|
key = system['key']
|
||||||
|
if key == 'rattail':
|
||||||
|
settings.append({'name': 'rattail.upgrades.command',
|
||||||
|
'value': system['command']})
|
||||||
|
else:
|
||||||
|
keys.append(key)
|
||||||
|
settings.append({'name': 'rattail.upgrades.system.{}.label'.format(key),
|
||||||
|
'value': system['label']})
|
||||||
|
settings.append({'name': 'rattail.upgrades.system.{}.command'.format(key),
|
||||||
|
'value': system['command']})
|
||||||
|
if keys:
|
||||||
|
settings.append({'name': 'rattail.upgrades.systems',
|
||||||
|
'value': ', '.join(keys)})
|
||||||
|
|
||||||
|
return settings
|
||||||
|
|
||||||
|
def configure_remove_settings(self):
|
||||||
|
super(UpgradeView, self).configure_remove_settings()
|
||||||
|
app = self.get_rattail_app()
|
||||||
|
model = self.model
|
||||||
|
|
||||||
|
to_delete = self.Session.query(model.Setting)\
|
||||||
|
.filter(sa.or_(
|
||||||
|
model.Setting.name == 'rattail.upgrades.command',
|
||||||
|
model.Setting.name == 'rattail.upgrades.systems',
|
||||||
|
model.Setting.name.like('rattail.upgrades.system.%.label'),
|
||||||
|
model.Setting.name.like('rattail.upgrades.system.%.command')))\
|
||||||
|
.all()
|
||||||
|
|
||||||
|
for setting in to_delete:
|
||||||
|
app.delete_setting(self.Session(), setting.name)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def defaults(cls, config):
|
def defaults(cls, config):
|
||||||
cls._defaults(config)
|
cls._defaults(config)
|
||||||
|
@ -520,10 +609,14 @@ class UpgradeView(MasterView):
|
||||||
|
|
||||||
def defaults(config, **kwargs):
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
rattail_config = config.registry['rattail_config']
|
||||||
|
|
||||||
UpgradeView = kwargs.get('UpgradeView', base['UpgradeView'])
|
UpgradeView = kwargs.get('UpgradeView', base['UpgradeView'])
|
||||||
UpgradeView.defaults(config)
|
UpgradeView.defaults(config)
|
||||||
|
|
||||||
|
if should_expose_websockets(rattail_config):
|
||||||
|
config.include('tailbone.views.asgi.upgrades')
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
defaults(config)
|
defaults(config)
|
||||||
|
|
Loading…
Reference in a new issue