Create a Notebook with Django

Christos Stathakis
8 min readApr 2, 2020
How our app will be show

We will create a app which we can use it to write notes easy and effective.

Framework and Libraries we will use

The requirements is python 3.4+ (recommended 3.7 or 3.8) and django 2+ (recommended django 3 or latest version of 2). You can grab them easy from the above links. I recommend to download the files from github, so you can create some files and folders needed later, easier.

First Part. Set up the Project.

So let’s start. First let’s create our project. Open the console on the desire folder and type the follow

$ django-admin startproject my_notebook
$ cd my_notebook
$ python manage.py startapp notebook

One last move before we start creating our models is to add the required libraries to our system.

$ pip install django
$ pip install django-tinymce

Now you installed the requirements, you can grab the app from github, and try it!, go to the end of the article to see the commands you need if needed.

Now, go grab the templates and static folders from here, and add them inside the notebook folder. This folders contains all files we need for our templates and the static files. It’s all the html, css, js files we will use for the front end.

Then let’s go to my_notebook folder and edit the settings.py. I will only add the changes.

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',

'notebook',

'django_tables2',
'tinymce',

]

And on the bottom add this.

TINYMCE_DEFAULT_CONFIG = {
'plugins': "spellchecker,paste,searchreplace",
'theme': "advanced",
'cleanup_on_startup': True,
'custom_undo_redo_levels': 10,
'width': '100%',
'height': '400px',

}

TINYMCE_SPELLCHECKER = True
TINYMCE_COMPRESSOR = True

Now the urls.py. Don’t worry we will create the files we mention later.

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

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

Second Part. Create Our Models.

Next move, on the new app which we created before(notebook), let’s open the models.py and add the follow.

from django.db import models
from django.urls import reverse
from tinymce.models import HTMLField
COLORS = (
('a', 'yellow'),
('b', 'white'),
('c', 'green'),
('d', 'red'),
('e', 'blue')
)

A quick view on imports, with models we can create our fields and tables for the database, with the reverse we will create dynamic url links for the frontend and the HTMLField will give us a fancy text editor. The colors will be used for a nice presentation.

Now our models here. Let’s continue…

class Tags(models.Model):
title = models.CharField(unique=True, max_length=240)

def __str__(self):
return self.title

def get_edit_url(self):
return reverse('notes:tag_update', kwargs={'pk': self.id})

def get_delete_url(self):
return reverse('notes:tag_delete', kwargs={'pk': self.id})

@staticmethod
def filters_data(request, qs):
q = request.GET.get('q', None)
search_name = request.GET.get('search_name', None)

qs = qs.filter(title__icontains=q) if q else qs
qs = qs.filter(title__icontains=search_name) if search_name else qs
return qs

The Tag model. We want our note to have some tags for easy filtering and categorize them. Because we don’t have many demand from that model we only add one field for title. We will explain the functions later.

Lets proceed to our Note model.

class Note(models.Model):
pinned = models.BooleanField(default=False, verbose_name='Καρφιτσωμενο')
title = models.CharField(max_length=400, verbose_name='Ονομασια')
description = HTMLField(blank=True, verbose_name='Περιγραφη')
timestamp = models.DateTimeField(auto_now_add=True)
date = models.DateField(blank=True, null=True)
tag = models.ManyToManyField(Tags, blank=True)
color = models.CharField(max_length=1, choices=COLORS, default='a', verbose_name='Χρωμα')

class Meta:
ordering = ['-pinned', '-timestamp']

def __str__(self):
return self.title

def get_edit_url(self):
return reverse('notes:note_update', kwargs={'pk': self.id})

def get_show_url(self):
return reverse('notes:show_note', kwargs={'pk': self.id})

@staticmethod
def filters_data(request, qs):
q = request.GET.get('q', None)
tags = request.GET.getlist('tag', None)
if tags:
tags_ = Tags.objects.filter(id__in=tags)
qs = qs.filter(tag__in=tags_)
qs = qs.filter(title__icontains=q) if q else qs
return qs

With pinned we will make sticky that note. Title, description, timestamp, date not much to analyse here. With tag we will connect our note to the tags we want, and the color field we will use it to give a personal touch to the frontend view (as said before.)

Next let’s create a new file called forms.py inside the notebook folder and add the next code.

from django import forms

from .models import Tags, Note


class BaseForm(forms.Form):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field_name, field in self.fields.items():
field.widget.attrs['class'] = 'form-control'


class NoteForm(BaseForm, forms.ModelForm):

class Meta:
model = Note
fields = ['pinned', 'color', 'title', 'tag', 'description']


class TagForm(BaseForm, forms.ModelForm):

class Meta:
model = Tags
fields = '__all__'

BaseForm is our base form we use it only for the def __init__. Because we will use the bootstrap framework on front end part, we want our form fields to have a compatibility class with bootstrap to be more beautiful.

Then we have the NoteForm and TagForm, both inheritance from BaseForm taking the extra styling, and on model field we add the desire model, and on fields we choose the desire fields.

Then lets create our table.py on same folder.

import django_tables2 as tables
from .models import Tags


class TagsTable(tables.Table):
action = tables.TemplateColumn('''
<a class="btn btn-primary" href="{{ record.get_edit_url }}">Επεξεργασια</a>
''')

class Meta:
model = Tags
template_name = 'django_tables2/bootstrap.html'
fields = ['title', 'action']

Not much here same syntax like forms.py, we will use here the library django_tables to boilerplate our frontend table. Thats all!

Last one the admin.py on same folder

from django.contrib import admin

from .models import Tags


@admin.register(Tags)
class TagAdmin(admin.ModelAdmin):
pass

Django have a admin system. We will only use it for the Tags model to add data on our database, because we will not create any specific tag view on this project. To work we only need to register the Tags to admin.

