From 1e8834eae761a06533f5ec897361331092efc759 Mon Sep 17 00:00:00 2001 From: saxix Date: Mon, 8 Jul 2024 10:41:03 +0200 Subject: [PATCH] ad ability do inspect django mddels --- src/hope_flex_fields/admin/fieldset.py | 90 ++++++++++++++++++- src/hope_flex_fields/apps.py | 10 +-- src/hope_flex_fields/models/definition.py | 21 ++++- .../flex_fields/fieldset/analyse.html | 44 +++++++++ src/hope_flex_fields/utils.py | 28 +++++- tests/.coveragerc | 1 + tests/admin/test_admin_fieldset.py | 27 ++++++ tests/extra/testutils/factories.py | 4 +- tests/models/test_model_definition.py | 7 ++ 9 files changed, 217 insertions(+), 15 deletions(-) create mode 100644 src/hope_flex_fields/templates/flex_fields/fieldset/analyse.html diff --git a/src/hope_flex_fields/admin/fieldset.py b/src/hope_flex_fields/admin/fieldset.py index 426df8b..f9936c4 100644 --- a/src/hope_flex_fields/admin/fieldset.py +++ b/src/hope_flex_fields/admin/fieldset.py @@ -1,15 +1,79 @@ +from django import forms from django.contrib import messages from django.contrib.admin import ModelAdmin, register +from django.contrib.contenttypes.models import ContentType +from django.db.transaction import atomic +from django.forms import modelform_factory from django.shortcuts import render from admin_extra_buttons.decorators import button from admin_extra_buttons.mixins import ExtraButtonsMixin from ..forms import FieldsetForm -from ..models import Fieldset +from ..models import FieldDefinition, Fieldset, FlexField +from ..utils import get_kwargs_from_formfield from .flexfield import FieldsetFieldTabularInline +class FieldSetForm(forms.Form): + content_type = forms.ModelChoiceField(queryset=ContentType.objects.all()) + + def analyse(self): + ct: ContentType = self.cleaned_data["content_type"] + model_class = ct.model_class() + model_form = modelform_factory( + model_class, exclude=(model_class._meta.pk.name,) + ) + errors = [] + fields = [] + config = {} + for name, field in model_form().fields.items(): + try: + fd = FieldDefinition.objects.get(name=type(field).__name__) + fld = FlexField( + name=name, field=fd, attrs=get_kwargs_from_formfield(field) + ) + fld.attrs = fld.get_merged_attrs() + fields.append(fld) + config["name"] = {"definition": fd.name, "attrs": fld.attrs} + fld.get_field() + except FieldDefinition.DoesNotExist: + errors.append( + { + "name": name, + "error": f"Field definition for '{type(field).__name__}' does not exist", + } + ) + return { + "fields": fields, + "errors": errors, + "config": config, + "content_type": ct, + } + + +class FieldSetForm2(forms.Form): + content_type = forms.ModelChoiceField( + queryset=ContentType.objects.all(), widget=forms.HiddenInput + ) + config = forms.JSONField(widget=forms.HiddenInput) + + def save(self): + with atomic(): + ct: ContentType = self.cleaned_data["content_type"] + model_class = ct.model_class() + fs, __ = Fieldset.objects.get_or_create( + name=f"{model_class._meta.app_label}_{model_class._meta.model_name}" + ) + for name, info in self.cleaned_data["config"].items(): + fd = FieldDefinition.objects.get(name=info["definition"]) + fs.fields.get_or_create(name=name, field=fd, attrs=info["attrs"]) + + +class inspect_field: + pass + + @register(Fieldset) class FieldsetAdmin(ExtraButtonsMixin, ModelAdmin): list_select_related = True @@ -18,6 +82,30 @@ class FieldsetAdmin(ExtraButtonsMixin, ModelAdmin): inlines = [FieldsetFieldTabularInline] form = FieldsetForm + @button() + def create_from_content_type(self, request): + ctx = self.get_common_context(request, title="Create from ContentType") + if request.method == "POST": + if "config" in request.POST: + form = FieldSetForm2(request.POST, request.FILES) + form.is_valid() + form.save() + else: + form = FieldSetForm(request.POST, request.FILES) + if form.is_valid(): + result = form.analyse() + ctx.update(**result) + form = FieldSetForm2( + initial={ + "content_type": result["content_type"], + "config": result["config"], + } + ) + else: + form = FieldSetForm() + ctx["form"] = form + return render(request, "flex_fields/fieldset/analyse.html", ctx) + @button() def inspect(self, request, pk): ctx = self.get_common_context(request, pk, title="Inspect") diff --git a/src/hope_flex_fields/apps.py b/src/hope_flex_fields/apps.py index 13142a1..40faf93 100644 --- a/src/hope_flex_fields/apps.py +++ b/src/hope_flex_fields/apps.py @@ -1,21 +1,13 @@ from django.apps import AppConfig from django.db.models.signals import post_migrate -from strategy_field.utils import fqn - def create_default_fields(sender, **kwargs): from hope_flex_fields.models import FieldDefinition - from hope_flex_fields.models.base import get_default_attrs from hope_flex_fields.registry import field_registry - from hope_flex_fields.utils import get_kwargs_for_field for fld in field_registry: - FieldDefinition.objects.get_or_create( - name=fld.__name__, - field_type=fqn(fld), - defaults={"attrs": get_kwargs_for_field(fld, get_default_attrs())}, - ) + FieldDefinition.objects.get_from_django_field(fld) class Config(AppConfig): diff --git a/src/hope_flex_fields/models/definition.py b/src/hope_flex_fields/models/definition.py index 0d1175a..a4c0b3a 100644 --- a/src/hope_flex_fields/models/definition.py +++ b/src/hope_flex_fields/models/definition.py @@ -1,12 +1,15 @@ import logging +from inspect import isclass +from django import forms from django.db import models from django.db.models import UniqueConstraint from django.utils.translation import gettext as _ from strategy_field.fields import StrategyClassField +from strategy_field.utils import fqn -from hope_flex_fields.utils import get_kwargs_for_field +from hope_flex_fields.utils import get_kwargs_from_field_class from ..fields import FlexFormMixin from ..registry import field_registry @@ -20,6 +23,20 @@ class FieldDefinitionManager(models.Manager): def get_by_natural_key(self, name): return self.get(name=name) + def get_from_django_field(self, django_field: "forms.Field|type[forms.Field]"): + if isinstance(django_field, forms.Field): + fld = type(django_field) + elif isclass(django_field) and issubclass(django_field, forms.Field): + fld = django_field + else: + raise ValueError(django_field) + name = fld.__name__ + return FieldDefinition.objects.get_or_create( + name=name, + field_type=fqn(fld), + defaults={"attrs": get_kwargs_from_field_class(fld, get_default_attrs())}, + )[0] + class FieldDefinition(AbstractField): field_type = StrategyClassField(registry=field_registry) @@ -49,7 +66,7 @@ def set_default_arguments(self): if not isinstance(self.attrs, dict) or not self.attrs: self.attrs = get_default_attrs() if self.field_type: - attrs = get_kwargs_for_field(self.field_type) + attrs = get_kwargs_from_field_class(self.field_type) attrs.update(**self.attrs) self.attrs = attrs diff --git a/src/hope_flex_fields/templates/flex_fields/fieldset/analyse.html b/src/hope_flex_fields/templates/flex_fields/fieldset/analyse.html new file mode 100644 index 0000000..d5d3887 --- /dev/null +++ b/src/hope_flex_fields/templates/flex_fields/fieldset/analyse.html @@ -0,0 +1,44 @@ +{% extends "admin_extra_buttons/action_page.html" %} +{% block action-content %} + {{ original }} + {% if fields %} +
+ {% csrf_token %} + {{ form }} + +
+ {% if errors %} +

