Repository: saleor/saleor
Pull request: Add form to configure payment gateways in dashboard
Right now there is no option to set payments data in dashboard 1.0 In this pull request I've added a section in the dashboard with listed all available payment gateways in `settings.PLUGINS` and option to configure them.
From developers side I've created model form for `Plugin Configuration` with dynamic fields created from `CONFIGURATION_STRUCTURE`.

Closes #4784 

### Screenshots

<!-- If your changes affect the UI, providing "before" and "after" screenshots will
greatly reduce the amount of work needed to review your work. -->

New views introduced:
![Zrzut ekranu 2019-10-4 o 15 29 04](https://user-images.githubusercontent.com/6984271/66211264-f2e6de00-e6bb-11e9-989a-d1a177018ea8.png)
![Zrzut ekranu 2019-10-4 o 15 30 21](https://user-images.githubusercontent.com/6984271/66211265-f2e6de00-e6bb-11e9-8d37-5b8d2649ca1f.png)


### Pull Request Checklist

<!-- Please keep this section. It will make maintainer's life easier. -->

1. [ ] Privileged views and APIs are guarded by proper permission checks.
1. [x] All visible strings are translated with proper context.
1. [x] All data-formatting is locale-aware (dates, numbers, and so on).
1. [ ] Database queries are optimized and the number of queries is constant.
1. [x] Database migration files are up to date.
1. [x] The changes are tested.
1. [ ] GraphQL schema and type definitions are up to date.
1. [x] Changes are mentioned in the changelog.


Problem statement:
Add configuration of payments gateway to dashboard 1.0
We moved payments gateway to the plugin architecture. Each plugin can be configured over dashboard2.0 by using dynamic settings. 
Because we removed the payment configuration from settings.py, we don't have any way to configure them by someone who uses dashboard1.0. We need to add some forms for each payment gateway this will give us the possibility to configure the payment gateway.

Hints from issue discussion:

Commit message: Update absctract class definition

Patch to review:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c4c4fe981e3..24cd438a82b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -90,6 +90,7 @@ All notable, unreleased changes to this project will be documented in this file.
 - Change `unique_together` in `AttributeValue` - #4805 by @fowczarek
 - Change max length of SKU in order/product variant to 255 - #4811 by @lex111
 - Replace Pipenv with Poetry - #3894 by @michaljelonek
+- Add form to configure payments in dashboard - #4807 by @szewczykmira
 
 ## 2.8.0
 
diff --git a/saleor/core/payments.py b/saleor/core/payments.py
index b32491f2a4d..a58458484df 100644
--- a/saleor/core/payments.py
+++ b/saleor/core/payments.py
@@ -7,7 +7,7 @@
 
 class PaymentInterface(ABC):
     @abstractmethod
-    def list_payment_gateways(self) -> List[str]:
+    def list_payment_gateways(self, active_only: bool) -> List[str]:
         pass
 
     @abstractmethod
diff --git a/saleor/dashboard/forms.py b/saleor/dashboard/forms.py
index 08ff6092826..6fc3c2675fe 100644
--- a/saleor/dashboard/forms.py
+++ b/saleor/dashboard/forms.py
@@ -198,3 +198,24 @@ class PermissionMultipleChoiceField(forms.ModelMultipleChoiceField):
 
     def label_from_instance(self, obj):
         return obj.name
+
+
+class ConfigField(forms.Field):
+    def __init__(self, *args, **kwargs):
+        structure = kwargs.pop("structure")
+        super().__init__(*args, **kwargs)
+        self.label = structure["name"]
+        self.name = structure["name"]
+        self.help_text = structure["help_text"]
+        self.initial = structure["value"]
+
+
+class ConfigBooleanField(forms.BooleanField, ConfigField):
+    def to_python(self, value):
+        return {"name": self.label, "value": super().to_python(value)}
+
+
+class ConfigCharField(forms.CharField, ConfigField):
+    def clean(self, value):
+        parsed_value = super().clean(value)
+        return {"name": self.label, "value": parsed_value}
diff --git a/saleor/dashboard/payment/__init__.py b/saleor/dashboard/payment/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/saleor/dashboard/payment/forms.py b/saleor/dashboard/payment/forms.py
new file mode 100644
index 00000000000..897f16ba2e7
--- /dev/null
+++ b/saleor/dashboard/payment/forms.py
@@ -0,0 +1,58 @@
+from typing import Dict
+
+from django import forms
+from django.utils.text import slugify
+
+from ...extensions import ConfigurationTypeField
+from ...extensions.models import PluginConfiguration
+from ..forms import ConfigBooleanField, ConfigCharField
+
+TYPE_TO_FIELD = {
+    ConfigurationTypeField.STRING: ConfigCharField,
+    ConfigurationTypeField.BOOLEAN: ConfigBooleanField,
+}
+
+
+def create_custom_form_field(structure: Dict[str, str]) -> forms.Field:
+    elem_type = structure["type"]
+    return TYPE_TO_FIELD[elem_type](structure=structure)
+
+
+class GatewayConfigurationForm(forms.ModelForm):
+    class Meta:
+        model = PluginConfiguration
+        fields = ("active",)
+
+    def __init__(self, plugin, *args, **kwargs):
+        self.plugin = plugin
+        kwargs["instance"] = self._get_current_configuration()
+        super().__init__(*args, **kwargs)
+        # add new fields specified for gateway
+        self._prepare_fields_for_given_config(self.instance)
+
+    def _get_current_configuration(self) -> PluginConfiguration:
+        qs = PluginConfiguration.objects.all()
+        return self.plugin.get_plugin_configuration(qs)
+
+    def _prepare_fields_for_given_config(
+        self, current_configuration: PluginConfiguration
+    ) -> Dict[str, forms.Field]:
+        parsed_fields = {}
+        structure = current_configuration.configuration
+        if structure is None:
+            raise Exception
+        for elem in structure:
+            slug = slugify(elem["name"])
+            self.fields[slug] = create_custom_form_field(elem)
+        return parsed_fields
+
+    def parse_values(self):
+        cleaned_data = self.cleaned_data
+        active = cleaned_data.pop("active", False)
+        data = {"active": active}
+        data["configuration"] = list(cleaned_data.values())
+        return data
+
+    def save(self, *args):
+        parse_value = self.parse_values()
+        self.plugin.save_plugin_configuration(self.instance, parse_value)
diff --git a/saleor/dashboard/payment/urls.py b/saleor/dashboard/payment/urls.py
new file mode 100644
index 00000000000..b6dab5336c4
--- /dev/null
+++ b/saleor/dashboard/payment/urls.py
@@ -0,0 +1,12 @@
+from django.conf.urls import url
+
+from . import views
+
+urlpatterns = [
+    url(r"^$", views.index, name="payments-index"),
+    url(
+        r"^configure/(?P<plugin_name>[\w-]+)$",
+        views.configure_payment_gateway,
+        name="configure-payment",
+    ),
+]
diff --git a/saleor/dashboard/payment/views.py b/saleor/dashboard/payment/views.py
new file mode 100644
index 00000000000..874eecb6b6c
--- /dev/null
+++ b/saleor/dashboard/payment/views.py
@@ -0,0 +1,41 @@
+from django.contrib import messages
+from django.contrib.auth.decorators import permission_required
+from django.http import Http404, HttpRequest, HttpResponse
+from django.shortcuts import redirect
+from django.template.response import TemplateResponse
+from django.utils.translation import pgettext_lazy
+
+from ...extensions.manager import get_extensions_manager
+from ..views import staff_member_required
+from .forms import GatewayConfigurationForm
+
+
+@staff_member_required
+@permission_required("extensions.manage_plugins")
+def index(request: "HttpRequest") -> TemplateResponse:
+    payment_gateways = [
+        gateway["name"]
+        for gateway in get_extensions_manager().list_payment_gateways(active_only=False)
+    ]
+    ctx = {"payment_gateways": payment_gateways}
+    return TemplateResponse(request, "dashboard/payments/index.html", ctx)
+
+
+@staff_member_required
+@permission_required("extensions.manage_plugins")
+def configure_payment_gateway(request: HttpRequest, plugin_name: str) -> HttpResponse:
+    plugin = get_extensions_manager().get_plugin(plugin_name)
+    if plugin is None:
+        msg = pgettext_lazy("Dashboard message", "Selected plugin does not exist.")
+        messages.error(request, msg)
+        raise Http404()
+
+    form = GatewayConfigurationForm(plugin, request.POST or None)
+    if form.is_valid():
+        form.save()
+        msg = pgettext_lazy("Dashboard message", "Payment config succesfully updated")
+        messages.success(request, msg)
+        return redirect("dashboard:payments-index")
+
+    ctx = {"plugin_name": plugin_name, "config_form": form}
+    return TemplateResponse(request, "dashboard/payments/configuration_form.html", ctx)
diff --git a/saleor/dashboard/urls.py b/saleor/dashboard/urls.py
index 8f0ccbeef19..fe09c222054 100644
--- a/saleor/dashboard/urls.py
+++ b/saleor/dashboard/urls.py
@@ -8,6 +8,7 @@
 from .menu.urls import urlpatterns as menu_urls
 from .order.urls import urlpatterns as order_urls
 from .page.urls import urlpatterns as page_urls
+from .payment.urls import urlpatterns as payments_urls
 from .product.urls import urlpatterns as product_urls
 from .search.urls import urlpatterns as search_urls
 from .shipping.urls import urlpatterns as shipping_urls
@@ -27,6 +28,7 @@
     url(r"^discounts/", include(discount_urls)),
     url(r"^settings/", include(site_urls)),
     url(r"^menu/", include(menu_urls)),
+    url(r"^payments/", include(payments_urls)),
     url(r"^shipping/", include(shipping_urls)),
     url(r"^style-guide/", core_views.styleguide, name="styleguide"),
     url(r"^search/", include(search_urls)),
diff --git a/saleor/extensions/manager.py b/saleor/extensions/manager.py
index 0fbd4387364..48a4566a31c 100644
--- a/saleor/extensions/manager.py
+++ b/saleor/extensions/manager.py
@@ -263,7 +263,7 @@ def list_payment_sources(
             )
         raise Exception(f"Payment plugin {gateway} is inaccessible!")
 
-    def list_payment_gateways(self) -> List[dict]:
+    def list_payment_gateways(self, active_only: bool = True) -> List[dict]:
         payment_method = "process_payment"
         return [
             {
@@ -272,7 +272,11 @@ def list_payment_gateways(self) -> List[dict]:
             }
             for plugin in self.plugins
             if payment_method in type(plugin).__dict__
-            and self.get_plugin_configuration(plugin.PLUGIN_NAME).active
+            and (
+                self.get_plugin_configuration(plugin.PLUGIN_NAME).active
+                if active_only
+                else True
+            )
         ]
 
     def get_payment_template(self, gateway: str) -> str:
diff --git a/templates/dashboard/base.html b/templates/dashboard/base.html
index 9ff67f365f9..07ed7ab5c8c 100644
--- a/templates/dashboard/base.html
+++ b/templates/dashboard/base.html
@@ -176,7 +176,7 @@
                 </ul>
               </li>
             {% endif %}
-            {% if perms.site.manage_settings or perms.shipping.manage_shipping or perms.product.manage_products or perms.page.manage_pages %}
+            {% if perms.site.manage_settings or perms.shipping.manage_shipping or perms.product.manage_products or perms.page.manage_pages or perms.extensions.manage_plugins %}
               <li class="side-nav-section">
                 <p>
                   {% trans "Configuration"  context "Dashboard configuration" %}
@@ -227,6 +227,13 @@
                       </a>
                     </li>
                   {% endif %}
+                  {% if perms.extensions.manage_plugins %}
+                    <li class="{% block menu_payments_class %}{% endblock %}">
+                      <a href="{% url "dashboard:payments-index" %}">
+                        {% trans "Payments configuration" context "Dashboard static pages" %}
+                      </a>
+                    </li>
+                  {% endif %}
                 </ul>
               </li>
               <li class="side-nav-section" id="version-indicator">
diff --git a/templates/dashboard/payments/configuration_form.html b/templates/dashboard/payments/configuration_form.html
new file mode 100644
index 00000000000..abcb727516e
--- /dev/null
+++ b/templates/dashboard/payments/configuration_form.html
@@ -0,0 +1,62 @@
+{% extends "dashboard/base.html" %}
+{% load i18n %}
+{% load materializecss %}
+{% load static %}
+
+{% block title %}
+  {% trans "Payment configuration" context "Dashboard payments form" %} - {{ block.super }}
+{% endblock %}
+
+{% block body_class %}body-customers{% endblock %}
+{% block menu_payments_class %}active{% endblock %}
+
+{% block breadcrumbs %}
+<ul class="breadcrumbs breadcrumbs--history">
+    <li>
+      <a href="{% url "dashboard:payments-index" %}" class="breadcrumb">
+        {% trans "Payments configurations" context "Dashboard payment configuration page" %}
+      </a>
+    </li>
+    <li class="back-mobile">
+      <a href="{% url "dashboard:payments-index" %}">
+        <svg data-src="{% static "dashboard/images/arrow-left.svg" %}" fill="#fff" width="20px" height="20px" />
+      </a>
+    </li>
+    <li>
+      <span class="breadcrumbs--ellipsed-item">
+        {{ plugin_name }}
+      </span>
+    </li>
+  </ul>
+{% endblock %}
+
+{% block content %}
+  <div class="tab-content" id="details">
+    <div class="row">
+      <div class="col m12 l9">
+        <form method="post" class="card" id="form-config" novalidate>
+          {% csrf_token %}
+          <div class="card-content">
+            <div class="row">
+              <div class="col s12 l8">
+                {% for field in config_form %}
+                  <div class="row">
+                    {{ field|materializecss }}
+                  </div>
+                {% endfor %}
+              </div>
+            </div>
+          </div>
+          <div class="card-action right-align">
+            <a href="{% url "dashboard:payments-index" %}" class="btn-flat waves-effect">
+              {% trans "Cancel" context "Dashboard cancel action" %}
+            </a>
+            <button type="submit" class="btn waves-effect">
+              {% trans "Save" context "Dashboard update action" %}
+            </button>
+          </div>
+        </form>
+      </div>
+    </div>
+  </div>
+{% endblock %}
diff --git a/templates/dashboard/payments/index.html b/templates/dashboard/payments/index.html
new file mode 100644
index 00000000000..10c1c01a1de
--- /dev/null
+++ b/templates/dashboard/payments/index.html
@@ -0,0 +1,52 @@
+{% extends "dashboard/base.html" %}
+{% load i18n %}
+{% load materializecss %}
+{% load static %}
+{% load utils %}
+
+{% block title %}
+  {% trans "Payments" context "Dashboard payments form" %} - {{ block.super }}
+{% endblock %}
+
+{% block body_class %}body-customers{% endblock %}
+{% block menu_payments_class %}active{% endblock %}
+
+{% block breadcrumbs %}
+<ul class="breadcrumbs" >
+  <li class="visible-s">
+    {% trans "Payments configuration" context "Dashboard payment configuration page" %}
+  </li>
+</ul>
+{% endblock %}
+
+{% block content %}
+<div class="row">
+  <div class="col s12 l9">
+    {% if payment_gateways %}
+      <div class="card">
+        <div class="data-table-container">
+          <table class="bordered highlight responsive data-table">
+            <thead>
+              <tr>
+                {% trans "Name" context "Payment gateways table header" as label %}
+                {% sorting_header 'name' label %}
+              </tr>
+            </thead>
+            <tbody>
+              {% for gateway in payment_gateways%}
+                <tr data-action-go="{% url "dashboard:configure-payment" gateway %}">
+                  <td>
+                    {{ gateway }}
+                  </td>
+                </tr>
+              {% endfor %}
+            </tbody>
+          </table>
+        </div>
+      </div>
+      {% paginate payment_gateways %}
+    {% else %}
+    {% endif %}
+  </div>
+</div>
+{% endblock %}
diff --git a/tests/dashboard/test_payment.py b/tests/dashboard/test_payment.py
new file mode 100644
index 00000000000..288730a9f34
--- /dev/null
+++ b/tests/dashboard/test_payment.py
@@ -0,0 +1,200 @@
+import pytest
+from django.urls import reverse
+
+from saleor.dashboard.forms import ConfigBooleanField, ConfigCharField
+from saleor.dashboard.payment.forms import (
+    GatewayConfigurationForm,
+    create_custom_form_field,
+)
+from saleor.extensions.models import PluginConfiguration
+from saleor.payment.gateways.braintree.plugin import BraintreeGatewayPlugin
+
+
+@pytest.fixture
+def plugin_configuration(db):
+    plugin_configuration = PluginConfiguration.objects.create(
+        **BraintreeGatewayPlugin._get_default_configuration()
+    )
+
+    config = [
+        {"name": "Public API key", "value": "123456"},
+        {"name": "Secret API key", "value": "654321"},
+        {"name": "Merchant ID", "value": "0987654321"},
+        {"name": "Use sandbox", "value": False},
+        {"name": "Store customers card", "value": False},
+        {"name": "Automatic payment capture", "value": False},
+        {"name": "Require 3D secure", "value": True},
+    ]
+
+    BraintreeGatewayPlugin._update_config_items(
+        config, plugin_configuration.configuration
+    )
+    plugin_configuration.active = True
+    plugin_configuration.save()
+    return plugin_configuration
+
+
+@pytest.fixture
+def gateway_config_form():
+    return GatewayConfigurationForm(BraintreeGatewayPlugin)
+
+
+@pytest.fixture(autouse=True)
+def enable_plugin(settings):
+    settings.PLUGINS = [
+        "saleor.payment.gateways.braintree.plugin.BraintreeGatewayPlugin"
+    ]
+
+
+@pytest.fixture
+def config_char_structure():
+    return {
+        "name": "Template path",
+        "value": "order/payment/braintree.html",
+        "type": "String",
+        "help_text": "Location of django payment template for gateway.",
+        "label": "Template path",
+    }
+
+
+@pytest.fixture(autouse=True)
+def staff_user_plugin_manager(staff_user, permission_manage_plugins):
+    staff_user.user_permissions.add(permission_manage_plugins)
+    return staff_user
+
+
+@pytest.fixture()
+def form_data():
+    return {
+        "active": False,
+        "template-path": "template/example.html",
+        "public-api-key": "abcs",
+        "secret-api-key": "secret",
+        "use-sandbox": False,
+        "merchant-id": "merchant",
+        "store-customers-card": False,
+        "automatic-payment-capture": True,
+        "require-3d-secure": False,
+    }
+
+
+def test_configuration_form_get_current_configuration(
+    plugin_configuration, gateway_config_form
+):
+    assert gateway_config_form._get_current_configuration() == plugin_configuration
+
+
+def test_configuration_form__create_field(config_char_structure):
+    template_field = create_custom_form_field(config_char_structure)
+    assert isinstance(template_field, ConfigCharField)
+    assert template_field.label == config_char_structure["name"]
+    assert template_field.initial == config_char_structure["value"]
+
+
+def test_config_boolean_field_returned_value():
+    config_boolean_structure = {
+        "name": "Require 3D secure",
+        "value": True,
+        "type": "Boolean",
+        "help_text": "Determines if Saleor should enforce 3D secure during payment.",
+        "label": "Require 3D secure",
+    }
+    boolean_field = ConfigBooleanField(structure=config_boolean_structure)
+    assert boolean_field.clean(True) == {
+        "name": config_boolean_structure["name"],
+        "value": True,
+    }
+    assert boolean_field.clean(False) == {
+        "name": config_boolean_structure["name"],
+        "value": False,
+    }
+
+
+def test_config_char_field_returned_value(config_char_structure):
+    char_field = ConfigCharField(structure=config_char_structure)
+    value = "1234466"
+    assert char_field.clean(value) == {
+        "name": config_char_structure["name"],
+        "value": value,
+    }
+
+
+def test_payment_index_display_only_available_gateways(staff_client):
+    url = reverse("dashboard:payments-index")
+    response = staff_client.get(url)
+    assert response.status_code == 200
+    payment_gateways = response.context["payment_gateways"]
+    assert len(payment_gateways) == 1
+    assert BraintreeGatewayPlugin.PLUGIN_NAME in payment_gateways
+
+
+def test_configure_payment_gateway_returns_404_when_wrong_plugin_name(staff_client):
+    url = reverse("dashboard:configure-payment", args=["wrongValue"])
+    response = staff_client.get(url)
+    assert response.status_code == 404
+
+
+def test_configure_payment_gateway_returns_proper_form(staff_client):
+    url = reverse("dashboard:configure-payment", args=["Braintree"])
+    response = staff_client.get(url)
+    assert response.status_code == 200
+    assert isinstance(response.context["config_form"], GatewayConfigurationForm)
+
+
+@pytest.mark.parametrize(
+    "expected_error",
+    ["template-path", "public-api-key", "secret-api-key", "merchant-id"],
+)
+def test_gateway_configuration_form_all_fields_are_required(expected_error, form_data):
+    form_data.pop(expected_error)
+    form = GatewayConfigurationForm(BraintreeGatewayPlugin, form_data)
+    assert not form.is_valid()
+    assert expected_error in form.errors
+
+
+def test_gateway_configuration_form_not_checked_active_field_set_to_false():
+    data = {
+        "template-path": "template/example.html",
+        "public-api-key": "abcs",
+        "secret-api-key": "secret",
+        "merchant-id": "merchant",
+    }
+
+    form = GatewayConfigurationForm(BraintreeGatewayPlugin, data)
+    assert form.is_valid()
+    assert form.cleaned_data["active"] is False
+
+
+@pytest.mark.parametrize(
+    "field_name",
+    [
+        "use-sandbox",
+        "store-customers-card",
+        "automatic-payment-capture",
+        "require-3d-secure",
+    ],
+)
+def test_gateway_configuration_form_json_field_set_to_false(field_name, form_data):
+    form_data.pop(field_name)
+    form = GatewayConfigurationForm(BraintreeGatewayPlugin, form_data)
+    assert form.is_valid()
+    cleaned_data = form.cleaned_data
+    assert cleaned_data[field_name]["value"] is False
+
+
+def test_form_properly_save_plugin_config(form_data, plugin_configuration):
+    assert plugin_configuration.active
+    form = GatewayConfigurationForm(BraintreeGatewayPlugin, form_data)
+    assert form.is_valid()
+    form.save()
+    plugin_configuration.refresh_from_db()
+    assert plugin_configuration.active is False
+
+
+def test_configure_payment_gateway_properly_handle_form(
+    form_data, plugin_configuration, staff_client
+):
+    assert plugin_configuration.active
+    url = reverse("dashboard:configure-payment", args=["Braintree"])
+    response = staff_client.post(url, form_data)
+    assert response.status_code == 302
diff --git a/tests/dashboard/test_permissions.py b/tests/dashboard/test_permissions.py
index 47f28962b66..5be35c27f4e 100644
--- a/tests/dashboard/test_permissions.py
+++ b/tests/dashboard/test_permissions.py
@@ -988,3 +988,33 @@ def test_staff_can_remove_user(staff_client, staff_user, permission_manage_users
 
     response = staff_client.get(url)
     assert response.status_code == 200
+
+
+def test_payment_index_view_cannot_be_accessed_without_permision(
+    staff_client, staff_user, permission_manage_plugins
+):
+    url = reverse("dashboard:payments-index")
+    assert not staff_user.has_perm("extensions.manage_plugins")
+    response = staff_client.get(url)
+    assert response.status_code == 302
+
+    staff_user.user_permissions.add(permission_manage_plugins)
+    response = staff_client.get(url)
+    assert response.status_code == 200
+
+
+def test_only_staff_user_with_proper_permission_can_update_payment_gateway(
+    staff_client, staff_user, permission_manage_plugins, settings
+):
+    settings.PLUGINS = [
+        "saleor.payment.gateways.braintree.plugin.BraintreeGatewayPlugin"
+    ]
+
+    url = reverse("dashboard:configure-payment", args=["Braintree"])
+    assert not staff_user.has_perm("extensions.manage_plugins")
+    response = staff_client.get(url)
+    assert response.status_code == 302
+
+    staff_user.user_permissions.add(permission_manage_plugins)
+    response = staff_client.get(url)
+    assert response.status_code == 200


Code review comments:
diff_hunk: @@ -272,7 +272,11 @@ def list_payment_gateways(self) -> List[dict]:
             }
             for plugin in self.plugins
             if payment_method in type(plugin).__dict__
-            and self.get_plugin_configuration(plugin.PLUGIN_NAME).active
+            and (
path: saleor/extensions/manager.py
text: @user1:
Maybe it would be a good idea to create a normal `if` statement instead of a conditional assignment .  The amount of condition is at least here, we could increase readability. What do you think? @author

diff_hunk: @@ -0,0 +1,60 @@
+from typing import Dict
+
+from django import forms
+from django.utils.text import slugify
+
+from ...extensions import ConfigurationTypeField
+from ...extensions.models import PluginConfiguration
+from ..forms import ConfigBooleanField, ConfigCharField
+
+TYPE_TO_FIELD = {
+    ConfigurationTypeField.STRING: ConfigCharField,
+    ConfigurationTypeField.BOOLEAN: ConfigBooleanField,
+}
+
+
+def create_custom_form_field(structure: Dict[str, str]) -> forms.Field:
+    elem_type = structure["type"]
+    return TYPE_TO_FIELD[elem_type](structure=structure)
+
+
+class GatewayConfigurationForm(forms.ModelForm):
+    class Meta:
+        model = PluginConfiguration
+        fields = ("active",)
+
+    def __init__(self, plugin, *args, **kwargs):
+        self.plugin = plugin
+        kwargs["instance"] = self._get_current_configuration()
+        super().__init__(*args, **kwargs)
+        # add new fields specified for gateway
+        self.fields.update(self._prepare_fields_for_given_config(self.instance))
+
+    def _get_current_configuration(self) -> PluginConfiguration:
+        qs = PluginConfiguration.objects.all()
+        return self.plugin.get_plugin_configuration(qs)
+
+    def _prepare_fields_for_given_config(
+        self, current_configuration: PluginConfiguration
+    ) -> Dict[str, forms.Field]:
+        parsed_fields = {}
+        structure = current_configuration.configuration
+        if structure is None:
+            raise Exception
path: saleor/dashboard/payment/forms.py
text: @user1:
I think that we can do nothing when configuriation is None. We have simillar flow for the Vatlayer nad Dashboard2.0. We don't have any fields in configuration we just show option to active/disable Vatlayer.