Skip to content

Instantly share code, notes, and snippets.

@JackAtOmenApps
Last active September 1, 2022 15:25
Show Gist options
  • Save JackAtOmenApps/4758cdbbb587200feaa97194e9b789e8 to your computer and use it in GitHub Desktop.
Save JackAtOmenApps/4758cdbbb587200feaa97194e9b789e8 to your computer and use it in GitHub Desktop.
This is a super basic example of using django-summernote to create a knowledgebase app in django (Make sure to pip install django-summernote)
{% extends "base.html" %}
{% load static %}
{% block title %}{% block head_title %}Knowledgebase - Delete {{ article.name }}{% endblock head_title %}{% endblock title %}
{% block content %}
<div class="container-fluid">
<div class="row ml-1 mr-1">
<div class="col-md-12">
<div class="card card-default mb-5">
<div class="card-body">
{% if article.description %}
<header>
{{ article.description }}
</header>
<hr>
{% endif %}
<section>
<h4>Delete this article?</h4>
<form method="post">{% csrf_token %}
<p>Are you sure you want to delete "{{ object }}"?</p>
<input class="btn btn-danger" type="submit" value="Confirm">
</form>
</section>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% extends "base.html" %}
{% load static %}
{% block title %}{% block head_title %}Knowledgebase - {{ article.name }}{% endblock head_title %}{% endblock title %}
{% block css %}
<style>
/* Makes embedded videos responsive */
iframe {
border-top-width: 0px;
border-right-width: 0px;
border-bottom-width: 0px;
border-left-width: 0px;
width: 100%;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row ml-1 mr-1">
<div class="col-md-12">
<article class="mb-4">
{% if article.description %}
<header class="mb-4">
{% if article.description %}<p><i>{{ article.description }}</i></p>{% endif %}
</header>
{% endif %}
<div class="card card-default mb-5">
<div class="card-body">
{{ article.content | safe }}
</div>
<div class="card-footer">
<ul class="list-unstyled mb-0">
<li>
<strong>Updated on</strong> <time>{{ article.modified }}</time>
</li>
<li>
<strong>Category:</strong> <a href="{% url 'knowledgebase:category' article.category.slug %}">{{ article.category.name }}</a>
</li>
</ul>
<p></p>
{% if 'manager' in header_user_groups or 'administrator' in header_user_groups %}
<a href="{% url 'knowledgebase:article_edit' article.slug %}" class="btn btn-success">Edit Article</a>
<a href="{% url 'knowledgebase:article_delete' article.slug %}" class="btn btn-danger">Delete Article</a>
{% endif %}
</div>
</div>
</article>
</div>
</div>
</div>
{% endblock %}
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% load static %}
{% block title %}{% block head_title %}Add / Edit Article{% endblock head_title %}{% endblock title %}
{% block css %}
<link href="{% static 'plugins/summernote/summernote-bs4.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/summernote/django_summernote.css' %}" rel="stylesheet" />
{% endblock %}
{% block content %}
<div class="container">
<form class="form-horizontal" method="post">
{% csrf_token %}
{% crispy form no_media %}
<div class="control-group">
<div class="controls">
<button type="submit" class="btn btn-primary">Update</button>
</div>
</div>
</form>
</div>
{% endblock %}
{% block scripts %}
<script src="{% static 'plugins/summernote/jquery.ui.widget.js' %}"></script>
<script src="{% static 'plugins/summernote/jquery.iframe-transport.js' %}"></script>
<script src="{% static 'plugins/summernote/jquery.fileupload.js' %}"></script>
<script src="{% static 'plugins/summernote/summernote-bs4.min.js' %}"></script>
<script src="{% static 'plugins/summernote/ResizeSensor.js' %}"></script>
{% endblock scripts %}
# Just an excerpt of the summernote specific items in my base settings file
INSTALLED_APPS = [
...
'django_summernote',
...
]
SUMMERNOTE_THEME = 'bs4'
SUMMERNOTE_CONFIG = {
# Using SummernoteWidget - iframe mode, default
'iframe': True,
# You can put custom Summernote settings
'summernote': {
# As an example, using Summernote Air-mode
'airMode': False,
# Change editor size
'width': '100%',
'height': '480',
# Use proper language setting automatically (default)
'lang': None,
},
# Need authentication while uploading attachments.
'attachment_require_authentication': True,
# You can disable attachment feature.
'disable_attachment': False,
# Lazy initialize
# If you want to initialize summernote at the bottom of page, set this as True
# and call `initSummernote()` on your page.
'lazy': True,
}
{% extends "base.html" %}
{% load static %}
{% block title %}{% block head_title %}Knowledgebase - Delete {{ category.name }}{% endblock head_title %}{% endblock title %}
{% block content %}
<div class="container-fluid">
<div class="row ml-1 mr-1">
<div class="col-md-12">
<div class="card card-default mb-5">
<div class="card-body">
{% if category.description %}
<header>
{{ category.description }}
</header>
<hr>
{% endif %}
<section>
<h4>Delete this category?</h4>
<form method="post">{% csrf_token %}
<p>Are you sure you want to delete "{{ object }}"?</p>
<input class="btn btn-danger" type="submit" value="Confirm">
</form>
</section>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% extends "base.html" %}
{% load static %}
{% block title %}{% block head_title %}Knowledgebase - {{ category.name }}{% endblock head_title %}{% endblock title %}
{% block content %}
<div class="container-fluid">
<div class="row ml-1 mr-1">
<div class="col-md-12">
<div class="card card-default mb-5">
<div class="card-body">
{% if category.description %}
<header>
{{ category.description }}
</header>
<hr>
{% endif %}
<section>
<h4>Articles in this category:</h4>
{% for article in articles_list %}
<br>
<article class="mb-4">
<a href="{% url 'knowledgebase:article' article.slug %}" class="text-success">
<h4 class="h4">{{ article.name }}</h4>
</a>
<p>
{% if article.description %}
<i>{{ article.description | truncatechars:200 }}</i>
{% else %}
{% endif %}
</p>
</article>
{% if 'manager' in header_user_groups or 'administrator' in header_user_groups %}
<a href="{% url 'knowledgebase:category_edit' category.slug %}" class="btn btn-success">Edit Category</a>
<a href="{% url 'knowledgebase:category_delete' category.slug %}" class="btn btn-danger">Delete Category</a>
{% endif %}
{% endfor %}
</section>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% load static %}
{% block title %}{% block head_title %}Add / Edit Category{% endblock head_title %}{% endblock title %}
{% block css %}
<link href="{% static 'plugins/summernote/summernote-bs4.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/summernote/django_summernote.css' %}" rel="stylesheet" />
{% endblock %}
{% block content %}
<div class="container">
<form class="form-horizontal" method="post">
{% csrf_token %}
{% crispy form no_media %}
<div class="control-group">
<div class="controls">
<button type="submit" class="btn btn-primary">Update</button>
</div>
</div>
</form>
</div>
{% endblock %}
{% block scripts %}
<script src="{% static 'plugins/summernote/jquery.ui.widget.js' %}"></script>
<script src="{% static 'plugins/summernote/jquery.iframe-transport.js' %}"></script>
<script src="{% static 'plugins/summernote/jquery.fileupload.js' %}"></script>
<script src="{% static 'plugins/summernote/summernote-bs4.min.js' %}"></script>
<script src="{% static 'plugins/summernote/ResizeSensor.js' %}"></script>
{% endblock scripts %}
from django import forms
from django_summernote.widgets import SummernoteWidget
from .models import Category, Article
class BasicArticleForm(forms.ModelForm):
class Meta:
model = Article
exclude = ('author', 'is_read_only')
widgets = {
'content': SummernoteWidget(),
}
class BasicCategoryForm(forms.ModelForm):
class Meta:
model = Category
exclude = ('author', )
{% extends "base.html" %}
{% load static %}
{% block title %}{% block head_title %}Knowledgebase - Home{% endblock head_title %}{% endblock title %}
{% block dashtitle %}Knowledgebase <small>Home</small>{% endblock dashtitle %}
{% block dashright %}
<a class="btn btn-success video-btn text-white less-buffer-top" href="{% url "knowledgebase:home" %}">Knowledgebase Home</a>
{% endblock dashright %}
{% block css %}
<link href="{% static 'plugins/summernote/summernote-bs4.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/summernote/django_summernote.css' %}" rel="stylesheet" />
{% endblock %}
{% block content %}
<div class="container-fluid bg-light pt-4">
<div class="row ml-1 mr-1">
<div class="col-md-12">
<h4>Category List:</h4>
<br>
<div class="row">
{% for category in categories_list %}
<div class="col mb-4">
<a href="{% url 'knowledgebase:category' category.slug %}">
<h4 class="text-success">{{ category.name }} ({{ category.articles.count }})</h4>
</a>
<div class="description">{{ category.description }}</div>
</div>
{% if forloop.counter|divisibleby:2 %}
<div class="w-100"></div>
{% endif %}
{% endfor %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{% static 'plugins/summernote/jquery.ui.widget.js' %}"></script>
<script src="{% static 'plugins/summernote/jquery.iframe-transport.js' %}"></script>
<script src="{% static 'plugins/summernote/jquery.fileupload.js' %}"></script>
<script src="{% static 'plugins/summernote/summernote-bs4.min.js' %}"></script>
<script src="{% static 'plugins/summernote/ResizeSensor.js' %}"></script>
{% endblock scripts %}
from django.contrib.gis.db import models
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from django_extensions.db.models import TimeStampedModel, AutoSlugField
from myproject.users.models import User
STATUSES = (
('draft', _('Draft')),
('published', _('Published')),
('deleted', _('Deleted')),
)
class CategoryQuerySet(models.QuerySet):
def by_author(self, user):
return self.filter(author=user)
class CategoryManager(models.Manager):
def get_queryset(self):
return CategoryQuerySet(self.model, using=self._db)
class Category(TimeStampedModel):
name = models.CharField(_('Category Name'), max_length=255)
slug = AutoSlugField(_('Category Slug (a short title used for web addresses)'), unique=True, populate_from='name')
category = models.ForeignKey('self', on_delete=models.CASCADE, related_name='categories', blank=True, null=True,
verbose_name=_('parent category'))
description = models.TextField(
_('Description'),
max_length=200,
blank=True
)
author = models.ForeignKey(
User,
on_delete=models.CASCADE)
objects = CategoryManager()
class Meta:
ordering = ['name']
verbose_name = _('Category')
verbose_name_plural = _('Categories')
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('knowledgebase:category', args=[str(self.slug)])
class ArticleQuerySet(models.QuerySet):
def category(self, category):
if isinstance(category, models.Model):
qs = self.filter(category=category)
elif category:
qs = self.filter(category__slug=category)
else:
qs = self.filter()
return qs
def by_author(self, user):
return self.filter(author=user)
def published(self):
return self.filter(status='published')
def unpublished(self):
return self.filter(status='draft')
def deleted(self):
return self.filter(status='deleted')
class ArticleManager(models.Manager):
def get_queryset(self):
return ArticleQuerySet(self.model, using=self._db)
class Article(TimeStampedModel):
name = models.CharField(_('Title'), max_length=100)
slug = AutoSlugField(_('Article Slug (a short title used for web addresses)'), unique=True, populate_from='name')
content = models.CharField(_('Content for this Article'), max_length=5000)
description = models.TextField(
_('Description'),
max_length=200,
blank=True
)
author = models.ForeignKey(User, blank=True, null=True, on_delete=models.SET_NULL, db_index=True)
status = models.CharField(_('Current status of the Article'),
max_length=32, choices=STATUSES,
default='draft')
category = models.ForeignKey(
Category,
related_name='articles',
on_delete=models.CASCADE)
objects = ArticleManager()
class Meta:
ordering = ['-modified']
verbose_name = _('Article')
verbose_name_plural = _('Articles')
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('knowledgebase:article', args=[str(self.slug)])
from django.urls import path
from . import views
app_name = 'knowledgebase'
urlpatterns = [
path('', views.HomeView.as_view(), name='home'),
path('article/new/', views.ArticleNewView.as_view(), name='new_article'),
path('article/<slug:slug>/', views.ArticleDetailView.as_view(), name='article'),
path('article/<slug:slug>/edit/', views.ArticleUpdateView.as_view(), name='article_edit'),
path('article/<slug:slug>/delete/', views.ArticleDeleteView.as_view(), name='article_delete'),
path('category/new/', views.CategoryNewView.as_view(), name='new_category'),
path('category/<slug:slug>/', views.CategoryDetailView.as_view(), name='category'),
path('category/<slug:slug>/edit/', views.CategoryUpdateView.as_view(), name='category_edit'),
path('category/<slug:slug>/delete/', views.CategoryDeleteView.as_view(), name='category_delete'),
]
# In my project I have limited viewing of articles to the people in the groups selected
# via the groups ManyToManyField on the Article model and editing to the people in my
# administrator group. I have skipped that here to make things a bit simpler, but you
# should absolutely limit access.
from django import forms
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.cache.backends.base import DEFAULT_TIMEOUT
from django.views.generic import (
FormView,
DetailView,
TemplateView,
UpdateView,
DeleteView
)
from django.http import HttpResponseRedirect
from django_summernote.widgets import SummernoteWidget
from .models import Category, Article
from .forms import BasicArticleForm, BasicCategoryForm
class ArticleNewView(LoginRequiredMixin, FormView):
form_class = BasicArticleForm
template_name = 'knowledgebase/article_form.html'
success_url = '/knowledgebase/'
def form_valid(self, form):
obj = form.save(commit=False)
obj.author = self.request.user
obj.save()
return HttpResponseRedirect(self.get_success_url())
class ArticleDetailView(LoginRequiredMixin, DetailView):
model = Article
context_object_name = "article"
count_hit = True
class ArticleDeleteView(LoginRequiredMixin, DeleteView):
model = Article
context_object_name = 'article'
success_url = '/knowledgebase/'
class ArticleUpdateView(LoginRequiredMixin, UpdateView):
model = Article
form_class = BasicArticleForm
template_name = "knowledgebase/article_form.html"
class CategoryNewView(LoginRequiredMixin, FormView):
form_class = BasicCategoryForm
template_name = 'knowledgebase/category_form.html'
success_url = '/knowledgebase/'
def form_valid(self, form):
obj = form.save(commit=False)
obj.author = self.request.user
obj.save()
return HttpResponseRedirect(self.get_success_url())
class HomeView(LoginRequiredMixin, TemplateView):
# Home view is the listing of categories to navigate to articles
template_name = 'knowledgebase/home.html'
def get_context_data(self, **kwargs):
context = super(HomeView, self).get_context_data(**kwargs)
context['categories_list'] = Category.objects.all()
return context
class CategoryDetailView(LoginRequiredMixin, DetailView):
model = Category
context_object_name = 'category'
def get_context_data(self, **kwargs):
context = super(CategoryDetailView, self).get_context_data(**kwargs)
context['articles_list'] = Article.objects.filter(category=self.object).published()
return context
class CategoryDeleteView(LoginRequiredMixin, DeleteView):
model = Category
context_object_name = 'category'
success_url = '/knowledgebase/'
class CategoryUpdateView(UpdateView):
model = Category
fields = ['name', 'category', 'description']
template_name = "knowledgebase/category_form.html"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment