summaryrefslogtreecommitdiff
path: root/news
diff options
context:
space:
mode:
Diffstat (limited to 'news')
-rw-r--r--news/admin.py9
-rw-r--r--news/migrations/0003_new_date_columns_precision.py4
-rw-r--r--news/migrations/0005_add_slugs.py4
-rw-r--r--news/migrations/0011_auto__add_field_news_safe_mode.py68
-rw-r--r--news/migrations/0012_mark_old_news_safe_exempt.py73
-rw-r--r--news/models.py22
-rw-r--r--news/urls.py25
-rw-r--r--news/views.py129
8 files changed, 238 insertions, 96 deletions
diff --git a/news/admin.py b/news/admin.py
index 1b7de1d8..562c16d4 100644
--- a/news/admin.py
+++ b/news/admin.py
@@ -2,9 +2,14 @@ from django.contrib import admin
from .models import News
+
class NewsAdmin(admin.ModelAdmin):
- list_display = ('title', 'author', 'postdate', 'last_modified')
- list_filter = ('postdate', 'author')
+ list_display = ('title', 'author', 'postdate', 'last_modified', 'safe_mode')
+ list_filter = ('postdate', 'author', 'safe_mode')
search_fields = ('title', 'content')
+ date_hierarchy = 'postdate'
+
admin.site.register(News, NewsAdmin)
+
+# vim: set ts=4 sw=4 et:
diff --git a/news/migrations/0003_new_date_columns_precision.py b/news/migrations/0003_new_date_columns_precision.py
index 21b64443..1c97f488 100644
--- a/news/migrations/0003_new_date_columns_precision.py
+++ b/news/migrations/0003_new_date_columns_precision.py
@@ -1,14 +1,14 @@
# encoding: utf-8
-import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
+from django.utils.timezone import now
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'News.last_modified'
- db.add_column('news', 'last_modified', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, default=datetime.datetime.now(), db_index=True, blank=True), keep_default=False)
+ db.add_column('news', 'last_modified', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, default=now(), db_index=True, blank=True), keep_default=False)
# Changing field 'News.postdate'
db.alter_column('news', 'postdate', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True))
# Adding index on 'News', fields ['postdate']
diff --git a/news/migrations/0005_add_slugs.py b/news/migrations/0005_add_slugs.py
index 2a3b6174..96bd5213 100644
--- a/news/migrations/0005_add_slugs.py
+++ b/news/migrations/0005_add_slugs.py
@@ -11,7 +11,7 @@ class Migration(DataMigration):
def forwards(self, orm):
existing = list(orm.News.objects.values_list(
'slug', flat=True).distinct())
- for item in orm.News.objects.all():
+ for item in orm.News.objects.defer('content').filter(slug=None):
suffixed = slug = slugify(item.title)
suffix = 1
while suffixed in existing:
@@ -24,7 +24,7 @@ class Migration(DataMigration):
item.save()
def backwards(self, orm):
- orm.News.obects.all.update(slug=None)
+ orm.News.objects.all.update(slug=None)
models = {
'auth.group': {
diff --git a/news/migrations/0011_auto__add_field_news_safe_mode.py b/news/migrations/0011_auto__add_field_news_safe_mode.py
new file mode 100644
index 00000000..565c7adb
--- /dev/null
+++ b/news/migrations/0011_auto__add_field_news_safe_mode.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ db.add_column('news', 'safe_mode',
+ self.gf('django.db.models.fields.BooleanField')(default=True),
+ keep_default=True)
+
+ def backwards(self, orm):
+ db.delete_column('news', 'safe_mode')
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'news.news': {
+ 'Meta': {'ordering': "('-postdate',)", 'object_name': 'News', 'db_table': "'news'"},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'news_author'", 'on_delete': 'models.PROTECT', 'to': "orm['auth.User']"}),
+ 'content': ('django.db.models.fields.TextField', [], {}),
+ 'guid': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_modified': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+ 'postdate': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+ 'safe_mode': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ }
+ }
+
+ complete_apps = ['news']
diff --git a/news/migrations/0012_mark_old_news_safe_exempt.py b/news/migrations/0012_mark_old_news_safe_exempt.py
new file mode 100644
index 00000000..b2661cd8
--- /dev/null
+++ b/news/migrations/0012_mark_old_news_safe_exempt.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+import markdown
+
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ md = markdown.Markdown(safe_mode=True, enable_attributes=False)
+ magic = md.html_replacement_text
+ items = orm.News.objects.all()
+ has_html = [item.pk for item in items if magic in md.convert(item.content)]
+ for pk in has_html:
+ orm.News.objects.filter(pk=pk).update(safe_mode=False)
+
+ def backwards(self, orm):
+ orm.News.objects.all().update(safe_mode=True)
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'news.news': {
+ 'Meta': {'ordering': "('-postdate',)", 'object_name': 'News', 'db_table': "'news'"},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'news_author'", 'on_delete': 'models.PROTECT', 'to': "orm['auth.User']"}),
+ 'content': ('django.db.models.fields.TextField', [], {}),
+ 'guid': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_modified': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+ 'postdate': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}),
+ 'safe_mode': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+ }
+ }
+
+ complete_apps = ['news']
+ symmetrical = True
diff --git a/news/models.py b/news/models.py
index 95026e1d..d51db7c7 100644
--- a/news/models.py
+++ b/news/models.py
@@ -1,8 +1,10 @@
+import markdown
+
from django.db import models
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
-
-from main.utils import utc_now
+from django.utils.safestring import mark_safe
+from django.utils.timezone import now
class News(models.Model):
@@ -14,10 +16,15 @@ class News(models.Model):
title = models.CharField(max_length=255)
guid = models.CharField(max_length=255, editable=False)
content = models.TextField()
+ safe_mode = models.BooleanField(default=True)
def get_absolute_url(self):
return '/news/%s/' % self.slug
+ def html(self):
+ return mark_safe(markdown.markdown(
+ self.content, safe_mode=self.safe_mode, enable_attributes=False))
+
def __unicode__(self):
return self.title
@@ -25,17 +32,18 @@ class News(models.Model):
db_table = 'news'
verbose_name_plural = 'news'
get_latest_by = 'postdate'
- ordering = ['-postdate']
+ ordering = ('-postdate',)
+
def set_news_fields(sender, **kwargs):
news = kwargs['instance']
- now = utc_now()
- news.last_modified = now
+ current_time = now()
+ news.last_modified = current_time
if not news.postdate:
- news.postdate = now
+ news.postdate = current_time
# http://diveintomark.org/archives/2004/05/28/howto-atom-id
news.guid = 'tag:%s,%s:%s' % (Site.objects.get_current(),
- now.strftime('%Y-%m-%d'), news.get_absolute_url())
+ current_time.strftime('%Y-%m-%d'), news.get_absolute_url())
# connect signals needed to keep cache in line with reality
from main.utils import refresh_latest
diff --git a/news/urls.py b/news/urls.py
index 10020f31..0eec6d86 100644
--- a/news/urls.py
+++ b/news/urls.py
@@ -1,14 +1,25 @@
from django.conf.urls import patterns
+from django.contrib.auth.decorators import permission_required
+from .views import (NewsDetailView, NewsListView,
+ NewsCreateView, NewsEditView, NewsDeleteView)
+
urlpatterns = patterns('news.views',
- (r'^$', 'news_list', {}, 'news-list'),
- (r'^add/$', 'add'),
- (r'^preview/$', 'preview'),
+ (r'^$',
+ NewsListView.as_view(), {}, 'news-list'),
+
+ (r'^preview/$', 'preview'),
# old news URLs, permanent redirect view so we don't break all links
- (r'^(?P<object_id>\d+)/$', 'view_redirect'),
- (r'^(?P<slug>[-\w]+)/$', 'view'),
- (r'^(?P<slug>[-\w]+)/edit/$', 'edit'),
- (r'^(?P<slug>[-\w]+)/delete/$', 'delete'),
+ (r'^(?P<object_id>\d+)/$', 'view_redirect'),
+
+ (r'^add/$',
+ permission_required('news.add_news')(NewsCreateView.as_view())),
+ (r'^(?P<slug>[-\w]+)/$',
+ NewsDetailView.as_view()),
+ (r'^(?P<slug>[-\w]+)/edit/$',
+ permission_required('news.change_news')(NewsEditView.as_view())),
+ (r'^(?P<slug>[-\w]+)/delete/$',
+ permission_required('news.delete_news')(NewsDeleteView.as_view())),
)
# vim: set ts=4 sw=4 et:
diff --git a/news/views.py b/news/views.py
index 7ac009ba..62d30fde 100644
--- a/news/views.py
+++ b/news/views.py
@@ -1,91 +1,68 @@
+import markdown
+
from django import forms
-from django.contrib.auth.decorators import permission_required
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect
-from django.template.defaultfilters import slugify
-from django.views.decorators.cache import never_cache
-from django.views.generic import list_detail, create_update
-from django.views.generic.simple import direct_to_template
-
-import markdown
+from django.views.decorators.http import require_POST
+from django.views.generic import (DetailView, ListView,
+ CreateView, UpdateView, DeleteView)
from .models import News
+from main.utils import find_unique_slug
+
+
+class NewsForm(forms.ModelForm):
+ class Meta:
+ model = News
+ exclude = ('id', 'slug', 'author', 'postdate', 'safe_mode')
+
+
+class NewsDetailView(DetailView):
+ model = News
+ template_name = "news/view.html"
+
+
+class NewsListView(ListView):
+ queryset = News.objects.all().select_related('author').defer('content')
+ template_name = "news/list.html"
+ paginate_by = 50
+
+
+class NewsCreateView(CreateView):
+ model = News
+ form_class = NewsForm
+ template_name = "news/add.html"
+
+ def form_valid(self, form):
+ # special logic, we auto-fill the author and slug fields
+ newsitem = form.save(commit=False)
+ newsitem.author = self.request.user
+ newsitem.slug = find_unique_slug(News, newsitem.title)
+ newsitem.save()
+ return super(NewsCreateView, self).form_valid(form)
+
+
+class NewsEditView(UpdateView):
+ model = News
+ form_class = NewsForm
+ template_name = "news/add.html"
+
+
+class NewsDeleteView(DeleteView):
+ model = News
+ template_name = "news/delete.html"
+ success_url = "/news/"
+
def view_redirect(request, object_id):
newsitem = get_object_or_404(News, pk=object_id)
return redirect(newsitem, permanent=True)
-def view(request, slug=None):
- return list_detail.object_detail(request, News.objects.all(),
- slug=slug,
- template_name="news/view.html",
- template_object_name='news')
-
-#TODO: May as well use a date-based list here sometime
-def news_list(request):
- return list_detail.object_list(request,
- News.objects.all().select_related('author').defer('content'),
- paginate_by=50,
- template_name="news/list.html",
- template_object_name="news")
-class NewsForm(forms.ModelForm):
- class Meta:
- model = News
- exclude = ('id', 'slug', 'author', 'postdate')
-
-def find_unique_slug(newsitem):
- '''Attempt to find a unique slug for this news item.'''
- existing = list(News.objects.values_list('slug', flat=True).distinct())
-
- suffixed = slug = slugify(newsitem.title)
- suffix = 0
- while suffixed in existing:
- suffix += 1
- suffixed = "%s-%d" % (slug, suffix)
-
- return suffixed
-
-@permission_required('news.add_news')
-@never_cache
-def add(request):
- if request.POST:
- form = NewsForm(request.POST)
- if form.is_valid():
- newsitem = form.save(commit=False)
- newsitem.author = request.user
- newsitem.slug = find_unique_slug(newsitem)
- newsitem.save()
- return redirect(newsitem)
- else:
- form = NewsForm()
- return direct_to_template(request, 'news/add.html', { 'form': form })
-
-@permission_required('news.delete_news')
-@never_cache
-def delete(request, slug):
- return create_update.delete_object(request,
- News,
- slug=slug,
- post_delete_redirect='/news/',
- template_name='news/delete.html',
- template_object_name='news')
-
-@permission_required('news.change_news')
-@never_cache
-def edit(request, slug):
- return create_update.update_object(request,
- slug=slug,
- form_class=NewsForm,
- template_name="news/add.html")
-
-@permission_required('news.change_news')
-@never_cache
+@require_POST
def preview(request):
- markup = ''
- if request.POST:
- data = request.POST.get('data', '')
- markup = markdown.markdown(data)
+ data = request.POST.get('data', '')
+ markup = markdown.markdown(data, safe_mode=True, enable_attributes=False)
return HttpResponse(markup)
# vim: set ts=4 sw=4 et: