Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
  • 6 commits
  • 11 files changed
  • 0 comments
  • 1 contributor
Apr 11, 2012
Noah Silas Set CSRF cookies in quote list views
Since these views perform AJAX POSTs, they need to have the CSRF cookie
set. Put back the CSRF decorator that was lost in 038d621.
54a755b
Noah Silas Coalesce vote null vote scores to zero
Postgres will return NULL scores for quotes that have no votes. These
get sorted up to the top of the list when ordering by score.

To fix this I had to create a new aggregate subclassing SUM. This isn't
tested outside of Postgres and SQLite, and I haven't researched support
for COALESCE on other platforms. Use at your own risk.
c316585
Noah Silas Add in a basic about page template 6c17285
Noah Silas Serve static files in development 9faee38
Apr 12, 2012
Noah Silas Add quote permalinks 43ccdab
Noah Silas Enable the django admin
The spammers have found us already. I need to build better moderation
tools, but in the meantime enabling the django admin will let me bulk
delete quotes.
2b35a18
26 qdb/aggregates.py
... ... @@ -0,0 +1,26 @@
  1 +from django.db.models.aggregates import Sum
  2 +from django.db.models.sql.aggregates import Sum as SumAggregate
  3 +
  4 +class DefaultAggregate(SumAggregate):
  5 + """ When summing no rows, databases may return null instead of zero.
  6 + Use this to supply a default value (usually zero) to be used in
  7 + its place. """
  8 + sql_template = 'COALESCE(%(function)s(%(field)s), %(default)s)'
  9 +
  10 +class DefaultSum(Sum):
  11 + """ When summing no rows, databases may return null instead of zero.
  12 + Use this to supply a default value (usually zero) to be used in
  13 + its place. """
  14 + name = "DefaultSum"
  15 +
  16 + def add_to_query(self, query, alias, col, source, is_summary):
  17 + params = {'default': 0}
  18 + params.update(self.extra)
  19 + query.aggregates[alias] = DefaultAggregate(
  20 + col,
  21 + source=source,
  22 + is_summary=is_summary,
  23 + **params
  24 + )
  25 +
  26 +
10 qdb/models.py
... ... @@ -1,12 +1,13 @@
1 1 from django.db import models
2 2 from django.core.exceptions import ValidationError
3 3 from hideable.models import Hideable, HideableManager
  4 +from qdb.aggregates import DefaultSum
4 5
5 6 class QuoteManager(HideableManager):
6 7 class QuerySet(HideableManager.QuerySet):
7 8 def with_scores(self):
8 9 "Annotates this QS with the voting score"
9   - return self.annotate(score=models.Sum('vote__score'))
  10 + return self.annotate(score=DefaultSum('vote__score', default=0))
10 11
11 12 def get_query_set(self):
12 13 return QuoteManager.QuerySet(self.model, using=self._db)
@@ -28,6 +29,9 @@ def get_score(self, refresh=False):
28 29 self.score = q.score
29 30 return self.score
30 31
  32 + def __unicode__(self):
  33 + return unicode(self.id)
  34 +
31 35
32 36 def check_score(score):
33 37 if abs(score) > 1:
@@ -37,3 +41,7 @@ class Vote(Hideable, models.Model):
37 41 ip = models.GenericIPAddressField()
38 42 quote = models.ForeignKey(Quote)
39 43 score = models.SmallIntegerField(validators=[check_score])
  44 +
  45 +
  46 +from django.contrib import admin
  47 +admin.site.register(Quote)
9 qdb/static/css/qdb.css
@@ -16,6 +16,15 @@
16 16 font-size: 100%;
17 17 }
18 18
  19 +.quote-list li .permalink {
  20 + display: none;
  21 + margin-left: 10px;
  22 + font-size: 80%;
  23 +}
  24 +.quote-list li:hover .permalink {
  25 + display: inline-block;
  26 +}
  27 +
