├── .gitignore ├── .hgignore ├── LICENSE ├── README.rst ├── blog ├── __init__.py ├── admin.py ├── feeds.py ├── markup.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_markupfield.py │ ├── 0003_remove_markupfield.py │ ├── 0004_begin_updated_date.py │ ├── 0005_populate_updated_date.py │ ├── 0006_updated_date_not_null.py │ └── __init__.py ├── models.py ├── templatetags │ ├── __init__.py │ ├── markup.py │ └── next_previous.py ├── urls │ ├── __init__.py │ ├── categories.py │ ├── entries.py │ └── feeds.py └── views.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | *.egg-info 4 | docs/_build/ 5 | .coverage -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | __pycache__ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2018, James Bennett. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided 12 | with the distribution. 13 | * Neither the name of the author nor the names of other 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. -*-restructuredtext-*- 2 | 3 | This is a Django application I developed, and originally distributed 4 | standalone, which powers the blog on my personal site. 5 | 6 | Since it was pretty narrowly tailored to my use case and never really 7 | gained wider use, I no longer maintain this as a separate application; 8 | the latest code now lives directly in `the repository of my personal 9 | site `_. The code in this 10 | repository is no longer maintained. -------------------------------------------------------------------------------- /blog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubernostrum/blog/dc87cf94e31f6d387508df36ad7b04e80ff2ce52/blog/__init__.py -------------------------------------------------------------------------------- /blog/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Category, Entry 4 | 5 | 6 | @admin.register(Category) 7 | class CategoryAdmin(admin.ModelAdmin): 8 | fieldsets = ( 9 | ('Metadata', { 10 | 'fields': ('title', 'slug') 11 | }), 12 | (None, { 13 | 'fields': ('description',) 14 | }), 15 | ) 16 | 17 | list_display = ('title', 'slug', 'num_live_entries') 18 | list_display_links = ('title', 'slug') 19 | 20 | prepopulated_fields = {'slug': ('title',)} 21 | 22 | def num_live_entries(self, obj): 23 | return obj.live_entries.count() 24 | num_live_entries.short_description = 'Live entries' 25 | 26 | 27 | @admin.register(Entry) 28 | class EntryAdmin(admin.ModelAdmin): 29 | date_hierarchy = 'pub_date' 30 | 31 | fieldsets = ( 32 | ('Metadata', { 33 | 'fields': ('author', 'pub_date', 'title', 'slug', 'status') 34 | }), 35 | (None, { 36 | 'fields': ('excerpt', 'body') 37 | }), 38 | (None, { 39 | 'fields': ('categories',) 40 | }) 41 | ) 42 | 43 | filter_horizontal = ('categories',) 44 | list_display = ('title', 'pub_date', 'status') 45 | list_display_links = ('title',) 46 | list_filter = ('status',) 47 | prepopulated_fields = {'slug': ('title',)} 48 | search_fields = ('title',) 49 | 50 | def get_queryset(self, request): 51 | # Default manager only returns live entries; we want them all. 52 | return Entry.objects.all() 53 | -------------------------------------------------------------------------------- /blog/feeds.py: -------------------------------------------------------------------------------- 1 | from django.utils.feedgenerator import Atom1Feed 2 | 3 | from django.contrib.sites.models import Site 4 | from django.contrib.syndication.views import Feed 5 | 6 | from .models import Category 7 | from .models import Entry 8 | 9 | 10 | current_site = Site.objects.get_current() 11 | 12 | 13 | class EntriesFeed(Feed): 14 | author_name = "James Bennett" 15 | copyright = "https://{}/about/copyright/".format(current_site.domain) 16 | description = "Latest entriess" 17 | feed_type = Atom1Feed 18 | item_copyright = "https://{}/about/copyright/".format(current_site.domain) 19 | item_author_name = "James Bennett" 20 | item_author_link = "https://{}/".format(current_site.domain) 21 | feed_url = "https://{}/feeds/entries/".format(current_site.domain) 22 | link = "https://{}/".format(current_site.domain) 23 | title = "James Bennett (b-list.org)" 24 | 25 | description_template = 'feeds/entry_description.html' 26 | title_template = 'feeds/entry_title.html' 27 | 28 | def item_categories(self, item): 29 | return [c.title for c in item.categories.all()] 30 | 31 | def item_guid(self, item): 32 | return "tag:{},{}:{}".format( 33 | current_site.domain, 34 | item.pub_date.strftime('%Y-%m-%d'), 35 | item.get_absolute_url() 36 | ) 37 | 38 | def item_pubdate(self, item): 39 | return item.pub_date 40 | 41 | def item_updateddate(self, item): 42 | return item.updated_date 43 | 44 | def items(self): 45 | return Entry.live.all()[:15] 46 | 47 | def item_link(self, item): 48 | return "https://{}{}".format( 49 | current_site.domain, 50 | item.get_absolute_url() 51 | ) 52 | 53 | 54 | class CategoryFeed(EntriesFeed): 55 | def feed_url(self, obj): 56 | return "https://{}/feeds/categories/{}/".format( 57 | current_site.domain, obj.slug 58 | ) 59 | 60 | def description(self, obj): 61 | return "Latest entries in category '{}'".format( 62 | obj.title 63 | ) 64 | 65 | def get_object(self, request, slug): 66 | return Category.objects.get(slug=slug) 67 | 68 | def items(self, obj): 69 | return obj.live_entries[:15] 70 | 71 | def link(self, obj): 72 | return self.item_link(obj) 73 | 74 | def title(self, obj): 75 | return "Latest entries in category '{}'".format( 76 | obj.title 77 | ) 78 | -------------------------------------------------------------------------------- /blog/markup.py: -------------------------------------------------------------------------------- 1 | from markdown import markdown 2 | from typogrify.filters import typogrify 3 | 4 | 5 | def markup(text): 6 | """ 7 | Mark up plain text into fancy HTML. 8 | 9 | """ 10 | return typogrify( 11 | markdown(text, 12 | lazy_ol=False, 13 | output_format='html5', 14 | extensions=['abbr', 15 | 'codehilite', 16 | 'fenced_code', 17 | 'sane_lists', 18 | 'smart_strong'])) 19 | -------------------------------------------------------------------------------- /blog/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import datetime 6 | from django.conf import settings 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Category', 18 | fields=[ 19 | ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), 20 | ('title', models.CharField(max_length=250)), 21 | ('slug', models.SlugField(unique=True)), 22 | ('description', models.TextField()), 23 | ('description_html', models.TextField(editable=False, blank=True)), 24 | ], 25 | options={ 26 | 'verbose_name_plural': 'Categories', 27 | 'ordering': ('title',), 28 | }, 29 | ), 30 | migrations.CreateModel( 31 | name='Entry', 32 | fields=[ 33 | ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), 34 | ('pub_date', models.DateTimeField(verbose_name='Date posted', default=datetime.datetime.now)), 35 | ('slug', models.SlugField(unique_for_date='pub_date')), 36 | ('status', models.IntegerField(choices=[(1, 'Live'), (2, 'Draft'), (3, 'Hidden')], default=1)), 37 | ('title', models.CharField(max_length=250)), 38 | ('body', models.TextField()), 39 | ('body_html', models.TextField(editable=False, blank=True)), 40 | ('excerpt', models.TextField(null=True, blank=True)), 41 | ('excerpt_html', models.TextField(null=True, editable=False, blank=True)), 42 | ('author', models.ForeignKey(to=settings.AUTH_USER_MODEL)), 43 | ('categories', models.ManyToManyField(to='blog.Category')), 44 | ], 45 | options={ 46 | 'verbose_name_plural': 'Entries', 47 | 'ordering': ('-pub_date',), 48 | 'get_latest_by': 'pub_date', 49 | }, 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /blog/migrations/0002_markupfield.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | # This migration and the following one have been edited to be no-ops; 8 | # previously, they set up django-markupfield's MarkupField for several 9 | # fields on the blog models, then returned those fields to their 10 | # original TextField definitions once I decided not to use 11 | # MarkupField. Preserving these migrations as they originally were 12 | # would require maintaining a dependency on django-markupfield (to 13 | # make MarkupField importable for these migrations) despite it no 14 | # longer being used. 15 | class Migration(migrations.Migration): 16 | dependencies = [ 17 | ('blog', '0001_initial'), 18 | ] 19 | 20 | operations = [ 21 | migrations.RunPython( 22 | migrations.RunPython.noop, 23 | migrations.RunPython.noop 24 | ) 25 | ] 26 | -------------------------------------------------------------------------------- /blog/migrations/0003_remove_markupfield.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | # This migration and the preceding one have been edited to be no-ops; 8 | # previously, they set up django-markupfield's MarkupField for several 9 | # fields on the blog models, then returned those fields to their 10 | # original TextField definitions once I decided not to use 11 | # MarkupField. Preserving these migrations as they originally were 12 | # would require maintaining a dependency on django-markupfield (to 13 | # make MarkupField importable for these migrations) despite it no 14 | # longer being used. 15 | class Migration(migrations.Migration): 16 | 17 | dependencies = [ 18 | ('blog', '0002_markupfield'), 19 | ] 20 | 21 | operations = [ 22 | migrations.RunPython( 23 | migrations.RunPython.noop, 24 | migrations.RunPython.noop 25 | ) 26 | ] 27 | -------------------------------------------------------------------------------- /blog/migrations/0004_begin_updated_date.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-03-26 03:07 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.manager 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('blog', '0003_remove_markupfield'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterModelManagers( 17 | name='entry', 18 | managers=[ 19 | ('live', django.db.models.manager.Manager()), 20 | ], 21 | ), 22 | migrations.AddField( 23 | model_name='entry', 24 | name='updated_date', 25 | field=models.DateTimeField(blank=True, null=True), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /blog/migrations/0005_populate_updated_date.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-03-26 03:14 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | from django.db import models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('blog', '0004_begin_updated_date'), 13 | ] 14 | 15 | operations = [ 16 | migrations.RunSQL( 17 | ["UPDATE blog_entry SET updated_date = pub_date;"], 18 | migrations.RunSQL.noop 19 | ) 20 | ] 21 | -------------------------------------------------------------------------------- /blog/migrations/0006_updated_date_not_null.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.10 on 2018-03-26 03:24 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('blog', '0005_populate_updated_date'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='entry', 17 | name='updated_date', 18 | field=models.DateTimeField(blank=True, editable=False), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /blog/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubernostrum/blog/dc87cf94e31f6d387508df36ad7b04e80ff2ce52/blog/migrations/__init__.py -------------------------------------------------------------------------------- /blog/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.conf import settings 4 | from django.db import models 5 | from django.urls import reverse 6 | from django.utils.encoding import python_2_unicode_compatible 7 | 8 | from .markup import markup 9 | 10 | 11 | class LiveEntryManager(models.Manager): 12 | """ 13 | Manager which will only fetch live entries. 14 | 15 | """ 16 | def get_queryset(self): 17 | return super( 18 | LiveEntryManager, self).get_queryset().filter( 19 | status=self.model.LIVE_STATUS 20 | ) 21 | 22 | 23 | @python_2_unicode_compatible 24 | class Entry(models.Model): 25 | """ 26 | An entry in the blog. 27 | 28 | """ 29 | LIVE_STATUS = 1 30 | DRAFT_STATUS = 2 31 | HIDDEN_STATUS = 3 32 | STATUS_CHOICES = ( 33 | (LIVE_STATUS, 'Live'), 34 | (DRAFT_STATUS, 'Draft'), 35 | (HIDDEN_STATUS, 'Hidden'), 36 | ) 37 | 38 | author = models.ForeignKey( 39 | settings.AUTH_USER_MODEL, 40 | on_delete=models.CASCADE 41 | ) 42 | pub_date = models.DateTimeField('Date posted', 43 | default=datetime.datetime.now) 44 | updated_date = models.DateTimeField( 45 | blank=True, 46 | editable=False, 47 | ) 48 | slug = models.SlugField(unique_for_date='pub_date') 49 | status = models.IntegerField(choices=STATUS_CHOICES, 50 | default=LIVE_STATUS) 51 | title = models.CharField(max_length=250) 52 | 53 | body = models.TextField() 54 | body_html = models.TextField(editable=False, blank=True) 55 | 56 | excerpt = models.TextField(blank=True, null=True) 57 | excerpt_html = models.TextField(editable=False, blank=True, null=True) 58 | 59 | categories = models.ManyToManyField('Category') 60 | 61 | live = LiveEntryManager() 62 | objects = models.Manager() 63 | 64 | class Meta: 65 | get_latest_by = 'pub_date' 66 | ordering = ('-pub_date',) 67 | verbose_name_plural = 'Entries' 68 | 69 | def __str__(self): 70 | return self.title 71 | 72 | def save(self, *args, **kwargs): 73 | self.body_html = markup(self.body) 74 | if self.excerpt: 75 | self.excerpt_html = markup(self.excerpt) 76 | self.updated_date = datetime.datetime.now() 77 | super(Entry, self).save(*args, **kwargs) 78 | 79 | def get_absolute_url(self): 80 | return reverse( 81 | 'blog_entry_detail', 82 | (), 83 | {'year': self.pub_date.strftime('%Y'), 84 | 'month': self.pub_date.strftime('%b').lower(), 85 | 'day': self.pub_date.strftime('%d'), 86 | 'slug': self.slug} 87 | ) 88 | 89 | 90 | @python_2_unicode_compatible 91 | class Category(models.Model): 92 | """ 93 | A category into which entries can be filed. 94 | 95 | """ 96 | title = models.CharField(max_length=250) 97 | slug = models.SlugField(unique=True) 98 | description = models.TextField() 99 | description_html = models.TextField(editable=False, blank=True) 100 | 101 | class Meta: 102 | verbose_name_plural = 'Categories' 103 | ordering = ('title',) 104 | 105 | def __str__(self): 106 | return self.title 107 | 108 | def save(self, *args, **kwargs): 109 | self.description_html = markup(self.description) 110 | super(Category, self).save(*args, **kwargs) 111 | 112 | def get_absolute_url(self): 113 | return reverse( 114 | 'blog_category_detail', 115 | (), 116 | {'slug': self.slug} 117 | ) 118 | 119 | def _get_live_entries(self): 120 | return self.entry_set.filter(status=Entry.LIVE_STATUS) 121 | live_entries = property(_get_live_entries) 122 | -------------------------------------------------------------------------------- /blog/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubernostrum/blog/dc87cf94e31f6d387508df36ad7b04e80ff2ce52/blog/templatetags/__init__.py -------------------------------------------------------------------------------- /blog/templatetags/markup.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.template.defaultfilters import stringfilter 3 | from django.utils.safestring import mark_safe 4 | 5 | from ..markup import markup as markup_func 6 | 7 | 8 | register = template.Library() 9 | 10 | 11 | @register.filter 12 | @stringfilter 13 | def markup(value): 14 | return mark_safe(markup_func(value)) 15 | -------------------------------------------------------------------------------- /blog/templatetags/next_previous.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.core.exceptions import ObjectDoesNotExist 3 | 4 | 5 | class NextPreviousNode(template.Node): 6 | def __init__(self, direction, queryset, date_field, varname): 7 | (self.direction, 8 | self.date_field, 9 | self.varname) = (direction, 10 | date_field, 11 | varname) 12 | self.queryset = template.Variable(queryset) 13 | 14 | def render(self, context): 15 | queryset = list(self.queryset.resolve(context)) 16 | result = None 17 | 18 | try: 19 | obj = queryset[{'next': 0, 20 | 'previous': -1}[self.direction]] 21 | except IndexError: 22 | return '' 23 | 24 | method = getattr(obj, 'get_%s_by_pub_date' % self.direction) 25 | 26 | try: 27 | result = method() 28 | except ObjectDoesNotExist: 29 | pass 30 | 31 | context[self.varname] = result 32 | return '' 33 | 34 | 35 | def next_previous(parser, token): 36 | """ 37 | Helps navigation of date-based archives, by finding next/previous 38 | objects. 39 | 40 | This is slightly different from what Django's date-based views do; 41 | they simply calculate date objects immediately beyond the range of 42 | the view. This tag finds the first actually-existing Entry, in 43 | either direction, so that links can go to a date range which 44 | actually has entries in it. 45 | 46 | This is done in a template tag rather than simply calling the 47 | model methods in templates because it needs to catch 48 | ``DoesNotExist`` and simply return ``None``. 49 | 50 | Can be called as either ``get_next`` (to get the first Entry after 51 | the date range) or ``get_previous`` (to get the first Entry before 52 | it). 53 | 54 | Syntax:: 55 | 56 | {% get_(next/previous) queryset as varname %} 57 | 58 | """ 59 | bits = token.contents.split() 60 | if len(bits) != 5: 61 | raise template.TemplateSyntaxError( 62 | "'%s' takes four arguments" % bits[0] 63 | ) 64 | if bits[3] != 'as': 65 | raise template.TemplateSyntaxError( 66 | "Third argument to '%s' must be 'as'" % bits[0] 67 | ) 68 | return NextPreviousNode(bits[0].split('_')[1], bits[1], bits[2], bits[4]) 69 | 70 | 71 | register = template.Library() 72 | register.tag('get_next', next_previous) 73 | register.tag('get_previous', next_previous) 74 | -------------------------------------------------------------------------------- /blog/urls/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ubernostrum/blog/dc87cf94e31f6d387508df36ad7b04e80ff2ce52/blog/urls/__init__.py -------------------------------------------------------------------------------- /blog/urls/categories.py: -------------------------------------------------------------------------------- 1 | """ 2 | URLs for categories in the blog. 3 | 4 | """ 5 | 6 | from django.conf.urls import url 7 | 8 | from blog import views 9 | 10 | urlpatterns = [ 11 | url(r'^$', 12 | views.CategoryList.as_view(), 13 | name='blog_category_list'), 14 | url(r'^(?P[-\w]+)/$', 15 | views.CategoryDetail.as_view(), 16 | name='blog_category_detail'), 17 | ] 18 | -------------------------------------------------------------------------------- /blog/urls/entries.py: -------------------------------------------------------------------------------- 1 | """ 2 | URLs for entries in the blog. 3 | 4 | """ 5 | 6 | from django.conf.urls import url 7 | 8 | from blog import views 9 | 10 | 11 | urlpatterns = [ 12 | url(r'^$', 13 | views.EntryArchiveIndex.as_view(), 14 | name='blog_entry_archive_index'), 15 | url(r'^(?P\d{4})/$', 16 | views.EntryArchiveYear.as_view(), 17 | name='blog_entry_archive_year'), 18 | url(r'^(?P\d{4})/(?P\w{3})/$', 19 | views.EntryArchiveMonth.as_view(), 20 | name='blog_entry_archive_month'), 21 | url(r'^(?P\d{4})/(?P\w{3})/(?P\d{2})/$', 22 | views.EntryArchiveDay.as_view(), 23 | name='blog_entry_archive_day'), 24 | url(r'^(?P\d{4})/(?P\w{3})/(?P\d{2})/(?P[-\w]+)/$', 25 | views.EntryDetail.as_view(), 26 | name='blog_entry_detail'), 27 | ] 28 | -------------------------------------------------------------------------------- /blog/urls/feeds.py: -------------------------------------------------------------------------------- 1 | """ 2 | URLs for the blog's feeds. 3 | 4 | """ 5 | 6 | from django.conf.urls import url 7 | 8 | from blog.feeds import CategoryFeed 9 | from blog.feeds import EntriesFeed 10 | 11 | 12 | urlpatterns = [ 13 | url(r'^entries/$', 14 | EntriesFeed(), 15 | name='blog_feeds_entries'), 16 | url(r'^categories/(?P[-\w]+)/$', 17 | CategoryFeed(), 18 | name='blog_feeds_category'), 19 | ] 20 | -------------------------------------------------------------------------------- /blog/views.py: -------------------------------------------------------------------------------- 1 | from django.views import generic 2 | 3 | from .models import Category, Entry 4 | 5 | 6 | class BaseEntryView(object): 7 | date_field = 'pub_date' 8 | model = Entry 9 | 10 | 11 | class BaseCategoryView(object): 12 | model = Category 13 | 14 | 15 | class EntryArchiveIndex(BaseEntryView, generic.ArchiveIndexView): 16 | pass 17 | 18 | 19 | class EntryArchiveYear(BaseEntryView, generic.YearArchiveView): 20 | make_object_list = True 21 | 22 | 23 | class EntryArchiveMonth(BaseEntryView, generic.MonthArchiveView): 24 | pass 25 | 26 | 27 | class EntryArchiveDay(BaseEntryView, generic.DayArchiveView): 28 | pass 29 | 30 | 31 | class EntryDetail(BaseEntryView, generic.DateDetailView): 32 | def get_queryset(self): 33 | # Allow logged-in users to view draft entries. 34 | if self.request.user.is_authenticated: 35 | return Entry.objects.all() 36 | return Entry.live.all() 37 | 38 | 39 | class CategoryList(BaseCategoryView, generic.ListView): 40 | pass 41 | 42 | 43 | class CategoryDetail(BaseCategoryView, generic.DetailView): 44 | pass 45 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | 4 | setup(name='blog', 5 | version='1.3.1', 6 | zip_safe=False, # eggs are the devil. 7 | description='A blog application for Django.', 8 | author='James Bennett', 9 | author_email='james@b-list.org', 10 | url='https://github.com/ubernostrum/blog/', 11 | packages=['blog', 'blog.urls', 'blog.templatetags'], 12 | classifiers=['Development Status :: 5 - Production/Stable', 13 | 'Environment :: Web Environment', 14 | 'Framework :: Django', 15 | 'Framework :: Django :: 1.11', 16 | 'Framework :: Django :: 2.0', 17 | 'Framework :: Django :: 2.1', 18 | 'Intended Audience :: Developers', 19 | 'License :: OSI Approved :: BSD License', 20 | 'Operating System :: OS Independent', 21 | 'Programming Language :: Python', 22 | 'Programming Language :: Python :: 2', 23 | 'Programming Language :: Python :: 2.7', 24 | 'Programming Language :: Python :: 3', 25 | 'Programming Language :: Python :: 3.4', 26 | 'Programming Language :: Python :: 3.5', 27 | 'Programming Language :: Python :: 3.6', 28 | 'Programming Language :: Python :: 3.7', 29 | 'Topic :: Utilities'], 30 | ) 31 | --------------------------------------------------------------------------------