Search code examples
djangodjango-formsinfinite-scrollpaginator

Using Django paginator to display multiple Django forms page by page but unable to save the data from the pages


I am using Django paginator to create an input page that display multiple forms as single pages. With the paginator in place i would like to enable endless scrolling.

I have asked questions to ChatGPT and so far the code provided does not save the data. The latest solution does not even show the save all button as intended. I am not sure if the data is actually stored in the session before changing to the next page as every time i change page, the form is blank.

below is the view function and html template. The generation of the page with the correct sub template is working.

`# views.py
    from django.core.paginator import Paginator
    from django.core.exceptions import ObjectDoesNotExist
    from django.core.exceptions import ValidationError
    from django.shortcuts import render, redirect, get_object_or_404
    from django.urls import reverse
    from django.contrib.auth import authenticate, logout
    from django.contrib import messages
    from django.http import JsonResponse, HttpResponseRedirect
from django.forms.models import model_to_dict
from django.db import transaction

...

# Define a function to handle the final save operation
def save_all_forms(form_classes, form_data_list):
    # Start a database transaction
    with transaction.atomic():
        # Save the first form instance
        first_form_class = form_classes[0]
        first_form_instance = first_form_class(**form_data_list[0]).save()

        # Save subsequent form instances
        instances = [first_form_instance]
        for form_class, form_data in zip(form_classes[1:], form_data_list[1:]):
            form_instance = form_class(**form_data)
            # Assuming the foreign key to the first form is named 'page1'
            setattr(form_instance, 'page1', first_form_instance)
            form_instance.save()
            instances.append(form_instance)

        # Convert model instances to dictionaries for any further processing
        instances_dict = [model_to_dict(instance) for instance in instances]
        return instances_dict  # This list can be used for further processing if needed



def ler_new_pages_application_fraud(request):
    form_classes = [LossEventPage1Form, LossEventPage2Form, DummyForm]
    form_data_list = request.session.get('form_data_list', [{} for _ in form_classes])
    all_forms_valid = all(form_data for form_data in form_data_list)

    if request.method == 'POST':
        # This is the page of the current form to validate.
        page_number = request.GET.get('page', 1)
        try:
            page_number = int(page_number)
        except ValueError:
            page_number = 1
        page_number = max(1, min(page_number, len(form_classes)))

        # Get the form class for the current form to validate
        current_form_class = form_classes[page_number - 1]
        current_form = current_form_class(request.POST, request.FILES)

        if current_form.is_valid():
            # Store the cleaned data from the form into the session
            form_data_list[page_number - 1] = current_form.cleaned_data
            request.session['form_data_list'] = form_data_list
            all_forms_valid = all(form_data for form_data in form_data_list)

            if 'save_all' in request.POST and all_forms_valid:
                # Save all forms here
                instances = save_all_forms(form_classes, form_data_list)
                del request.session['form_data_list']  # Clear the session data after saving
                return redirect('ler_listing')  # Redirect to a success page
            elif page_number < len(form_classes):
                # Redirect to the next form page
                return redirect(f"{reverse('pages-application-fraud')}?page={page_number + 1}")

    # If not POST or forms are not valid, display current form
    page_number = request.GET.get('page', 1)
    try:
        page_number = int(page_number)
    except ValueError:
        page_number = 1
    page_number = max(1, min(page_number, len(form_classes)))
    current_form_class = form_classes[page_number - 1]
    current_form = current_form_class(initial=form_data_list[page_number - 1])

    paginator = Paginator(form_classes, 1)
    page_obj = paginator.get_page(page_number)

    context = {
        'form': current_form,
        'page_obj': page_obj,
        'all_forms_valid': all_forms_valid,
    }

    return render(request, 'ler_new_pages_application_fraud.html', context)

#html template
{% extends 'base.html' %}
{% load static %}

{% block content %}
{% if all_forms_valid %}
<form method="post">
    {% csrf_token %}
    <button name="save_all" type="submit">Save All</button>
</form>
{% else %}
<form method="post" enctype="multipart/form-data">
    {% csrf_token %}

    {% if  page_obj.number == 1 %}
    {% include 'subtemplate/lerCommonPage01p.html' %}
    {% endif %}

    {% if  page_obj.number == 2 %}
    {% include 'subtemplate/lerCommonPage02p.html' %}
    {% endif %}

    {% if  page_obj.number == 3 %}
    {% include 'subtemplate/lerCommonPage03.html' %}
    {% endif %}
</form>

{% endif %}

<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
        <a href="?page=1">&laquo; first</a>
        <a href="?page={{ page_obj.previous_page_number }}">previous</a>
        {% endif %}

        <span class="current">
            Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
        </span>

        {% if page_obj.has_next %}
        <a href="?page={{ page_obj.next_page_number }}">next</a>
        <a href="?page={{ page_obj.paginator.num_pages }}">last &raquo;</a>
        {% endif %}
    </span>
</div>




<script src="{% static 'js/jquery_3_5_1.min.js'%}"></script>

<!-- Add the following JavaScript code -->
<script>
    $(document).ready(function () {
        var max_length = 1500;  // Set your desired maximum character length
        var descriptionField = $('#id_incidentSummary');  // Replace 'id_description' with your field's ID

        descriptionField.on('input', function () {
            var current_length = descriptionField.val().length;
            var remaining = max_length - current_length;
            $('#char-count-incidentSummary').text(remaining + ' characters remaining');
        });
    });
</script>

{% endblock %}

#models.py
class LossEventPage1(models.Model):
    code = models.CharField(default=1, max_length=30)
    description = models.CharField(max_length=512)
    reportingEntity = models.ManyToManyField(website.models.ReportingEntity)
    id_Org = models.ForeignKey(website.models.Org, on_delete=models.SET_NULL, null=True)
    incidentSummary = models.CharField(max_length=1000)
    id_Location = models.ForeignKey(
        website.models.Location, on_delete=models.SET_NULL, null=True
    )
    locationDesc = models.CharField(max_length=4000)
    timesurvey = models.TimeField()  # Time Of Event Detection
    date_survey = models.DateField()  # Date Of Event Detection
    amount_involved = models.DecimalField(max_digits=12, decimal_places=2)
    amount_involved_estd = models.DecimalField(max_digits=12, decimal_places=2)


class LossEventPage2(models.Model):
    lossEventPage1 = models.OneToOneField(
        LossEventPage1, on_delete=models.CASCADE, null=True,
        related_name='losseventpage2'
    )
    incidentSummary = models.CharField(max_length=1000)

#forms.py
from django import forms
        
class DummyForm(forms.Form):
    pass  # No form fields required

class LossEventPage1Form(forms.ModelForm):
    date_survey = forms.DateField(widget=forms.DateInput(attrs={"type": "date"}))
    timesurvey = forms.TimeField(widget=forms.TimeInput(attrs={"type": "time"}))

    class Meta:
        model = LossEventPage1
        fields = [
            "description",
            "reportingEntity",
            "id_Org",
            "timesurvey",
            "date_survey",
            "id_Location",
            "locationDesc",
            "amount_involved",
            "amount_involved_estd",
 
        ]
        widgets = {
            "reportingEntity": forms.CheckboxSelectMultiple,
        }

class LossEventPage2Form(forms.ModelForm):
    incidentSummary = forms.CharField(
        widget=forms.Textarea(attrs={"rows": 4, "cols": 50, "maxlength": 1000}),
        label="Incident Summary",
        help_text="Enter a description (max 1000 characters)",
    )

    class Meta:
        model = LossEventPage2
        fields = ["incidentSummary"]`

