Arsalan Shahid

Extending StackDuplica with Answering Features – StackDuplica Part 2

My dear reader, how are you? السلام عليكم

Go as far as you can see; when you get there, you will be able to see further – Thomas Carlyle

This is the second part of StackDuplica web-Application tutorial series. We add an answering feature for questions posted by users on our platform. We will essentially create a detail view for questions to display the question itself, the list of available answers. an option to add more questions and finally allow the asker to approve or disapprove an answer.


Few useful links to practically follow the project:

  1. StackDuplica GitHub repository — DirectMe
  2. All other tutorials on StackDuplica — DirectMe
  3. Set your GitHub repo to StackDuplica Part 1 using the following command:
git fetch 04b9dd5036f9bb6036a0fb7aabcf7caaf3f727d9

Adding Question detail View

Follow the steps as shown below

1) We will start by first creating an answer form as shown below

# open qanda/forms.py and add the following program

class AnswerForm(forms.ModelForm):
    user = forms.ModelChoiceField(
        widget=forms.HiddenInput,
        queryset=get_user_model().objects.all(),
        disabled=True,
    )
    question = forms.ModelChoiceField(
        widget=forms.HiddenInput,
        queryset=Question.objects.all(),
        disabled=True,
    )

    class Meta:
        model = Answer
        fields = ['answer', 'user', 'question', ]

# open qanda/forms.py and add the following Answer acceptance forms

class AnswerAcceptanceForm(forms.ModelForm):
    accepted = forms.BooleanField(
        widget=forms.HiddenInput,
        required=False,
    )

    class Meta:
        model = Answer
        fields = ['accepted', ]

2) Let us now create a question detail view that will list the answers with an option to accept and reject the answers.

# open qanda/views.py and add the following question detail view

from .forms import AnswerForm, AnswerAcceptanceForm
from .models import Answer

class QuestionDetailView(DetailView):
    model = Question

    ACCEPT_FORM = AnswerAcceptanceForm(initial={'accepted': True})
    REJECT_FORM = AnswerAcceptanceForm(initial={'accepted': False})

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx.update({
            'answer_form': AnswerForm(initial={
                'user': self.request.user.id,
                'question': self.object.id,
            })
        })
        if self.object.can_accept_answers(self.request.user):
            ctx.update({
                'accept_form': self.ACCEPT_FORM,
                'reject_form': self.REJECT_FORM,
            })
        return ctx

3) Let us now add a template for the Question Detail view. We will create templates for all the things that we want in our detail view and then add all of them in one main template using a {% include %} tag

# Create qanda/templates/qanda/question_detail.html and add the following program

{% extends "base.html" %}

{% block title %}{{ question.title }} - {{ block.super }}{% endblock %}

{% block body %}
  {% include "qanda/common/display_question.html" %}
  {% include "qanda/common/list_answers.html" %}
  {% if user.is_authenticated %}
    {% include "qanda/common/post_answer.html" %}
  {% else %}
    <div >Login to post answers.</div >
  {% endif %}
{% endblock %}

# create a qanda/common/display_question.html and add the following program

{% load markdownify %}
<div class="question" >
  <div class="meta col-sm-12" >
    <h1 >{{ question.title }}</h1 >
    Asked by {{ question.user }} on {{ question.created }}
  </div >

  <div class="body col-sm-12" >
    {{ question.question|markdownify }}
  </div >
</div >

# create a qanda/common/list_answers.html and add the following program

{% load markdownify %}
<h3 >Answers</h3 >
<ul class="list-unstyled answers" >
  {% for answer in question.answer_set.all %}
    <li class="answer row" >
      <div class="col-sm-3 col-md-2 text-center" >
        {% if answer.accepted %}
          <span class="badge badge-pill badge-success" >Accepted</span >
        {% endif %}
        {% if answer.accepted and reject_form %}
          <form method="post"
                action="{% url "qanda:update_answer_acceptance" pk=answer.id %}" >
            {% csrf_token %}
            {{ reject_form }}
            <button type="submit" class="btn btn-link" >
              <i class="fa fa-times" aria-hidden="true" ></i>
              Reject
            </button >
          </form >
        {% elif accept_form %}
          <form method="post"
                action="{% url "qanda:update_answer_acceptance" pk=answer.id %}" >
            {% csrf_token %}
            {{ accept_form }}
            <button type="submit" class="btn btn-link" title="Accept answer" >
              <i class="fa fa-check-circle" aria-hidden="true"></i >
              Accept
            </button >
          </form >
        {% endif %}
      </div >
      <div class="col-sm-9 col-md-10" >
        <div class="body" >{{ answer.answer|markdownify }}</div >
        <div class="meta font-weight-light" >
          Answered by {{ answer.user }} on {{ answer.created }}
        </div >
      </div >
    </li >
  {% empty %}
    <li class="answer" >No answers yet!</li >
  {% endfor %}
</ul >

# create a qanda/common/post_answer.html and add the following program

{% load crispy_forms_tags %}

<div class="col-sm-12" >
<h3 >Post your answer</h3 >
<form method="post"
action="{% url "qanda:answer_question" pk=question.id %}" >
{{ answer_form | crispy }}
{% csrf_token %}
<button class="btn btn-primary" type="submit" name="action"
value="PREVIEW" >Preview
</button >
<button class="btn btn-primary" type="submit" name="action"
value="SAVE" >Answer
</button >
</form >
</div >

