flows: export export/import functions in UI
This commit is contained in:
parent
b7ca40d98e
commit
a977184577
13
passbook/admin/templates/administration/flow/import.html
Normal file
13
passbook/admin/templates/administration/flow/import.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends base_template|default:"generic/form.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block above_form %}
|
||||||
|
<h1>
|
||||||
|
{% trans 'Import Flow' %}
|
||||||
|
</h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block action %}
|
||||||
|
{% trans 'Import Flow' %}
|
||||||
|
{% endblock %}
|
|
@ -20,6 +20,7 @@
|
||||||
<div class="pf-c-toolbar__content">
|
<div class="pf-c-toolbar__content">
|
||||||
<div class="pf-c-toolbar__bulk-select">
|
<div class="pf-c-toolbar__bulk-select">
|
||||||
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||||
|
<a href="{% url 'passbook_admin:flow-import' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-secondary" type="button">{% trans 'Import' %}</a>
|
||||||
</div>
|
</div>
|
||||||
{% include 'partials/pagination.html' %}
|
{% include 'partials/pagination.html' %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,6 +63,7 @@
|
||||||
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:flow-update' pk=flow.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:flow-update' pk=flow.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
||||||
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:flow-delete' pk=flow.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:flow-delete' pk=flow.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||||
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:flow-execute' pk=flow.pk %}?next={{ request.get_full_path }}">{% trans 'Execute' %}</a>
|
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:flow-execute' pk=flow.pk %}?next={{ request.get_full_path }}">{% trans 'Execute' %}</a>
|
||||||
|
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:flow-export' pk=flow.pk %}?next={{ request.get_full_path }}">{% trans 'Export' %}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -81,6 +83,7 @@
|
||||||
{% trans 'Currently no flows exist. Click the button below to create one.' %}
|
{% trans 'Currently no flows exist. Click the button below to create one.' %}
|
||||||
</div>
|
</div>
|
||||||
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||||
|
<a href="{% url 'passbook_admin:flow-import' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Import' %}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
<div class="pf-l-stack__item">
|
<div class="pf-l-stack__item">
|
||||||
<div class="pf-c-card">
|
<div class="pf-c-card">
|
||||||
<div class="pf-c-card__body">
|
<div class="pf-c-card__body">
|
||||||
<form action="" method="post" class="pf-c-form pf-m-horizontal">
|
<form action="" method="post" class="pf-c-form pf-m-horizontal" enctype="multipart/form-data">
|
||||||
{% include 'partials/form_horizontal.html' with form=form %}
|
{% include 'partials/form_horizontal.html' with form=form %}
|
||||||
{% block beneath_form %}
|
{% block beneath_form %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -191,6 +191,7 @@ urlpatterns = [
|
||||||
# Flows
|
# Flows
|
||||||
path("flows/", flows.FlowListView.as_view(), name="flows"),
|
path("flows/", flows.FlowListView.as_view(), name="flows"),
|
||||||
path("flows/create/", flows.FlowCreateView.as_view(), name="flow-create",),
|
path("flows/create/", flows.FlowCreateView.as_view(), name="flow-create",),
|
||||||
|
path("flows/import/", flows.FlowImportView.as_view(), name="flow-import",),
|
||||||
path(
|
path(
|
||||||
"flows/<uuid:pk>/update/", flows.FlowUpdateView.as_view(), name="flow-update",
|
"flows/<uuid:pk>/update/", flows.FlowUpdateView.as_view(), name="flow-update",
|
||||||
),
|
),
|
||||||
|
@ -199,6 +200,9 @@ urlpatterns = [
|
||||||
flows.FlowDebugExecuteView.as_view(),
|
flows.FlowDebugExecuteView.as_view(),
|
||||||
name="flow-execute",
|
name="flow-execute",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"flows/<uuid:pk>/export/", flows.FlowExportView.as_view(), name="flow-export",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"flows/<uuid:pk>/delete/", flows.FlowDeleteView.as_view(), name="flow-delete",
|
"flows/<uuid:pk>/delete/", flows.FlowDeleteView.as_view(), name="flow-delete",
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
"""passbook Flow administration"""
|
"""passbook Flow administration"""
|
||||||
|
from django.contrib import messages
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.contrib.auth.mixins import (
|
from django.contrib.auth.mixins import (
|
||||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||||
)
|
)
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse, JsonResponse
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.views.generic import DetailView, ListView, UpdateView
|
from django.views.generic import DetailView, FormView, ListView, UpdateView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
from passbook.admin.views.utils import DeleteMessageView
|
from passbook.admin.views.utils import DeleteMessageView
|
||||||
from passbook.flows.forms import FlowForm
|
from passbook.flows.forms import FlowForm, FlowImportForm
|
||||||
from passbook.flows.models import Flow
|
from passbook.flows.models import Flow
|
||||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
|
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
|
from passbook.flows.transfer.exporter import FlowExporter
|
||||||
|
from passbook.flows.transfer.importer import FlowImporter
|
||||||
from passbook.flows.views import SESSION_KEY_PLAN, FlowPlanner
|
from passbook.flows.views import SESSION_KEY_PLAN, FlowPlanner
|
||||||
from passbook.lib.utils.urls import redirect_with_qs
|
from passbook.lib.utils.urls import redirect_with_qs
|
||||||
from passbook.lib.views import CreateAssignPermView
|
from passbook.lib.views import CreateAssignPermView
|
||||||
|
@ -88,3 +91,43 @@ class FlowDebugExecuteView(LoginRequiredMixin, PermissionRequiredMixin, DetailVi
|
||||||
return redirect_with_qs(
|
return redirect_with_qs(
|
||||||
"passbook_flows:flow-executor-shell", self.request.GET, flow_slug=flow.slug,
|
"passbook_flows:flow-executor-shell", self.request.GET, flow_slug=flow.slug,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FlowImportView(LoginRequiredMixin, FormView):
|
||||||
|
"""Import flow from JSON Export; only allowed for superusers
|
||||||
|
as these flows can contain python code"""
|
||||||
|
|
||||||
|
form_class = FlowImportForm
|
||||||
|
template_name = "administration/flow/import.html"
|
||||||
|
success_url = reverse_lazy("passbook_admin:flows")
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
if not request.user.is_superuser:
|
||||||
|
return self.handle_no_permission()
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def form_valid(self, form: FlowImportForm) -> HttpResponse:
|
||||||
|
importer = FlowImporter(form.cleaned_data["flow"].read().decode())
|
||||||
|
successful = importer.apply()
|
||||||
|
if not successful:
|
||||||
|
messages.error(self.request, _("Failed to import flow."))
|
||||||
|
else:
|
||||||
|
messages.success(self.request, _("Successfully imported flow."))
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
class FlowExportView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
||||||
|
"""Export Flow"""
|
||||||
|
|
||||||
|
model = Flow
|
||||||
|
permission_required = "passbook_flows.export_flow"
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def get(self, request: HttpRequest, pk: str) -> HttpResponse:
|
||||||
|
"""Debug exectue flow, setting the current user as pending user"""
|
||||||
|
flow: Flow = self.get_object()
|
||||||
|
exporter = FlowExporter(flow)
|
||||||
|
export = exporter.export_to_string()
|
||||||
|
response = JsonResponse(export)
|
||||||
|
response["Content-Disposition"] = f'attachment; filename="{flow.slug}.json"'
|
||||||
|
return response
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
"""Flow and Stage forms"""
|
"""Flow and Stage forms"""
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.forms import ValidationError
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from passbook.flows.models import Flow, FlowStageBinding, Stage
|
from passbook.flows.models import Flow, FlowStageBinding, Stage
|
||||||
|
from passbook.flows.transfer.importer import FlowImporter
|
||||||
from passbook.lib.widgets import GroupedModelChoiceField
|
from passbook.lib.widgets import GroupedModelChoiceField
|
||||||
|
|
||||||
|
|
||||||
|
@ -53,3 +55,18 @@ class FlowStageBindingForm(forms.ModelForm):
|
||||||
widgets = {
|
widgets = {
|
||||||
"name": forms.TextInput(),
|
"name": forms.TextInput(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FlowImportForm(forms.Form):
|
||||||
|
"""Form used for flow importing"""
|
||||||
|
|
||||||
|
flow = forms.FileField()
|
||||||
|
|
||||||
|
def clean_flow(self):
|
||||||
|
"""Check if the flow is valid and rewind the file to the start"""
|
||||||
|
flow = self.cleaned_data["flow"].read()
|
||||||
|
valid = FlowImporter(flow.decode()).validate()
|
||||||
|
if not valid:
|
||||||
|
raise ValidationError(_("Flow invalid."))
|
||||||
|
self.cleaned_data["flow"].seek(0)
|
||||||
|
return self.cleaned_data["flow"]
|
||||||
|
|
|
@ -135,6 +135,10 @@ class Flow(SerializerModel, PolicyBindingModel):
|
||||||
verbose_name = _("Flow")
|
verbose_name = _("Flow")
|
||||||
verbose_name_plural = _("Flows")
|
verbose_name_plural = _("Flows")
|
||||||
|
|
||||||
|
permissions = [
|
||||||
|
("export_flow", "Can export a Flow"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class FlowStageBinding(SerializerModel, PolicyBindingModel):
|
class FlowStageBinding(SerializerModel, PolicyBindingModel):
|
||||||
"""Relationship between Flow and Stage. Order is required and unique for
|
"""Relationship between Flow and Stage. Order is required and unique for
|
||||||
|
|
Reference in a new issue