diff --git a/passbook/admin/templates/administration/flow/import.html b/passbook/admin/templates/administration/flow/import.html new file mode 100644 index 000000000..14eca092a --- /dev/null +++ b/passbook/admin/templates/administration/flow/import.html @@ -0,0 +1,13 @@ +{% extends base_template|default:"generic/form.html" %} + +{% load i18n %} + +{% block above_form %} +

+{% trans 'Import Flow' %} +

+{% endblock %} + +{% block action %} +{% trans 'Import Flow' %} +{% endblock %} diff --git a/passbook/admin/templates/administration/flow/list.html b/passbook/admin/templates/administration/flow/list.html index 0956ead3f..9d8891a1d 100644 --- a/passbook/admin/templates/administration/flow/list.html +++ b/passbook/admin/templates/administration/flow/list.html @@ -20,6 +20,7 @@
{% trans 'Create' %} + {% trans 'Import' %}
{% include 'partials/pagination.html' %}
@@ -62,6 +63,7 @@ {% trans 'Edit' %} {% trans 'Delete' %} {% trans 'Execute' %} + {% trans 'Export' %} {% endfor %} @@ -81,6 +83,7 @@ {% trans 'Currently no flows exist. Click the button below to create one.' %} {% trans 'Create' %} + {% trans 'Import' %} {% endif %} diff --git a/passbook/admin/templates/generic/form.html b/passbook/admin/templates/generic/form.html index 44f8f0e83..5ce597ea9 100644 --- a/passbook/admin/templates/generic/form.html +++ b/passbook/admin/templates/generic/form.html @@ -30,7 +30,7 @@
-
+ {% include 'partials/form_horizontal.html' with form=form %} {% block beneath_form %} {% endblock %} diff --git a/passbook/admin/urls.py b/passbook/admin/urls.py index 832f1d0df..3c2771f5d 100644 --- a/passbook/admin/urls.py +++ b/passbook/admin/urls.py @@ -191,6 +191,7 @@ urlpatterns = [ # Flows path("flows/", flows.FlowListView.as_view(), name="flows"), path("flows/create/", flows.FlowCreateView.as_view(), name="flow-create",), + path("flows/import/", flows.FlowImportView.as_view(), name="flow-import",), path( "flows//update/", flows.FlowUpdateView.as_view(), name="flow-update", ), @@ -199,6 +200,9 @@ urlpatterns = [ flows.FlowDebugExecuteView.as_view(), name="flow-execute", ), + path( + "flows//export/", flows.FlowExportView.as_view(), name="flow-export", + ), path( "flows//delete/", flows.FlowDeleteView.as_view(), name="flow-delete", ), diff --git a/passbook/admin/views/flows.py b/passbook/admin/views/flows.py index 91e2910d6..73318f0ca 100644 --- a/passbook/admin/views/flows.py +++ b/passbook/admin/views/flows.py @@ -1,19 +1,22 @@ """passbook Flow administration""" +from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import ( PermissionRequiredMixin as DjangoPermissionRequiredMixin, ) 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.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 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.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.lib.utils.urls import redirect_with_qs from passbook.lib.views import CreateAssignPermView @@ -88,3 +91,43 @@ class FlowDebugExecuteView(LoginRequiredMixin, PermissionRequiredMixin, DetailVi return redirect_with_qs( "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 diff --git a/passbook/flows/forms.py b/passbook/flows/forms.py index 7abfe2d30..46b90b05b 100644 --- a/passbook/flows/forms.py +++ b/passbook/flows/forms.py @@ -1,9 +1,11 @@ """Flow and Stage forms""" from django import forms +from django.forms import ValidationError from django.utils.translation import gettext_lazy as _ from passbook.flows.models import Flow, FlowStageBinding, Stage +from passbook.flows.transfer.importer import FlowImporter from passbook.lib.widgets import GroupedModelChoiceField @@ -53,3 +55,18 @@ class FlowStageBindingForm(forms.ModelForm): widgets = { "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"] diff --git a/passbook/flows/models.py b/passbook/flows/models.py index 1792710da..aec61cadf 100644 --- a/passbook/flows/models.py +++ b/passbook/flows/models.py @@ -135,6 +135,10 @@ class Flow(SerializerModel, PolicyBindingModel): verbose_name = _("Flow") verbose_name_plural = _("Flows") + permissions = [ + ("export_flow", "Can export a Flow"), + ] + class FlowStageBinding(SerializerModel, PolicyBindingModel): """Relationship between Flow and Stage. Order is required and unique for