diff --git a/requirements.txt b/requirements.txt index edf60c6..7583329 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ django-braces==1.14.0 django-crispy-forms==1.8.1 django-filter==2.2.0 django-tables2==2.2.1 -django==3.0.2 +django==3.0.3 pytz==2019.3 # via django pyyaml==5.3 six==1.14.0 # via django-braces diff --git a/requirements/dev.txt b/requirements/dev.txt index 8f5a3f7..74ccbc6 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -4,56 +4,57 @@ # # pip-compile requirements/dev.in # -appdirs==1.4.3 # via black + +appdirs==1.4.3 # via black, virtualenv asgiref==3.2.3 # via django aspy.yaml==1.3.0 # via pre-commit astroid==2.3.3 # via pylint attrs==19.3.0 # via black -autopep8==1.4.4 +autopep8==1.5 backcall==0.1.0 # via ipython black==19.10b0 certifi==2019.11.28 # via requests -cfgv==2.0.1 # via pre-commit +cfgv==3.0.0 # via pre-commit chardet==3.0.4 # via requests click==7.0 # via black coverage==5.0.3 decorator==4.4.1 # via ipython, traitlets defusedxml==0.6.0 # via python3-openid +distlib==0.3.0 # via virtualenv django-allauth==0.41.0 django-braces==1.14.0 django-crispy-forms==1.8.1 django-filter==2.2.0 django-tables2==2.2.1 -django==3.0.2 # via django-allauth, django-braces, django-filter, django-tables2, model-mommy +django==3.0.3 # via django-allauth, django-braces, django-filter, django-tables2, model-mommy entrypoints==0.3 # via flake8 -filelock==3.0.12 # via tox +filelock==3.0.12 # via tox, virtualenv flake8==3.7.9 -identify==1.4.10 # via pre-commit +identify==1.4.11 # via pre-commit idna==2.8 # via requests -importlib-metadata==1.4.0 # via pluggy, pre-commit, tox -importlib-resources==1.0.2 # via pre-commit +importlib-metadata==1.5.0 # via pluggy, pre-commit, tox, virtualenv +importlib-resources==1.0.2 # via pre-commit, virtualenv ipdb==0.12.3 ipython-genutils==0.2.0 # via traitlets -ipython==7.11.1 # via ipdb +ipython==7.12.0 # via ipdb isort==4.3.21 -jedi==0.15.2 # via ipython +jedi==0.16.0 # via ipython lazy-object-proxy==1.4.3 # via astroid mccabe==0.6.1 # via flake8, pylint model-mommy==2.0.0 -more-itertools==8.1.0 # via zipp mypy-extensions==0.4.3 # via mypy mypy==0.761 -nodeenv==1.3.4 # via pre-commit +nodeenv==1.3.5 # via pre-commit oauthlib==3.1.0 # via requests-oauthlib -packaging==20.0 # via tox -parso==0.5.2 # via jedi +packaging==20.1 # via tox +parso==0.6.1 # via jedi pathspec==0.7.0 # via black pep8==1.7.1 -pexpect==4.7.0 # via ipython +pexpect==4.8.0 # via ipython pickleshare==0.7.5 # via ipython pluggy==0.13.1 # via tox -pre-commit==1.21.0 -prompt-toolkit==3.0.2 # via ipython +pre-commit==2.0.1 +prompt-toolkit==3.0.3 # via ipython psycopg2-binary==2.8.4 ptyprocess==0.6.0 # via pexpect py==1.8.1 # via tox @@ -71,21 +72,21 @@ pyyaml==5.3 # via aspy.yaml, pre-commit regex==2020.1.8 # via black requests-oauthlib==1.3.0 # via django-allauth requests==2.22.0 # via django-allauth, requests-oauthlib -six==1.14.0 # via astroid, cfgv, django-braces, packaging, pre-commit, tox, traitlets +six==1.14.0 # via astroid, django-braces, packaging, tox, traitlets, virtualenv snowballstemmer==2.0.0 # via pydocstyle sqlparse==0.3.0 # via django tablib==1.0.0 toml==0.10.0 # via black, pre-commit, tox -tox==3.14.3 +tox==3.14.4 traitlets==4.3.3 # via ipython typed-ast==1.4.1 # via astroid, black, mypy typing-extensions==3.7.4.1 # via mypy -urllib3==1.25.7 # via requests -virtualenv==16.7.9 # via pre-commit, tox +urllib3==1.25.8 # via requests +virtualenv==20.0.3 # via pre-commit, tox wcwidth==0.1.8 # via prompt-toolkit wrapt==1.11.2 # via astroid yapf==0.29.0 -zipp==1.0.0 # via importlib-metadata +zipp==2.2.0 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/tests/artist_app/urls.py b/tests/artist_app/urls.py index e553cf6..ab1435d 100644 --- a/tests/artist_app/urls.py +++ b/tests/artist_app/urls.py @@ -12,6 +12,7 @@ custom_default_patterns = views.CustomDefaultActions().url_patterns() patterns_42 = views.Artist42CRUD().url_patterns() plainform_patterns = views.PlainFormCRUD().url_patterns() +create_artist_only_patterns = views.CreateOnlyCRUD().url_patterns() urlpatterns = ( @@ -30,4 +31,5 @@ + perms_crud_song_patterns + patterns_42 + plainform_patterns + + create_artist_only_patterns ) diff --git a/tests/artist_app/views.py b/tests/artist_app/views.py index 26be44d..a278313 100644 --- a/tests/artist_app/views.py +++ b/tests/artist_app/views.py @@ -250,3 +250,13 @@ class BandCRUD(VegaCRUDView): model = Band protected_actions: Union[None, List[str]] = None permissions_actions: Union[None, List[str]] = None + + +class CreateOnlyCRUD(VegaCRUDView): + """Vega CRUD view created with plain form.""" + + model = Artist + protected_actions: Union[None, List[str]] = None + permissions_actions: Union[None, List[str]] = None + actions = ["create"] + crud_path = "create-artist-only" diff --git a/tests/test_crud.py b/tests/test_crud.py index 7c34ceb..7616316 100644 --- a/tests/test_crud.py +++ b/tests/test_crud.py @@ -209,7 +209,6 @@ def test_custom_views(self): def test_custom_default_views(self): """Test custom default views.""" - artist = mommy.make("artist_app.Artist") url = reverse("custom-default-actions-list") res = self.client.get(url) diff --git a/tests/test_utils.py b/tests/test_utils.py index 1b139a4..738d1d2 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -7,6 +7,7 @@ from django.forms import CharField, ModelForm from django.test import TestCase, override_settings +from crispy_forms.bootstrap import FormActions from django_filters import FilterSet from django_tables2 import Table from model_mommy import mommy @@ -38,10 +39,24 @@ def test_customize_modelform(self): self.assertTrue(issubclass(custom_form_class, PlainArtistForm)) self.assertEqual("VegaCustomFormClass", custom_form_class.__name__) + # test that form kwargs have been set try: - custom_form_class(**{settings.VEGA_MODELFORM_KWARG: dict()}) + form = custom_form_class( + **{"request": None, settings.VEGA_MODELFORM_KWARG: dict()} + ) except TypeError: self.fail("Form kwargs have not been set properly") + else: + self.assertTrue(hasattr(form, "helper")) + self.assertEqual(form.helper.form_method, "post") + self.assertEqual(form.helper.form_tag, True) + self.assertEqual(form.helper.form_show_labels, True) + self.assertEqual(form.helper.include_media, True) + self.assertEqual(form.helper.render_required_fields, True) + self.assertEqual(form.helper.html5_required, True) + self.assertEqual(form.helper.form_id, "artist-form") + self.assertEqual(form.helper.layout.fields[0], "name") + self.assertTrue(isinstance(form.helper.layout.fields[1], FormActions)) def test_get_modelform(self): """Test get_modelform""" diff --git a/tests/test_views.py b/tests/test_views.py index 5992241..874ae28 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,6 +1,4 @@ -""" -vega-admin module to test views -""" +"""vega-admin module to test views.""" from django.conf import settings from django.contrib.auth.models import Permission, User from django.contrib.contenttypes.models import ContentType @@ -8,134 +6,135 @@ from model_mommy import mommy -from vega_admin.views import (VegaCreateView, VegaCRUDView, VegaDeleteView, - VegaDetailView, VegaListView, VegaUpdateView) +from vega_admin.views import ( + VegaCreateView, + VegaCRUDView, + VegaDeleteView, + VegaDetailView, + VegaListView, + VegaUpdateView, +) from .artist_app.forms import ArtistForm from .artist_app.models import Artist, Song -from .artist_app.views import (ArtistCreate, ArtistDelete, ArtistListView, - ArtistRead, ArtistUpdate) +from .artist_app.views import ( + ArtistCreate, + ArtistDelete, + ArtistListView, + ArtistRead, + ArtistUpdate, +) class TestViewsBase(TestCase): - """ - Base test class for views - """ - - def _song_permissions(self): - """ - Create permissions - """ + """Base test class for views.""" + + maxDiff = None + + def _song_permissions(self): # pylint: disable=no-self-use + """Create permissions.""" content_type = ContentType.objects.get_for_model(Song) list_permission, _ = Permission.objects.get_or_create( - codename='list_song', + codename="list_song", content_type=content_type, - defaults=dict(name='Can List Songs'), + defaults=dict(name="Can List Songs"), ) create_permission, _ = Permission.objects.get_or_create( - codename='create_song', + codename="create_song", content_type=content_type, - defaults=dict(name='Can Create Songs'), + defaults=dict(name="Can Create Songs"), ) view_permission, _ = Permission.objects.get_or_create( - codename='view_song', + codename="view_song", content_type=content_type, - defaults=dict(name='Can View Songs'), + defaults=dict(name="Can View Songs"), ) update_permission, _ = Permission.objects.get_or_create( - codename='update_song', + codename="update_song", content_type=content_type, - defaults=dict(name='Can Update Songs'), + defaults=dict(name="Can Update Songs"), ) delete_permission, _ = Permission.objects.get_or_create( - codename='delete_song', + codename="delete_song", content_type=content_type, - defaults=dict(name='Can Delete Songs'), + defaults=dict(name="Can Delete Songs"), ) artists_permission, _ = Permission.objects.get_or_create( - codename='artists_song', + codename="artists_song", content_type=content_type, - defaults=dict(name='Can List Song Artists'), + defaults=dict(name="Can List Song Artists"), ) - return [list_permission, create_permission, update_permission, - delete_permission, artists_permission, view_permission, ] + return [ + list_permission, + create_permission, + update_permission, + delete_permission, + artists_permission, + view_permission, + ] - def _artist_permissions(self): - """ - Create permissions - """ + def _artist_permissions(self): # pylint: disable=no-self-use + """Create permissions.""" content_type = ContentType.objects.get_for_model(Artist) list_permission, _ = Permission.objects.get_or_create( - codename='list_artist', + codename="list_artist", content_type=content_type, - defaults=dict(name='Can List Artists'), + defaults=dict(name="Can List Artists"), ) other_permission, _ = Permission.objects.get_or_create( - codename='other_artist', + codename="other_artist", content_type=content_type, - defaults=dict(name='Can `Other` Artists'), + defaults=dict(name="Can `Other` Artists"), ) - return [list_permission, other_permission, ] + return [list_permission, other_permission] def setUp(self): - """setUp""" + """Set up.""" super().setUp() - self.maxDiff = None self.user = User.objects.create_user( - username='mosh', - email='mosh@example.com', - password='hunter2', + username="mosh", email="mosh@example.com", password="hunter2" ) def tearDown(self): - """tearDown""" + """Tear down.""" super().tearDown() Song.objects.all().delete() Artist.objects.all().delete() Permission.objects.filter( - content_type=ContentType.objects.get_for_model(Song)).delete() + content_type=ContentType.objects.get_for_model(Song) + ).delete() Permission.objects.filter( - content_type=ContentType.objects.get_for_model(Artist)).delete() + content_type=ContentType.objects.get_for_model(Artist) + ).delete() User.objects.all().delete() -@override_settings( - ROOT_URLCONF="tests.artist_app.urls", - VEGA_TEMPLATE="basic" -) +@override_settings(ROOT_URLCONF="tests.artist_app.urls", VEGA_TEMPLATE="basic") class TestViews(TestViewsBase): - """ - Test class for views - """ + """Test class for views.""" def test_vega_crud_view(self): - """ - Test VegaCRUDView - """ + """Test VegaCRUDView.""" default_actions = ["create", "view", "update", "list", "delete"] class ArtistCrud(VegaCRUDView): + """ArtistCrud class.""" + model = Artist view = ArtistCrud() self.assertEqual(Artist, view.model) - self.assertEqual( - list(set(default_actions)), list(set(view.get_actions()))) + self.assertEqual(list(set(default_actions)), list(set(view.get_actions()))) self.assertEqual("artist_app.artist", view.crud_path) self.assertEqual("artist_app", view.app_label) self.assertEqual("artist", view.model_name) - self.assertEqual( - Artist, view.get_view_class_for_action("create")().model) - self.assertEqual( - Artist, view.get_view_class_for_action("update")().model) - self.assertEqual( - Artist, view.get_view_class_for_action("delete")().model) - self.assertEqual( - Artist, view.get_view_class_for_action("list")().model) - self.assertEqual( - Artist, view.get_view_class_for_action("view")().model) + self.assertEqual(Artist, view.get_view_class_for_action("create")().model) + self.assertEqual(Artist, view.get_view_class_for_action("update")().model) + self.assertEqual(Artist, view.get_view_class_for_action("delete")().model) + self.assertEqual(Artist, view.get_view_class_for_action("list")().model) + self.assertEqual(Artist, view.get_view_class_for_action("view")().model) self.assertIsInstance( view.get_view_class_for_action("create")(), VegaCreateView @@ -146,10 +145,8 @@ class ArtistCrud(VegaCRUDView): self.assertIsInstance( view.get_view_class_for_action("delete")(), VegaDeleteView ) - self.assertIsInstance( - view.get_view_class_for_action("list")(), VegaListView) - self.assertIsInstance( - view.get_view_class_for_action("view")(), VegaDetailView) + self.assertIsInstance(view.get_view_class_for_action("list")(), VegaListView) + self.assertIsInstance(view.get_view_class_for_action("view")(), VegaDetailView) self.assertEqual( f"{view.crud_path}/create/", @@ -184,15 +181,12 @@ class ArtistCrud(VegaCRUDView): for action in default_actions: self.assertEqual( - f"{view.crud_path}-{action}", - view.get_url_name_for_action(action) + f"{view.crud_path}-{action}", view.get_url_name_for_action(action) ) @override_settings(VEGA_FORCE_ORDERING=True, VEGA_ORDERING_FIELD=["pk"]) def test_vega_list_view(self): - """ - Test VegaListView - """ + """Test VegaListView.""" Artist.objects.all().delete() artist = mommy.make("artist_app.Artist", name="Bob") @@ -207,21 +201,18 @@ def test_vega_list_view(self): self.assertTemplateUsed(res, "vega_admin/basic/list.html") res = self.client.get("/list/artists/?q=Bob") - self.assertDictEqual({ - "q": "Bob" - }, res.context["vega_listview_search_form"].initial) + self.assertDictEqual( + {"q": "Bob"}, res.context["vega_listview_search_form"].initial + ) self.assertEqual( - set(["q", ]), - set(res.context["vega_listview_search_form"].fields.keys()) + set(["q"]), set(res.context["vega_listview_search_form"].fields.keys()) ) self.assertEqual(res.context["object_list"].count(), 1) self.assertTrue(res.context["object_list"].ordered) self.assertEqual(res.context["object_list"].first(), artist) def test_vega_view_view(self): - """ - Test VegaReadView - """ + """Test VegaReadView.""" artist = mommy.make("artist_app.Artist", name="Bob") res = self.client.get(f"/view/artists/view/{artist.id}") self.assertEqual(res.status_code, 200) @@ -230,9 +221,7 @@ def test_vega_view_view(self): self.assertTemplateUsed(res, "vega_admin/basic/read.html") def test_vega_create_view(self): - """ - Test VegaCreateView - """ + """Test VegaCreateView.""" res = self.client.get("/edit/artists/create/") self.assertEqual(res.status_code, 200) self.assertIsInstance(res.context["form"], ArtistForm) @@ -244,8 +233,7 @@ def test_vega_create_view(self): self.assertRedirects(res, "/edit/artists/create/") self.assertQuerysetEqual(Artist.objects.all(), [""]) self.assertTrue( - settings.VEGA_FORM_VALID_CREATE_TXT in - res.cookies["messages"].value + settings.VEGA_FORM_VALID_CREATE_TXT in res.cookies["messages"].value ) self.assertFalse( settings.VEGA_FORM_INVALID_TXT in res.cookies["messages"].value @@ -253,13 +241,10 @@ def test_vega_create_view(self): res = self.client.post("/edit/artists/create/", {}) self.assertEqual(res.status_code, 200) - self.assertTrue( - settings.VEGA_FORM_INVALID_TXT in res.cookies["messages"].value) + self.assertTrue(settings.VEGA_FORM_INVALID_TXT in res.cookies["messages"].value) def test_vega_update_view(self): - """ - Test VegaUpdateView - """ + """Test VegaUpdateView.""" artist = mommy.make("artist_app.Artist", name="Bob") res = self.client.get(f"/edit/artists/edit/{artist.id}") self.assertEqual(res.status_code, 200) @@ -267,15 +252,13 @@ def test_vega_update_view(self): self.assertIsInstance(res.context["view"], ArtistUpdate) self.assertIsInstance(res.context["view"], VegaUpdateView) self.assertTemplateUsed(res, "vega_admin/basic/update.html") - res = self.client.post( - f"/edit/artists/edit/{artist.id}", {"name": "Mosh"}) + res = self.client.post(f"/edit/artists/edit/{artist.id}", {"name": "Mosh"}) self.assertEqual(res.status_code, 302) self.assertRedirects(res, f"/edit/artists/edit/{artist.id}") artist.refresh_from_db() self.assertEqual("Mosh", artist.name) self.assertTrue( - settings.VEGA_FORM_VALID_UPDATE_TXT in - res.cookies["messages"].value + settings.VEGA_FORM_VALID_UPDATE_TXT in res.cookies["messages"].value ) self.assertFalse( settings.VEGA_FORM_INVALID_TXT in res.cookies["messages"].value @@ -283,13 +266,10 @@ def test_vega_update_view(self): res = self.client.post(f"/edit/artists/edit/{artist.id}", {}) self.assertEqual(res.status_code, 200) - self.assertTrue( - settings.VEGA_FORM_INVALID_TXT in res.cookies["messages"].value) + self.assertTrue(settings.VEGA_FORM_INVALID_TXT in res.cookies["messages"].value) def test_vega_delete_view(self): - """ - Test ArtistDelete - """ + """Test ArtistDelete.""" artist = mommy.make("artist_app.Artist", name="Bob") res = self.client.get(f"/edit/artists/delete/{artist.id}") self.assertEqual(res.status_code, 200) @@ -307,7 +287,17 @@ def test_vega_delete_view(self): mommy.make("artist_app.Song", name="Nuts", artist=artist2) res = self.client.post(f"/edit/artists/delete/{artist2.id}") self.assertTrue( - settings.VEGA_DELETE_PROTECTED_ERROR_TXT in - res.cookies["messages"].value + settings.VEGA_DELETE_PROTECTED_ERROR_TXT in res.cookies["messages"].value ) self.assertTrue(Artist.objects.filter(id=artist2.id).exists()) + + def test_vega_crud_view_no_list_action(self): + """Test Vega CRUD view with no list action.""" + res = self.client.get("/create-artist-only/create/") + # we are checking that the cancel button is rendered as expected + self.assertContains( + res, + """Cancel""", + status_code=200, + html=True, + ) diff --git a/vega_admin/contrib/users/forms.py b/vega_admin/contrib/users/forms.py index 3d36d37..7717600 100644 --- a/vega_admin/contrib/users/forms.py +++ b/vega_admin/contrib/users/forms.py @@ -10,7 +10,7 @@ from crispy_forms.bootstrap import Field from crispy_forms.layout import Layout -from vega_admin.crispy_utils import get_form_actions, get_form_helper_class +from vega_admin.crispy_utils import get_default_formhelper, get_form_actions try: # pylint: disable=import-error @@ -30,22 +30,6 @@ def validate_unique_email(value): return value -def get_formhelper(): - """ - Get form helper class. - This is simply for convenience and to avoid pylint warning about - duplicate code. - """ - return get_form_helper_class( - form_method="POST", - form_tag=True, - form_show_labels=True, - include_media=True, - render_required_fields=True, - html5_required=True, - ) - - class UserFormMixin: # pylint: disable=too-few-public-methods """User forms mixin""" @@ -93,7 +77,7 @@ def __init__(self, *args, **kwargs): self.fields["username"].required = False self.fields["username"].help_text = _(settings.VEGA_USERNAME_HELP_TEXT) self.fields["email"].help_text = _(settings.VEGA_OPTIONAL_TXT) - self.helper = get_formhelper() + self.helper = get_default_formhelper() self.helper.form_id = "add-user-form" self.helper.layout = Layout( Field("first_name"), @@ -172,7 +156,7 @@ def __init__(self, *args, **kwargs): self.vega_extra_kwargs = kwargs.pop("vega_extra_kwargs", dict()) super().__init__(*args, **kwargs) self.fields["email"].help_text = _(settings.VEGA_OPTIONAL_TXT) - self.helper = get_formhelper() + self.helper = get_default_formhelper() self.helper.form_id = "edit-user-form" self.helper.layout = Layout( Field("first_name"), @@ -193,7 +177,7 @@ def __init__(self, *args, **kwargs): self.request = kwargs.pop("request", None) self.vega_extra_kwargs = kwargs.pop("vega_extra_kwargs", dict()) super().__init__(user=self.instance, *args, **kwargs) - self.helper = get_formhelper() + self.helper = get_default_formhelper() self.helper.form_id = "change-password-form" self.helper.layout = Layout( Field("password1"), diff --git a/vega_admin/crispy_utils.py b/vega_admin/crispy_utils.py index d441bb0..06e929a 100644 --- a/vega_admin/crispy_utils.py +++ b/vega_admin/crispy_utils.py @@ -79,6 +79,23 @@ def get_form_helper_class( # pylint: disable=too-many-arguments,bad-continuatio return helper +def get_default_formhelper(): + """ + Get form helper class with reasonable defaults. + + This is simply for convenience because it represents a very commonly used + form helper class. + """ + return get_form_helper_class( + form_method="POST", + form_tag=True, + form_show_labels=True, + include_media=True, + render_required_fields=True, + html5_required=True, + ) + + def get_layout( # pylint: disable=bad-continuation formfields: List[str], with_actions: bool = False, cancel_url: str = "/" ) -> Layout: diff --git a/vega_admin/utils.py b/vega_admin/utils.py index ac48106..719f83a 100644 --- a/vega_admin/utils.py +++ b/vega_admin/utils.py @@ -14,7 +14,7 @@ import django_tables2 as tables from django_filters import FilterSet -from vega_admin.crispy_utils import get_form_helper_class, get_layout +from vega_admin.crispy_utils import get_default_formhelper, get_layout from vega_admin.mixins import VegaFormMixin @@ -73,14 +73,7 @@ def _constructor(self, *args, **kwargs): self.vega_extra_kwargs = kwargs.pop(settings.VEGA_MODELFORM_KWARG, dict()) super(modelform_class, self).__init__(*args, **kwargs) # add crispy forms FormHelper - self.helper = get_form_helper_class( - form_tag=True, - form_method="POST", - render_required_fields=True, - form_show_labels=True, - html5_required=True, - include_media=True, - ) + self.helper = get_default_formhelper() self.helper.form_id = f"{self.model._meta.model_name}-form" self.helper.layout = get_layout( self.fields.keys(), @@ -252,7 +245,8 @@ def customize_modelform(form_class: Union[forms.Form, forms.ModelForm]): """ Add custom keyword arguments to a provided form class. - Adds the custom kwargs required for vega_admin, if they are missing. + - Adds the custom kwargs required for vega_admin, if they are missing. + - Adds a crispy form helper if missing Arguments: form_class {Union[Form, ModelForm]} -- the form class @@ -279,6 +273,14 @@ def __init__(self, *args, **kwargs): settings.VEGA_MODELFORM_KWARG, dict() ) super().__init__(*args, **kwargs) + if not hasattr(self, "helper"): + self.helper = get_default_formhelper() + self.helper.form_id = f"{self.Meta.model._meta.model_name}-form" + self.helper.layout = get_layout( + self.fields.keys(), + with_actions=True, + cancel_url=self.vega_extra_kwargs.get("cancel_url", "/"), + ) return VegaCustomFormClass