Errors

+ + {% for err in errors %} + + + + + {% endfor %} +
{{ err.name }}{{ err.error }}
+ {% endif %} +
+

Fields

+ + {% for fld in fields %} + + + + + + {% endfor %} +
{{ fld }}{{ fld.field }}{{ fld.attrs }}
+ {% else %} +
+ + {% csrf_token %} + {{ form }} +
+ +
+
+ {% endif %} +
+ +{% endblock %} diff --git a/src/hope_flex_fields/utils.py b/src/hope_flex_fields/utils.py index 3493f76..b2c857b 100644 --- a/src/hope_flex_fields/utils.py +++ b/src/hope_flex_fields/utils.py @@ -1,5 +1,7 @@ import inspect +from django import forms +from django.forms.fields import DateTimeFormatsIterator from django.utils.text import slugify @@ -7,7 +9,7 @@ def namefy(value): return slugify(value).replace("-", "_") -def get_kwargs_for_field(field, extra: dict | None = None): +def get_kwargs_from_field_class(field, extra: dict | None = None): sig: inspect.Signature = inspect.signature(field) arguments = extra or {} field_arguments = { @@ -17,3 +19,27 @@ def get_kwargs_for_field(field, extra: dict | None = None): } arguments.update(field_arguments) return arguments + + +def get_kwargs_from_formfield(field: forms.Field): + from hope_flex_fields.models import FieldDefinition + + fd = FieldDefinition.objects.get(name=type(field).__name__) + ret = {} + for attr_name in fd.attrs.keys(): + if attr_name in ( + "widget", + "validators", + "error_messages", + "error_messages", + "help_text", + "label", + ): + continue + value = getattr(field, attr_name) + # if attr_name in ("help_text", "label"): + # value = str(value) + if isinstance(value, DateTimeFormatsIterator): + value = [str(v) for v in value] + ret[attr_name] = value + return ret diff --git a/tests/.coveragerc b/tests/.coveragerc index 9d2d738..18f074b 100644 --- a/tests/.coveragerc +++ b/tests/.coveragerc @@ -24,6 +24,7 @@ exclude_lines = if DEBUG: except NoReverseMatch: if TYPE_CHECKING: + raise ValueError(...) ignore_errors = True diff --git a/tests/admin/test_admin_fieldset.py b/tests/admin/test_admin_fieldset.py index 1a820a0..9ca647a 100644 --- a/tests/admin/test_admin_fieldset.py +++ b/tests/admin/test_admin_fieldset.py @@ -1,4 +1,6 @@ from django import forms +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType from django.urls import reverse import pytest @@ -8,6 +10,8 @@ FlexFieldFactory, ) +from hope_flex_fields.models import Fieldset + pytestmark = [pytest.mark.admin, pytest.mark.smoke, pytest.mark.django_db] @@ -33,3 +37,26 @@ def test_fieldset_test(app, record): res = res.forms["test"].submit() messages = [s.message for s in res.context["messages"]] assert messages == ["Valid"] + + +@pytest.mark.parametrize( + "model_class", + [ + User, + ], +) +def test_fieldset_create_from_content_type(app, model_class): + url = reverse("admin:hope_flex_fields_fieldset_create_from_content_type") + res = app.get(url) + res = res.forms["analyse-form"].submit() + assert res.status_code == 200 + res.forms["analyse-form"]["content_type"] = ContentType.objects.get_for_model( + model_class + ).pk + res = res.forms["analyse-form"].submit() + res.forms["create-form"].submit() + fs = Fieldset.objects.filter( + name=f"{model_class._meta.app_label}_{model_class._meta.model_name}" + ).first() + assert fs + assert fs.fields.exists() diff --git a/tests/extra/testutils/factories.py b/tests/extra/testutils/factories.py index 5917279..fa58a34 100644 --- a/tests/extra/testutils/factories.py +++ b/tests/extra/testutils/factories.py @@ -78,9 +78,9 @@ class Meta: @classmethod def _create(cls, model_class, *args, **kwargs): if "attrs" in kwargs: - from hope_flex_fields.utils import get_kwargs_for_field + from hope_flex_fields.utils import get_kwargs_from_field_class - attrs = get_kwargs_for_field(kwargs["field_type"]) + attrs = get_kwargs_from_field_class(kwargs["field_type"]) attrs.update(**kwargs["attrs"]) kwargs["attrs"] = attrs return super()._create(model_class, *args, **kwargs) diff --git a/tests/models/test_model_definition.py b/tests/models/test_model_definition.py index b985735..cf7bbb3 100644 --- a/tests/models/test_model_definition.py +++ b/tests/models/test_model_definition.py @@ -48,3 +48,10 @@ def test_override(db): with pytest.raises(ValidationError) as e: field.clean(11) assert e.value.messages == [r"Invalid format. Allowed Regex is '\d$'"] + + +@pytest.mark.parametrize("form_field", [forms.CharField(), forms.CharField]) +def test_get_from_django_field(db, form_field): + from hope_flex_fields.models import FieldDefinition + + assert FieldDefinition.objects.get_from_django_field(form_field)