flows: export export/import functions in UI

This commit is contained in:
Jens Langhammer 2020-08-28 15:06:25 +02:00
parent b7ca40d98e
commit a977184577
7 changed files with 88 additions and 4 deletions

View 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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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",
), ),

View file

@ -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

View file

@ -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"]

View file

@ -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