Skip to content

Instantly share code, notes, and snippets.

@philgyford
Created August 4, 2022 09:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save philgyford/5ddc7facef0d661c3cd1b2f79c4bf93f to your computer and use it in GitHub Desktop.
Save philgyford/5ddc7facef0d661c3cd1b2f79c4bf93f to your computer and use it in GitHub Desktop.
A quick Django blog app.
from ckeditor.widgets import CKEditorWidget
from django import forms
from django.contrib import admin
from django.db import models
from django.utils import timezone
from .models import Post
class PostAdminForm(forms.ModelForm):
"""
So we can add custom validation and autocomplete for tags, and tweak
formatting of other inputs.
"""
class Meta:
model = Post
fields = "__all__"
def clean(self):
"""
A Post that's Scheduled should have a time_published that's in the future.
"""
status = self.cleaned_data.get("status")
time_published = self.cleaned_data.get("time_published")
if status == Post.Status.SCHEDULED:
if time_published is None:
raise forms.ValidationError(
"If this post is Scheduled it should have a Time Published."
)
elif time_published <= timezone.now():
raise forms.ValidationError(
"This post is Scheduled but its Time Published is in the past."
)
return self.cleaned_data
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ("title", "status_icon", "time_published")
list_filter = ("time_published", "status")
search_fields = ("title", "intro", "body")
date_hierarchy = "time_published"
form = PostAdminForm
fieldsets = (
(None, {"fields": ("title", "slug", "status", "time_published")}),
("The post", {"fields": ("intro", "body", "author")}),
(
"Times",
{"classes": ("collapse",), "fields": ("time_created", "time_modified")},
),
)
formfield_overrides = {models.TextField: {"widget": CKEditorWidget}}
prepopulated_fields = {"slug": ("title",)}
readonly_fields = ("time_created", "time_modified")
@admin.display(description="Status")
def status_icon(self, obj):
if obj.status == Post.Status.LIVE:
return "✅"
elif obj.status == Post.Status.DRAFT:
return "…"
elif obj.status == Post.Status.SCHEDULED:
return "🕙"
else:
return ""
from django.apps import AppConfig
class BlogAppConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "myproject.blog"
from django.db import models
class PublicPostManager(models.Manager):
"""
Returns Posts that have been published.
"""
def get_queryset(self):
return super().get_queryset().filter(status=self.model.Status.LIVE)
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import models
from django.urls import reverse
from django.utils import timezone
from .managers import PublicPostManager
class Post(models.Model):
class Status(models.IntegerChoices):
DRAFT = 1, "Draft"
LIVE = 2, "Published"
SCHEDULED = 4, "Scheduled"
title = models.CharField(blank=False, max_length=255, help_text="No HTML")
intro = models.TextField(
blank=False, help_text="First paragraph or so of the post. HTML."
)
body = models.TextField(blank=True, help_text="The rest of the post text. HTML.")
time_published = models.DateTimeField(null=True, blank=False, default=timezone.now)
slug = models.SlugField(
max_length=255,
unique_for_date="time_published",
help_text="Must be unique within its date of publication",
)
status = models.PositiveSmallIntegerField(
blank=False, choices=Status.choices, default=Status.DRAFT
)
author = models.ForeignKey(
get_user_model(),
default=1,
on_delete=models.CASCADE,
null=True,
blank=False,
related_name="posts",
)
time_created = models.DateTimeField(
auto_now_add=True, help_text="The time this post was created in the database."
)
time_modified = models.DateTimeField(
auto_now=True, help_text="The time this postwas last saved to the database."
)
# All posts, no matter what their status.
objects = models.Manager()
# Posts that have been published.
public_objects = PublicPostManager()
class Meta:
ordering = ["-time_published", "-time_created"]
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse(
"blog:post_detail",
kwargs={"year": self.time_published.strftime("%Y"), "slug": self.slug},
)
def get_previous_post(self):
"Gets the previous public Post, by time_published."
return (
self.__class__.public_objects.filter(time_published__lt=self.time_published)
.order_by("-time_published")
.first()
)
def get_next_post(self):
"Gets the next public Post, by time_published."
return (
self.__class__.public_objects.filter(time_published__gt=self.time_published)
.order_by("time_published")
.first()
)
from django.contrib.sitemaps import Sitemap
from .models import Post
class PostSitemap(Sitemap):
"""Lists all Blog Posts for the sitemap."""
changefreq = "never"
priority = 0.8
def items(self):
return Post.public_objects.all()
def lastmod(self, obj):
return obj.time_modified
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path("", views.BlogHomeView.as_view(), name="home"),
path("<int:year>/<slug:slug>/", views.PostDetailView.as_view(), name="post_detail"),
]
from django.http import Http404
from django.utils.translation import gettext as _
from django.views.generic import DetailView, ListView
from .models import Post
class BlogHomeView(ListView):
"""
Lists the most recent live Posts.
"""
model = Post
allow_empty = False
queryset = Post.public_objects.all()
page_kwarg = "p"
paginate_by = 10
template_name = "blog/blog_home.html"
class PostDetailView(DetailView):
"""
Displays a single Post based on slug and year.
"""
model = Post
# True, because we want to be able to preview scheduled posts:
allow_future = True
date_field = "time_published"
year = None
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.object:
if self.object.status != Post.Status.LIVE:
context["is_preview"] = True
return context
def get_object(self, queryset=None):
if queryset is None:
queryset = self.get_queryset()
slug = self.kwargs.get(self.slug_url_kwarg)
year = self.get_year()
obj = queryset.filter(slug=slug, time_published__year=year).first()
if obj is None:
raise Http404(_(f"No Post found with a slug of {slug} and year of {year}."))
return obj
def get_queryset(self):
"""
Allow a Superuser to see draft and scheduled Posts.
Everyone else can only see live Posts.
"""
if self.request.user.is_superuser:
return self.model.objects.all()
else:
return self.model.public_objects.all()
def get_year(self):
"""Return the year for which this view should display data.
Copied from DateDetailView."""
year = self.year
if year is None:
try:
year = self.kwargs["year"]
except KeyError:
try:
year = self.request.GET["year"]
except KeyError:
raise Http404(_("No year specified"))
return year
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment