Arsalan Shahid

StackDuplica: A Django Based Mini-stackoverflow Application

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

Strength and growth come only through continuous effort and struggle – Napoleon Hill

This is the first part of StackDuplica web-Application tutorial series. This application has been adopted from the book Building Django 2.0 Web Applications. We will create a question-answer platform for users to be able to ask questions and get responses.


Few useful links to practically follow the project:

  1. StackDuplica GitHub repository — DirectMe
  2. All other tutorials on StackDuplica — DirectMe
  3. Clone the application from GitHub using
git clone https://github.com/ArsalanShahid116/StackDuplica

Setting-up Django Project

StackDuplica$ virtualenv stackenv 
StackDuplica$ source stackenv/bin/activate
(stackenv)StackDuplica$ pip3 install django
(stackenv)StackDuplica$ pip3 freeze > requirements.txt

# Create a Django project
(stackenv)StackDuplica$ django-admin startproject config 

# Change project name
(stackenv)StackDuplica$ mv config StackApp 
(stackenv)StackDuplica/StackApp$ django-admin startapp qanda

# list the qanda application in the list of installed apps in settings.py of project as shown below

INSTALLED_APPS = [
'qanda', # add this
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]

# Configure Django project to use PostgreSQL. 
# open StackApp/config/settings.py and update the following DATABASE variable as follows

DATABASES = {
        'default':
            {
                'ENGINE': 'django.db.backends.postgresql',
                'NAME': 'mymdb',
                'USER': 'mymdb',
                'PASSWORD': 'development',
                'HOST': '127.0.0.1',
                'PORT': '5432',
            }
}

(stackenv)StackDuplica/StackApp$ pip3 install psycopg2
(stackenv)StackDuplica/StackApp$ python3 manage.py makemigrations
(stackenv)StackDuplica/StackApp$ python3 manage.py migrate
(stackenv)StackDuplica/StackApp$ python3 manage.py runserver

Check if development server is working by entering the following URL in browser http://127.0.0.1:8000/. It should be giving you something as shown in Figure 1. If it does, you are doing great. cheers!

A detail on individual Django project files is given in one of my earlier posts at DirectMe

Image result for django welcome page

Fig 1: Django Welcome Page


Defining StackDuplica models and adding application wide templates and style sheets

We will start by first defining our model functions. Since it would be a question-answer application, we may need two models, i.e., one for questions and the other for answers. Each question can have one or more answers from users so we will use a question model as a ForeignKey. The models are as below:

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

from django.conf import settings
from django.db import models
from django.urls.base import reverse


class Question(models.Model):
    title = models.CharField(max_length=140)
    question = models.TextField()
    user = models.ForeignKey(to=settings.AUTH_USER_MODEL,
                             on_delete=models.CASCADE)
    created = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('cueeneh:question_detail', kwargs={'pk': self.id})

    def can_accept_answers(self, user):
        return user == self.user

class Answer(models.Model):
    answer = models.TextField()
    user = models.ForeignKey(to=settings.AUTH_USER_MODEL,
                             on_delete=models.CASCADE)
    created = models.DateTimeField(auto_now_add=True)
    question = models.ForeignKey(to=Question,
                                 on_delete=models.CASCADE)
    accepted = models.BooleanField(default=False)

    class Meta:
        ordering = ('-created', )

We will now create database migrations as shown below

(stackenv)StackDuplica/StackApp$ python3 manage.py makemigrations qanda (stackenv)StackDuplica/StackApp$ python3 manage.py migrate qanda

We will now add a path to our system level templates and static files in settings.py as shown below

# open config/settings.py and add the application-wide templates directory path as highlighted below 

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]

# open config/settings.py and add the application-wide static files directory path

STATISFILES_DIR = [os.path.join(BASE_DIR, 'static'),]

We will now add a system-wide base template

# open templates/base.html and add the following program

{% load static %}
<!DOCTYPE html>
<html lang="en" >
<head >
  <meta charset="UTF-8" >
  <title >{% block title %}StackDuplica{% endblock %}</title >
  <link
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb"
      crossorigin="anonymous" >
  <link
      href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
      rel="stylesheet"
      integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN"
      crossorigin="anonymous" >
  <link rel="stylesheet" href="{% static "base.css" %}" >