19 28 .quoteform textarea {
20 29 width: 60%;
21 30 min-width: 300px;
25 qdb/templates/qdb/quote_detail.html
... ... @@ -0,0 +1,25 @@
  1 +{% extends 'base.html' %}
  2 +
  3 +{% block extra_head %}
  4 + <link rel="stylesheet" href="{{STATIC_URL}}css/qdb.css">
  5 +{% endblock extra_head %}
  6 +
  7 +{% block content %}
  8 + <ul class="quote-list">
  9 + <li>
  10 + <div class="score" data-quote='{{quote.id}}'>
  11 + <a href="#" class="upvote" data-quote='{{quote.id}}'>&uarr;</a>
  12 + <span class="novote" data-quote='{{quote.id}}'>{{ quote.score|default:0 }}</span>
  13 + <a href="#" class="downvote" data-quote='{{quote.id}}'>&darr;</a>
  14 + </div>
  15 + <blockquote>
  16 + {{ quote.body|linebreaks }}
  17 + </blockquote>
  18 + </li>
  19 + </ul>
  20 +{% endblock content %}
  21 +
  22 +{% block scripts %}
  23 +{{block.super}}
  24 +<script src="{{STATIC_URL}}js/qdb.js"></script>
  25 +{% endblock scripts %}
1  qdb/templates/qdb/quote_list.html
@@ -12,6 +12,7 @@
12 12 <a href="#" class="upvote" data-quote='{{quote.id}}'>&uarr;</a>
13 13 <span class="novote" data-quote='{{quote.id}}'>{{ quote.score|default:0 }}</span>
14 14 <a href="#" class="downvote" data-quote='{{quote.id}}'>&darr;</a>
  15 + <a href="{% url qdb:permalink quote.id %}" class="permalink">permalink</a>
15 16 </div>
16 17 <blockquote>
17 18 {{ quote.body|linebreaks }}
4 qdb/urls.py
... ... @@ -1,11 +1,13 @@
1 1 from django.conf.urls import patterns, include, url
2   -from qdb.views import TopQuotesView, NewQuotesView, CreateQuoteView
  2 +from qdb.views import TopQuotesView, NewQuotesView, CreateQuoteView, \
  3 + QuoteDetailView
3 4
4 5 urlpatterns = patterns('qdb.views',
5 6 url(r'^$', TopQuotesView.as_view(), name='home'),
6 7 url(r'^top$', TopQuotesView.as_view(), name='top'),
7 8 url(r'^newest$', NewQuotesView.as_view(), name='newest'),
8 9 url(r'^submit$', CreateQuoteView.as_view(), name='submit'),
  10 + url(r'^quotes/(?P<pk>\d+)$', QuoteDetailView.as_view(), name='permalink'),
9 11
10 12 url(r'^vote$', 'cast_vote', name='vote'),
11 13 )
15 qdb/views.py
... ... @@ -1,6 +1,7 @@
1 1 from django.shortcuts import render
  2 +from django.utils.decorators import method_decorator
2 3 from django.views.decorators.csrf import ensure_csrf_cookie
3   -from django.views.generic import ListView, CreateView
  4 +from django.views.generic import ListView, CreateView, DetailView
4 5 from django.core.urlresolvers import reverse
5 6 from django.utils.timezone import now
6 7 from django.http import HttpResponse, HttpResponseBadRequest
@@ -20,6 +21,10 @@ class QuoteListView(ListView):
20 21 def get_queryset(self):
21 22 return Quote.objects.active().with_scores()
22 23
  24 + @method_decorator(ensure_csrf_cookie)
  25 + def dispatch(self, *args, **kwargs):
  26 + return super(QuoteListView, self).dispatch(*args, **kwargs)
  27 +
23 28
24 29 class TopQuotesView(QuoteListView):
25 30 def get_queryset(self):
@@ -33,6 +38,14 @@ def get_queryset(self):
33 38 return qs.order_by('-created_at')
34 39
35 40
  41 +class QuoteDetailView(DetailView):
  42 + queryset = Quote.objects.active().with_scores()
  43 +
  44 + @method_decorator(ensure_csrf_cookie)
  45 + def dispatch(self, *args, **kwargs):
  46 + return super(QuoteDetailView, self).dispatch(*args, **kwargs)
  47 +
  48 +
36 49 def cast_vote(request):
37 50 ip = get_ip(request)
38 51
2  qute/settings.py
@@ -123,7 +123,7 @@
123 123 'django.contrib.messages',
124 124 'django.contrib.staticfiles',
125 125 # Uncomment the next line to enable the admin:
126   - # 'django.contrib.admin',
  126 + 'django.contrib.admin',
127 127 # Uncomment the next line to enable admin documentation:
128 128 # 'django.contrib.admindocs',
129 129 'hideable',
16 qute/templates/about.html
... ... @@ -0,0 +1,16 @@
  1 +{% extends 'base.html' %}
  2 +
  3 +{% block content %}
  4 + <h1>Qute.co</h1>
  5 + <p>
  6 + Just another <a href="http://bash.org">bash.org</a> clone. It's meant to
  7 + make it super easy to spin up your own QDB instance on heroku. Try it out
  8 + by following the instructions in the Readme. Please fork and submit pull
  9 + requests! Find the source at
  10 + <a href="https://github.com/noah256/qute">https://github.com/noah256/qute</a>.
  11 + </p>
  12 + <p>
  13 + Qute is a project of <a href="http://www.noahsilas.com">Noah Silas</a>
  14 + (<a href="http://twitter.com/noah256">@noah256</a>)
  15 + </p>
  16 +{% endblock content %}
2  qute/templates/base.html
@@ -46,7 +46,7 @@
46 46 <li class="{% active request 'qdb:top' %}{% active request 'home' %}"><a href="{% url home %}">Top</a></li>
47 47 <li class="{% active request 'qdb:newest' %}"><a href="{% url qdb:newest %}">Newest</a></li>
48 48 <li class="{% active request 'qdb:submit' %}"><a href="{% url qdb:submit %}">Submit</a></li>
49   - <li class="{% active request 'qdb:about' %}"><a href="#about">About</a></li>
  49 + <li class="{% active request 'about' %}"><a href="{% url about %}">About</a></li>
50 50 </ul>
51 51 </div><!--/.nav-collapse -->
52 52 </div>
11 qute/urls.py
... ... @@ -1,9 +1,11 @@
1 1 from django.conf.urls import patterns, include, url
  2 +from django.views.generic.simple import direct_to_template
2 3 from qdb.views import TopQuotesView
  4 +from django.contrib.staticfiles.urls import staticfiles_urlpatterns
3 5
4 6 # Uncomment the next two lines to enable the admin:
5   -# from django.contrib import admin
6   -# admin.autodiscover()
  7 +from django.contrib import admin
  8 +admin.autodiscover()
7 9
8 10 urlpatterns = patterns('',
9 11 # Examples:
@@ -14,8 +16,11 @@
14 16 # url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
15 17
16 18 # Uncomment the next line to enable the admin:
17   - # url(r'^admin/', include(admin.site.urls)),
  19 + url(r'^admin/', include(admin.site.urls)),
18 20
19 21 url(r'^$', TopQuotesView.as_view(), name='home'),
20 22 url(r'^qdb/', include('qdb.urls', namespace='qdb')),
  23 + url(r'^about$', direct_to_template, {'template':'about.html'}, name='about'),
21 24 )
  25 +
  26 +urlpatterns += staticfiles_urlpatterns()

No commit comments for this range

Something went wrong with that request. Please try again.