Third Part. Create The front end.

Time to add data to our views.py on same folder. First the imports we will explain everything when we meet them on our views.

from django.shortcuts import reverse, redirect, get_object_or_404, HttpResponseRedirect
from django.urls import reverse_lazy
from django.views.generic import ListView, UpdateView, DetailView, CreateView
from django.utils.decorators import method_decorator
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib import messages

Lets continue with the our code imports. Not much here too.

from django_tables2 import RequestConfig

from .models import Tags, Note
from .forms import NoteForm, TagForm
from .tables import TagsTable

Next.

@method_decorator(staff_member_required, name='dispatch')
class NoteHomepageView(ListView):
template_name = 'notes/homepage.html'
model = Note

def get_queryset(self):
qs = Note.objects.all()
qs = Note.filters_data(self.request, qs)
return qs

def get_context_data(self,**kwargs):
context = super().get_context_data(**kwargs)
context['create_form'] = NoteForm()
context['pinned_qs'] = self.object_list.filter(pinned=True)
context['qs'] = self.object_list.filter(pinned=False)[:30]
return context

Now the game begins! The method_decorator and staff_member_required, its a fast way to give access on to staff people, then we add model and the template we want to connect. With get_context_data function we pass the data we want to the template. We will use two different querysets, one for the pinned objects and the other for the rest, and the form to create a new form.

The get_queryset is for filtering our queryset. When the user hit the search or choose a tag, this function get in action. So we pass the request with the queryset and we filter the data, and we return the result…

This function we saw it before on models.py.

def filters_data(request, qs):
q = request.GET.get('q', None)
tags = request.GET.getlist('tag', None)
if tags:
tags_ = Tags.objects.filter(id__in=tags)
qs = qs.filter(tag__in=tags_)
qs = qs.filter(title__icontains=q) if q else qs
return qs

We check if the user used the search with the q parameter or he used a tag with the tag parameter and after that we use the django ORM to filter the queryset. If user used the search we take the data he submit and we look if any from our objects title contains that data. If the user used the tag, we take the id of that tag and we filter the data.

Let’s see the template now. Because templates is huge, i will only explain the parts i think is needed.

{% extends 'dashboard.html' %}  {% load render_table from django_tables2 %}

The first line. Dashboard.html contains all the css, js etc, so because we don’t want to add the on every template, we just extends that template, and we add the new code on the blocks we created. After that we load the render_table.

When we want to add data to our database we hit the new note button and a modal appears

<form method='post' class='form' action='{% url "notes:validate_note_creation" %}'>
{% csrf_token %}
{{ create_form }}
<br />
<button type='submit' class='btn btn-primary'><i class='fa fa-save'></i> Save</button>
</form>

When the user create the data and saves them we redirect the the data on a new view to check if anything is right. We use the template tag url, and the names of the view. When we create the urls.py the names will make sense.

That view is

@staff_member_required
def validate_new_note_view(request):
form = NoteForm(request.POST or None)
if form.is_valid():
form.save()
messages.success(request, 'New message added')
return redirect(reverse('notes:home'))

So we add the data for request.POST on NoteForm, and we check if its valid. If it is with form.save(), we add the data to the database and we message we inform the user. An then we redirect back to homepage.

Now the update_view.

When use hit the little blue edit button on corner, we go to this page.

@method_decorator(staff_member_required, name='dispatch')
class NoteUpdateView(UpdateView):
form_class = NoteForm
success_url = reverse_lazy('notes:home')
template_name = 'notes/form.html'
model = Note

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['back_url'] = self.success_url
context['form_title'] = f'Επεξεργασια {self.object.title}'
return context

def form_valid(self, form):
form.save()
messages.success(self.request, f'Η σημειωση ανανεώθηκε!')
return super().form_valid(form)

New thing here form_valid, works like before, but here we use a class view so a little different syntax.

If the use press the pin we go to this view.

@staff_member_required
def pinned_view(request, pk):
instance = get_object_or_404(Note, id=pk)
instance.pinned = False if instance.pinned else True
instance.save()
return HttpResponseRedirect(request.META.get('HTTP_REFERER'), reverse('notes:home'))

So here we get the instance, and then depends if the note is pin or not we change the status and save it, then we redirect to homepage.

And final the delete view if someone press the delete button.

@staff_member_required
def delete_note_view(request, pk):
instance = get_object_or_404(Note, id=pk)
instance.delete()
messages.warning(request, 'Διαγραφηκε')
return redirect(reverse('notes:home'))

Now lets add the urls.py.

from django.urls import path
from .views import (NoteHomepageView, validate_new_note_view, NoteUpdateView, pinned_view, delete_note_view,

)

app_name = 'notes'

urlpatterns = [
path('', NoteHomepageView.as_view(), name='home'),
path('pinned/<int:pk>/', pinned_view, name='pinned'),
path('validate-note-creation/', validate_new_note_view, name='validate_note_creation'),
path('note/update/<int:pk>/', NoteUpdateView.as_view(), name='note_update'),
path('note/delete/<int:pk>/', delete_note_view, name="delete_note"),

]

We import all the views we created, and with path we create the desire urls and make the connection. With name we use a friendly title to every view for easy access from the url template tag we saw before.

Before on models we saw this.

def get_edit_url(self):
return reverse('notes:tag_update', kwargs={'pk': self.id})

def get_delete_url(self):
return reverse('notes:tag_delete', kwargs={'pk': self.id})

So we use the reverse and the names we created on urls.py, to make the connection here. For example when the user press the edit button, the fuction get_edit_url get in action and with reverse sends back the url we need to access.

That’s it. Thx for reading!

$ python manage.py makemigrations
$ python manage.py migrate
$ python manage.py createsuperuser
$ python manage.py runserver

Enjoy!

--

--