i have asked chatGPT for solutions and each solution does not work so far.

I am trying to create an input page that uses multiple forms and to show each form as a single page with django paginator. Once I can save all form data to Database I am going to use the paginator form to enable endless scroll of the input page.


Solution

  • To begin with, the functionality you are trying to implement (multiple forms on the same page) is inherently tough in Django, albeit certainly doable.

    However, the way you're currently doing it with paginators is counter-intuitive, and the clue that you're 'working against' the more standard way to do this is the size of your view. It is very large, and that's already with a large external function for form saving.

    Also, you write:

    I am not sure if the data is actually stored in the session before changing to the next page as every time i change page, the form is blank.

    In your current implementation, this data will not be stored to the session without specifying the following:

    request.session.modified = True
    

    And you must specify this after any change to the session.

    Data also won't persist in the browser, and the reason is that default pagination in Django involves GET Querystring URL Parameters; therefore it is not single-page. When you go from mysite.com/objects/?page=1 to mysite.com/objects/?page=2, you get a complete page refresh with no data saved either to the database (requires a valid POST request) or to your browser (requires at a minimum some complex JS or a frontend framework); and without the request.session.modified = True argument, it's not saving to the session either.

    Therefore, my first recommendation would be to rethink this implementation; certainly the pagination, but potentially the whole approach. It seems overly complex.

    Now the best implementation will depend on your business logic, but some good places to start may be as follows:

    • Firstly, is there a way to wrap all the small, individual forms together into 1 big form? If so, you will find the behaviour becomes much more manageable.
    • If not, is there a model with M2M relationships that you can define which encapsulates the multi-form logic, and would this then permit you to serve a single form? Again, if so, you will find this implementation much easier.

    If you cannot do either of the above, then that is fine although it will be tricky. You should still move away from default Django Pagination for sure.

    It sounds like you want a 'single-page' style feel for your frontend, with data persisting over time between each 'form instance'.

    For this you will either need a lot of custom vanilla JS you implement yourself, or some helper libraries. Personally, I find the best two frontend libraries for this that integrate nicely with Django are HTMX (hugely useful for async POST requests, which you may find you need to implement) and Alpine (a very lightweight frontend lib, a bit like Vue, which you can probably use to replicate the 'pagination' but in a way that makes data persist over time).

    Then for each Form, you will need to handle it carefully inside your views. If it is a Form for an object that already exists (ie. an update form), then make sure you are correctly passing it the 'instance' parameter; you can do this either through the URL (if you define it to include the instance id) or through a hidden 'id' field in the form itself. I would recommend the URL based approach, as it is paradigmatically how Django is built to deal with forms, and if you then want to have multiple forms on the same page with data persisting over time you can use HTMX to handle changes to those forms, with data from each form going to the endpoint with the appropriate URL for the form instance's ID (i.e. you have 'multiple' forms on the page, with the backend logic working nicely for 'single' forms - blissfully unaware that there are several on the same page, with the POST requests going asynchronously to different endpoints).