barseghyanartur / django-fobi

Form generator/builder application for Django done right: customisable, modular, user- and developer- friendly.
https://pypi.python.org/pypi/django-fobi
485 stars 112 forks source link

Trying to create extensible model/forms with data stored in JSON field #257

Closed amitjindal closed 3 years ago

amitjindal commented 3 years ago

Hi, I am trying to implement the following but am not sure how to go about it. I tried searching in docs as well as examples and even source code. But unable to understand how I would do the following. I think this should be easy but I am just not able to get this working.

Use Case

I want to have a model that can be extended at runtime with data stored in JSON field.

Tried this

This is just an example. My use case is a bit more complicated. Say I have a Employee. I want the ability for this employee model to have custom fields. I want to go into the admin and add some fields.

I visited /fobi/* pages and already created a form named 'contrator' (slug also contractor).

Now I want to display this underneath an employee form.

So trying something like:

class Employee(models.Model):
    # Fields
    name = models.CharField(max_length=25)
    slug = models.SlugField()
    created = models.DateTimeField(auto_now_add=True, editable=False)
    updated = models.DateTimeField(auto_now=True, editable=False)
    joiningDate = models.DateField()
    department = models.ForeignKey(to=Department, on_delete=CASCADE)

class EmployeeForm(forms.ModelForm):
    class Meta:
        model = Employee
        fields = ['name', 'slug', 'joiningDate', 'department']

class EmployeeCreateView(CreateView):
    model = Employee
    form_class = EmployeeForm

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        contractor_form = FormEntry.objects.get(slug="contractor")
        ctx["contractor"] = contractor_form
        return ctx

    def form_valid(self, form):
        return super().form_valid(form)
{% load crispy_forms_tags fobi_tags %}

            <form method="post">
            {% csrf_token %}
              {% crispy form %}
              {% crispy contractor_form %}
            <button class="btn btn-primary pull-right" type="submit">{% trans "Save" %}</button>
            </form>

Result

This did not work. I get an error:

'FormEntry' object has no attribute 'is_bound'

I also tried to add snippets/form_snippet.html for crispy but that also did not work. I must be missing something small. What object do I pass or how do I render this form?

Ideally I would like to capture the submission in a JSON field in Employee.

Please help.

barseghyanartur commented 3 years ago

I have to tell you, you've done it all wrong.

https://github.com/barseghyanartur/django-fobi#rendering-forms-using-third-party-libraries

It is possible to render the form using third party libs (like crispy forms or other), but in order to get to a form, you need more than just a form object. You have to reproduce most of the parts of the form rendering view.

https://github.com/barseghyanartur/django-fobi/blob/v0.17.x/src/fobi/views.py#L2109

amitjindal commented 3 years ago

@barseghyanartur Thanks for the advice. I spent some time reading the code and found out how to make it work.

I reduced the code to this:

def get_dynamic_form(request: HttpRequest, form_slug: str, initial):
    """View created form.

    :param django.http.HttpRequest request:
    :param string form_slug:
    :return DynamicForm
    """
    form_cls = get_dynamic_form_class(request, form_slug)

    form = None
    if request.POST:
        form = form_cls(request.POST, request.FILES)
    else:
        # Providing initial form data by feeding entire GET dictionary to the form, if ``GET_PARAM_INITIAL_DATA``
        # is present in the GET.
        kwargs = {}
        if GET_PARAM_INITIAL_DATA in request.GET:
            kwargs = {'initial': request.GET}
        form = form_cls(**kwargs)
    return form

def get_dynamic_form_class(request: HttpRequest, slug: str):
    try:
        form_entry = FormEntry.manager.get(slug=slug)
    except FormEntry.DoesNotExist as err:
        raise None
    form_element_entries = form_entry.formelemententry_set.all()
    # This is where the form is being built dynamically.
    form_cls = assemble_form_class(form_entry, form_element_entries=form_element_entries, request=request)
    return form_cls

Now in the view I can simply do:

class EmployeeCreateView(CreateView):
    model = Employee
    form_class = EmployeeForm

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        diary_form = get_dynamic_form(self.request, "diary")
        ctx["diary_form"] = diary_form
        return ctx

    def post(self, request, *args, **kwargs):
        """
        Handle POST requests: instantiate a form instance with the passed
        POST variables and then check if it's valid.
        """
        form = self.get_form()
        diary_form = get_dynamic_form(request, "diary")
        if form.is_valid() and diary_form.is_valid():
            print(diary_form.cleaned_data)
            return self.form_valid(form)
        else:
            return self.form_invalid(form)
            <form method="post">
            {% csrf_token %}
              {{ form | crispy }}
              {{ diary_form | crispy }}
            <button class="btn btn-primary pull-right" type="submit">{% trans "Save" %}</button>
            </form>

Leaving this here in case someone else needs it.

I later realized this is GPL licensed and I won't be able to use this in our project. Thanks for your work on this anyway. This is a really good project.


Updated: Added get_dynamic_form_class

barseghyanartur commented 3 years ago

@amitjindal:

As of licensing, it's dual license (GPL/LGPL). Thus, I'm more than sure you can use it in your project. :)

amitjindal commented 3 years ago

@barseghyanartur Thank you for pointing that out. I again missed it. I sincerely thank you for helping me with this and letting me know about the LGPL. I will do further work with this if possible. If I end up using this (or even not) and I find some interesting use cases I will post them here.

barseghyanartur commented 3 years ago

@amitjindal:

In the example you've given, you probably forgot to include the implementation of the get_dynamic_form function (which should be some wrapper around fobi.dynamic.assemble_form_class). That would be useful for others to see. And I even might add this as an example to the FAQ.

amitjindal commented 3 years ago

@barseghyanartur Its there. That's the first part of my sample above.

amitjindal commented 3 years ago

I also need to find how to separate/use form creation/editing as I need them to show nested in an existing form. I will post that code also here once I find out.

BTW the sample above works correctly. I am bypassing all handlers etc as I simply want the form to validate the data and give me cleaned_data. I plan to store it in a JSONField.

Once I cleanup I will provide the cleaned up code as well. I was thinking if there is a way to build a custom FormField that encapsulated the Dynamic form you generated. Then the form field will directly use the cleaned data to populate itself. But I am not as familiar with custom form fields so I am at a loss here. Will try this week.

barseghyanartur commented 3 years ago

@barseghyanartur Its there. That's the first part of my sample above.

Ah, yep, but the get_dynamic_form_class isn't.

As of custom FormField you've mentioned, I've done similar things in Django (a long long time ago). You can even define a single form field which produces multiple fields.

amitjindal commented 3 years ago

@barseghyanartur My bad. Added it to sample above. My mind is not concentrated today. Sorry.

I will try the custom form field or some way so that it integrates properly with others. Will let you know.