4) Finally, add a route to question detail view as shown below

# open qanda/urls.py and update with the following roution for question_detail

from django.urls.conf import path
from qanda import views

app_name = 'qanda'

urlpatterns = [
    path('ask', views.AskQuestionView.as_view(), name='ask'),
    path('question/<int:pk>', views.QuestionDetailView.as_view(),name='question_detail'),
]

5) Let us now add a create answer view as shown below

class CreateAnswerView(LoginRequiredMixin, CreateView):
    form_class = AnswerForm
    template_name = 'qanda/create_answer.html'

    def get_initial(self):
        return {
            'question': self.get_question().id,
            'user': self.request.user.id,
        }

    def get_context_data(self, **kwargs):
        return super().get_context_data(question=self.get_question(),
                                        **kwargs)

    def get_success_url(self):
        return self.object.question.get_absolute_url()

    def form_valid(self, form):
        action = self.request.POST.get('action')
        if action == 'SAVE':
            # save and redirect as usual.
            return super().form_valid(form)
        elif action == 'PREVIEW':
            ctx = self.get_context_data(preview=form.cleaned_data['answer'])
            return self.render_to_response(context=ctx)
        return HttpResponseBadRequest()

    def get_question(self):
        return Question.objects.get(pk=self.kwargs['pk'])

6) Add a template for creating the answers for questions as shown below

# create qanda/templates/qanda/create_answer.html and add the following program

{% extends "base.html" %}
{% load markdownify %}

{% block body %}
  {% include 'qanda/common/display_question.html' %}
  {% if preview %}
    <div class="card question-preview" >
      <div class="card-header" >
        Answer Preview
      </div >
      <div class="card-body" >
        {{ preview|markdownify }}
      </div >
    </div >
  {% endif %}
  {% include 'qanda/common/post_answer.html' with answer_form=form %}
{% endblock %}

7) Add a route to Create answer view

# open qanda/urls.py and add the following path 

path('question/<int:pk>/answer', views.CreateAnswerView.as_view(),name='answer_question'),

8) Add an update view for the answer acceptance view

# open qanda/views.py and the following view

class UpdateAnswerAcceptanceView(LoginRequiredMixin, UpdateView):
    form_class = AnswerAcceptanceForm
    queryset = Answer.objects.all()

    def get_success_url(self):
        return self.object.question.get_absolute_url()

    def form_invalid(self, form):
        return HttpResponseRedirect(
            redirect_to=self.object.question.get_absolute_url())

9) Add a route to update answer acceptance view

# open qanda/urls.py and add the following path 

path('question/<int:pk>/accept', views.UpdateAnswerAcceptanceView.as_view(),name='update_answer_acceptance'),

Create a daily question list Feature

Let us first add a view for daily questions as shown below

# open qanda/views.py and add the following view

from django.views.generic import DayArchiveView

class DailyQuestionList(DayArchiveView):
    queryset = Question.objects.all()
    date_field = 'created'
    month_format = '%m'
    allow_empty = True

Add a template for daily question list as shown below

# create qanda/templates/qanda/question_archive_day.html and add the following program

{% extends "base.html" %}

{% block title %} Questions on {{ day }} {% endblock %}

{% block body %}
<div class="col-sm-12" >
<h1 >Highest Voted Questions of {{ day }}</h1 >
<ul >
{% for question in object_list %}
<li >
{{ question.votes }}
<a href="{{ question.get_absolute_url }}" >
{{ question }}
</a >
by
{{ question.user }}
on {{ question.created }}
</li >
{% empty %}
<li>Hmm... Everyone thinks they know everything today.</li>
{% endfor %}
</ul >
<div>
{% if previous_day %}
<a href="{% url "qanda:daily_questions" year=previous_day.year month=previous_day.month day=prev
ious_day.day %}" >
<< Previous Day
</a >
{% endif %}
{% if next_day %}
<a href="{% url "qanda:daily_questions" year=next_day.year month=next_day.month day=next_day.day
%}" >
Next Day >>
</a >
{% endif %}
</div >
</div >
{% endblock %}

Let us now add a route to daily question list a shown below

# open qanda/urls.py and add the following path 

path('daily/<int:year>/<int:month>/<int:day>/', views.DailyQuestionList.as_view(), name='daily_questions'),

Create a Today’s question list Feature

First, add a view using Django RedirectView as shown below

# open qanda/views.py and add the folliwing program

from django.views.generic import RedirectView

class TodaysQuestionList(RedirectView):
    def get_redirect_url(self, *args, **kwargs):
        today = timezone.now()
        return reverse(
            'qanda:daily_questions',
            kwargs={
                'day': today.day,
                'month': today.month,
                'year': today.year,
            }
        )

Let us now add a route to today’s question list a shown below

# open qanda/urls.py and add the following path 

path('', views.TodaysQuestionList.as_view(), name='index'),

If you run the development server at this point in time you should be able to see the added functionalities at http://127.0.0.1:8000/


I hope you find this tutorial useful. If you find any errors or feel any need for improvement, let me know in your comments below.

Signing off for today. Stay tuned and I will see you next week! Happy learning.

Exit mobile version