Hassan Baig
Hassan Baig

Reputation: 15844

Displaying validation errors at the right position in a Django page containing multiple form fields

In a Django social networking website I built, users can chat in a general room, or create private groups.

Each user has a main dashboard where all the conversations they're a part of appear together, stacked over one another (paginated by 20 objects). I call this the unseen activity page. Every unseen conversation on this page has a text box a user can directly type a reply into. Such replies are submitted via a POST request inside a <form>.

The action attribute of each <form> points to different urls, depending on which type of reply was submitted (e.g. home_comment, or group_reply). This is because they have different validation and processing requirements, etc.

The problem is this: If a ValidationError is raised (e.g. the user typed a reply with forbidden characters), it gets displayed on multiple forms in the unseen_activity page, instead of just the particular form it was generated from. How can I ensure all ValidationErrors solely appear over the form they originated from? An illustrative example would be great!


The form class attached to all this is called UnseenActivityForm, and is defined as such:

class UnseenActivityForm(forms.Form):
    comment = forms.CharField(max_length=250)
    group_reply = forms.CharField(max_length=500)
    class Meta:
        fields = ("comment", "group_reply", )

    def __init__(self,*args,**kwargs):
        self.request = kwargs.pop('request', None)
        super(UnseenActivityForm, self).__init__(*args, **kwargs)

    def clean_comment(self):
        # perform some validation checks
        return comment

    def clean_group_reply(self):
        # perform some validation checks
        return group_reply

The template looks like so:

{% for unseen_obj in object_list %}

    {% if unseen_obj.type == '1' %}

    {% if form.comment.errors %}{{ form.comment.errors.0 }}{% endif %}
    <form method="POST" action="{% url 'process_comment' pk %}">
    {% csrf_token %}
    {{ form.comment }}
    <button type="submit">Submit</button>
    </form>

    {% if unseen_obj.type == '2' %}

    {% if form.group_reply.errors %}{{ form.group_reply.errors.0 }}{% endif %}
    <form method="POST" action="{% url 'process_group_reply' pk %}">
    {% csrf_token %}
    {{ form.group_reply }}
    <button type="submit">Submit</button>
    </form>

    {% endif %}

{% endfor %}

And now for the views. I don't process everything in a single one. One function takes care of generating the content for the GET request, and others take care handling POST data processing. Here goes:

def unseen_activity(request, slug=None, *args, **kwargs):
        form = UnseenActivityForm()
        notifications = retrieve_unseen_notifications(request.user.id)
        page_num = request.GET.get('page', '1')
        page_obj = get_page_obj(page_num, notifications, ITEMS_PER_PAGE)
        if page_obj.object_list:
            oblist = retrieve_unseen_activity(page_obj.object_list)
        else:
            oblist = []
        context = {'object_list': oblist, 'form':form, 'page':page_obj,'nickname':request.user.username}
        return render(request, 'user_unseen_activity.html', context)

def unseen_reply(request, pk=None, *args, **kwargs):
        if request.method == 'POST':
            form = UnseenActivityForm(request.POST,request=request)
            if form.is_valid():
                # process cleaned data
            else:
                notifications = retrieve_unseen_notifications(request.user.id)
                page_num = request.GET.get('page', '1')
                page_obj = get_page_obj(page_num, notifications, ITEMS_PER_PAGE)
                if page_obj.object_list:
                    oblist = retrieve_unseen_activity(page_obj.object_list)
                else:
                    oblist = []
                context = {'object_list': oblist, 'form':form, 'page':page_obj,'nickname':request.user.username}
                return render(request, 'user_unseen_activity.html', context)

def unseen_group_reply(group_reply, pk=None, *args, **kwargs):
            #similar processing as unseen_reply

Note: the code is a simplified version of my actual code. Ask for more details in case you need them.

Upvotes: 1

Views: 612

Answers (1)

AKS
AKS

Reputation: 19861

Following the discussion in the comments above:

What I suggest is that you create a form for each instance in the view. I have refactored your code to have a function which returns object lists which you can use in both unseen_reply and group_reply functions:

def get_object_list_and_forms(request):
    notifications = retrieve_unseen_notifications(request.user.id)
    page_num = request.GET.get('page', '1')
    page_obj = get_page_obj(page_num, notifications, ITEMS_PER_PAGE)
    if page_obj.object_list:
        oblist = retrieve_unseen_activity(page_obj.object_list)
    else:
        oblist = []

    # here create a forms dict which holds form for each object 
    forms = {}
    for obj in oblist:
        forms[obj.pk] = UnseenActivityForm()

    return page_obj, oblist, forms


def unseen_activity(request, slug=None, *args, **kwargs):
    page_obj, oblist, forms = get_object_list_and_forms(request)

    context = {
        'object_list': oblist,
        'forms':forms,
        'page':page_obj,
        'nickname':request.user.username
    }
    return render(request, 'user_unseen_activity.html', context)

Now, you need to access the form in template using the object id from forms dict.

{% for unseen_obj in object_list %}
    <!-- use the template tag in the linked post to get the form using obj pk -->
    {% with forms|get_item:unseen_obj.pk as form %}
        {% if unseen_obj.type == '1' %}

            {% if form.comment.errors %}{{ form.comment.errors.0 }}{% endif %}
            <form method="POST" action="{% url 'process_comment' pk %}">
                {% csrf_token %}
                {{ form.comment }}
                <button type="submit">Submit</button>
            </form>

        {% elif unseen_obj.type == '2' %}

            {% if form.group_reply.errors %}{{ form.group_reply.errors.0 }}{% endif %}
            <form method="POST" action="{% url 'process_group_reply' pk %}">
                {% csrf_token %}
                {{ form.group_reply }}
                <button type="submit">Submit</button>
            </form>

        {% endif %}
    {% endwith %}
{% endfor %}

While processing the reply, you again need to attach the form which throws error with the particular object pk:

def unseen_reply(request, pk=None, *args, **kwargs):
    if request.method == 'POST':
        form = UnseenActivityForm(request.POST,request=request)
        if form.is_valid():
        # process cleaned data
        else:
            page_obj, oblist, forms = get_object_list_and_forms(request)

            # explicitly set the form which threw error for this pk
            forms[pk] = form

            context = {
                'object_list': oblist,
                'forms':forms,
                'page':page_obj,
                'nickname':request.user.username
            }
            return render(request, 'user_unseen_activity.html', context)

Upvotes: 1

Related Questions