</head >
<body >
<nav class="navbar navbar-expand-lg  bg-light" >
  <div class="container" >
    <a class="navbar-brand" href="/" >StackDuplica</a >
    <ul class="navbar-nav" >
    </ul >
  </div >
</nav >
<div class="container" >
  {% block body %}{% endblock %}
</div >
</body >
</html >

Let us now add a simple system-wide style sheet for our application as shown below

# open static/base.css and add the following program

nav.navbar {
margin-bottom: 1em;
}

.question-preview {
margin-bottom: 1em;
}

.question .meta
{
border-bottom: lightgrey solid .01em;
margin-bottom: 2em;
padding-bottom: 1em;
}

.question .body
{
padding-bottom: 1em;
}

.answer {
border-bottom: lightgrey dotted .01em;
margin-bottom: 1em;
padding-bottom: .5em;
}

Adding Forms, Views, and templates for Asking Questions

First add the Question form as below

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

from django import forms
from django.contrib.auth import get_user_model

from .models import Question

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

    class Meta:
        model = Question
        fields = ['title', 'question', 'user', ]

Now add a view to allow user posting the questions using Django class-based gerenic CreateView

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

from django.shortcuts import render
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http.response import HttpResponseRedirect, HttpResponseBadRequest
from django.urls.base import reverse
from django.views.generic import (
    CreateView,
)

from .forms import QuestionForm
from .models import Question

class AskQuestionView(LoginRequiredMixin, CreateView):
    form_class = QuestionForm
    template_name = 'qanda/ask.html'

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

    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':
            preview = Question(
                question=form.cleaned_data['question'],
                title=form.cleaned_data['title'])
            ctx = self.get_context_data(preview=preview)
            return self.render_to_response(context=ctx)
        return HttpResponseBadRequest()

Let us now add a template for users to ask questions that extends the base template.

{% extends "base.html" %}

{% load markdownify %}
{% load crispy_forms_tags %}

{% block title %} Ask a question {% endblock %}

{% block body %}
  <div class="col-md-12" >
    <h1 >Ask a question</h1 >
    {% if preview %}
      <div class="card question-preview" >
        <div class="card-header" >
          Question Preview
        </div >
        <div class="card-body" >
          <h1 class="card-title" >{{ preview.title }}</h1>
          {{ preview.question |  markdownify }}
        </div >
      </div >
    {% endif %}

    <form method="post" >
      {{ 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" >
        Ask!
      </button >
    </form >
  </div >
{% endblock %}

If we take a look at the template then we can see that we have loaded two packages just below extending the base template. One is markdownify and the second is crispy_forms. The first one is to add some text editing features like bold, italic and etc. And the second one is to add a style to standard Django forms.

We will now install and configure both of these packages as shown below:

# Installing and configuring markdownify

(stackenv)StackDuplica$ pip3 install django-markdownify

# open qanda/settings.py and add the following

INSTALLED_APPS = [
    'qanda',
    'markdownify', # add this
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MARKDOWNIFY_STRIP = False
MARKDOWNIFY_WHITELIST_TAGS = [
    'a',
    'abbr',
    'acronym',
    'b',
    'blockquote',
    'em',
    'i',
    'li',
    'ol',
    'p',
    'strong',
    'ul'
]

# installing and configuring crispy forms

(stackenv)StackDuplica$ pip3 install django-crispy-forms

# open config/settings.py and add the following 

INSTALLED_APPS = [
    'qanda',
    'markdownify',
    'crispy_forms', # add this
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

CRISPY_TEMPLATE_PACK = 'bootstrap4'

Finally add the routes for AskQuestions as following

# create qanda/urls.py and add the following program 

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

app_name = 'qanda'

urlpatterns = [
    path('ask', views.AskQuestionView.as_view(), name='ask'),
]

# create config/urls.py and add the following program 

from django.contrib import admin
from django.urls import path, include

import qanda.urls

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include(qanda.urls, namespace='qanda')),
]

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/ask

In the next tutorial, we will add an answer feature to the StackDuplica.


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