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:
- StackDuplica GitHub repository — DirectMe
- All other tutorials on StackDuplica — DirectMe
- 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.