Created
August 4, 2022 09:47
-
-
Save philgyford/5ddc7facef0d661c3cd1b2f79c4bf93f to your computer and use it in GitHub Desktop.
A quick Django blog app.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 "" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from django.apps import AppConfig | |
class BlogAppConfig(AppConfig): | |
default_auto_field = "django.db.models.BigAutoField" | |
name = "myproject.blog" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"), | |
] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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