1

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.

Hassan Baig
  • 15,055
  • 27
  • 102
  • 205
  • Please also add the view where you are creating the form and unseen objects. It looks like you are using the same form for all the unseen objects. Looking at the template it also seems that the same URL is being used in all the forms. – AKS Feb 21 '17 at 10:54
  • @AKS: Indeed, you're right. I added views as well, have a look. What would be a workaround here? – Hassan Baig Feb 21 '17 at 15:50
  • You need to create separate form for each unseen activity to make sure that only the form related to certain activity displays the error. Also, the `action` you are using in each form is not utilizing the `pk` url params at all. – AKS Feb 22 '17 at 06:48
  • @AKS: sorry the `action` was erroneously shown - I've amended it (in my real code, I did include the `pk`). Re: making separate forms for each unseen_activity, this is the part that's confusing me. I'm generating the forms in a for loop - there can be many unseen activities (so I paginate it by 20). How would I create them manually with a unique identity here? Can't seem to wrap my head around it. Can you give me an example? – Hassan Baig Feb 22 '17 at 06:57
  • The `form` you are using in the for loop is created in the view and passed to template through context. So you are using the same form instance for all the unseen activities. What you need to do is create a separate form for each unseen activity in the view context itself. And, have a mapping between the form and the activity which you can use to render the form later. – AKS Feb 22 '17 at 07:00

1 Answers1

1

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)
Community
  • 1
  • 1
AKS
  • 18,983
  • 3
  • 43
  • 54
  • 1
    I see. Very nuanced answer, exciting to try it out right now. I'll get back to you in a bit, thanks a bunch for chiming in mate :-) – Hassan Baig Feb 22 '17 at 21:09
  • Btw I got curious while thinking about this: would `formsets` have worked for my particular problem? – Hassan Baig Feb 23 '17 at 15:21