Compare commits

...

14 Commits

Author SHA1 Message Date
James Turk
8d1492f9b3 updates 2015-06-16 17:19:53 -04:00
James Turk
05dee45ee8 Update README.rst 2015-03-03 17:46:25 -05:00
James Turk
60aedff443 bump for 0.2.1 2009-12-09 14:22:56 -05:00
James Turk
0863c41293 prevent empty ideas (if only it were that easy) 2009-08-31 17:04:00 -04:00
James Turk
07921966b8 Merge branch 'master' of git@github.com:sunlightlabs/django-brainstorm 2009-08-25 16:19:09 -04:00
James Turk
4d1c1ce952 check if Idea is registered with gatekeeper 2009-08-25 16:18:01 -04:00
James Turk
3288c2af78 fix reverse subsite 2009-08-19 14:04:10 -04:00
James Turk
ca6062f5af working with the new labs site 2009-08-19 13:49:19 -04:00
James Turk
f3cfc80f15 still very much a work in progress 2009-08-19 13:04:48 -04:00
James Turk
ef03c3fa74 fix extra query for user_vote 2009-08-19 12:57:16 -04:00
James Turk
cda56dba20 post_save imported 2009-08-19 12:25:35 -04:00
James Turk
104cf2a32a attempt to merge brainstorm with what was anthill.ideas 2009-08-19 12:18:57 -04:00
James Turk
c91028d840 Revert "htmlize"
This reverts commit a1b7f2d9fa.
2009-06-26 14:15:51 -04:00
James Turk
a1b7f2d9fa htmlize 2009-06-26 14:06:15 -04:00
10 changed files with 123 additions and 94 deletions

14
CHANGELOG Normal file
View File

@ -0,0 +1,14 @@
0.2.1 - December 9 2009
=======================
- disallow submission of empty ideas
- fix a packaging bug
0.2.0 - August 25 2009
======================
- rewrite for Sunlight Labs website
- reduced a few extra queries
- integrate with django-gatekeeper if installed
0.1.0 - May 27 2009
=====
- initial working release, focused on subsites

View File

@ -1,3 +1,4 @@
Copyright (c) 2015, James Turk
Copyright (c) 2009, Sunlight Foundation Copyright (c) 2009, Sunlight Foundation
All rights reserved. All rights reserved.
@ -5,6 +6,5 @@ Redistribution and use in source and binary forms, with or without modification,
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of Sunlight Foundation nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,4 +1,5 @@
include LICENSE include LICENSE
include CHANGELOG
include README.rst include README.rst
include *.py include *.py
recursive-include brainstorm/templates * recursive-include brainstorm/templates *

View File

@ -4,21 +4,14 @@ django-brainstorm
Django app for creating a site with multiple areas to brainstorm ideas. Django app for creating a site with multiple areas to brainstorm ideas.
This app powers http://feedback.sunlightfoundation.com/hackathon/ and http://feedback.sunlightfoundation.com/oogl/ and makes it easy to create any number of these 'subsites.' Source: http://github.com/jamesturk/django-brainstorm/
django-brainstorm is a project of Sunlight Labs (c) 2009.
Written by James Turk <jturk@sunlightfoundation.com>.
All code is under a BSD-style license, see LICENSE for details.
Source: http://github.com/sunlightlabs/django-brainstorm/
Requirements Requirements
============ ============
python >= 2.4 * python >= 2.4
django >= 1.0 * django >= 1.0
Usage Usage
===== =====

View File

@ -0,0 +1 @@
__version__ = '0.2.1'

View File

@ -3,25 +3,29 @@ from brainstorm.models import Subsite
class SubsiteFeed(Feed): class SubsiteFeed(Feed):
description_template = 'feedback/idea_rss_description.html' title_template = 'brainstorm/feed_title.html'
description_template = 'brainstorm/feed_description.html'
def get_object(self, bits): def get_object(self, bits):
return Subsite.objects.get(slug__exact=bits[0]) return Subsite.objects.get(slug__exact=bits[0])
def title(self, obj): def title(self, obj):
return '%s' % obj.name return 'Latest ideas submitted for %s' % obj.name
def description(self, obj):
return 'Latest ideas submitted for %s' % obj.name
def link(self, obj): def link(self, obj):
if not obj: if not obj:
raise FeedDoesNotExist raise FeedDoesNotExist
return obj.get_absolute_url() return obj.get_absolute_url()
def description(self, obj):
return 'Latest ideas submitted for %s' % obj.name
def items(self, obj): def items(self, obj):
return obj.ideas.order_by('-submit_date')[:30] return obj.ideas.order_by('-submit_date')[:30]
def item_link(self, item):
return item.get_absolute_url()
def item_author_name(self, item): def item_author_name(self, item):
return item.user return item.user

View File

@ -3,7 +3,7 @@ from django.contrib.auth.models import User
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.contenttypes import generic from django.contrib.contenttypes import generic
from django.contrib.comments.models import Comment from django.contrib.comments.models import Comment
import secretballot from django.db.models.signals import post_save
ALLOW_ALL, REQUIRE_LOGIN, DISALLOW_ALL = range(3) ALLOW_ALL, REQUIRE_LOGIN, DISALLOW_ALL = range(3)
SUBSITE_POST_STATUS = ( SUBSITE_POST_STATUS = (
@ -26,7 +26,7 @@ class Subsite(models.Model):
return self.name return self.name
def get_absolute_url(self): def get_absolute_url(self):
return reverse('subsite', args=[self.slug]) return reverse('ideas_popular', args=[self.slug])
def user_can_post(self, user): def user_can_post(self, user):
if self.post_status == DISALLOW_ALL: if self.post_status == DISALLOW_ALL:
@ -36,10 +36,16 @@ class Subsite(models.Model):
elif self.post_status == REQUIRE_LOGIN: elif self.post_status == REQUIRE_LOGIN:
return not user.is_anonymous() return not user.is_anonymous()
class IdeaManager(models.Manager):
def with_user_vote(self, user):
return self.extra(select={'user_vote':'SELECT value FROM brainstorm_vote WHERE idea_id=brainstorm_idea.id AND user_id=%s'}, select_params=[user.id])
class Idea(models.Model): class Idea(models.Model):
title = models.CharField(max_length=100) title = models.CharField(max_length=100)
description = models.TextField() description = models.TextField()
score = models.IntegerField(default=0)
submit_date = models.DateTimeField(auto_now_add=True) submit_date = models.DateTimeField(auto_now_add=True)
@ -48,10 +54,28 @@ class Idea(models.Model):
comments = generic.GenericRelation(Comment, object_id_field='object_pk') comments = generic.GenericRelation(Comment, object_id_field='object_pk')
objects = IdeaManager()
def __unicode__(self): def __unicode__(self):
return self.title return self.title
def get_absolute_url(self): def get_absolute_url(self):
return reverse('idea', args=[self.subsite_id, self.id]) return reverse('idea_detail', args=[self.subsite_id, self.id])
secretballot.enable_voting_on(Idea) class Vote(models.Model):
user = models.ForeignKey(User, related_name='idea_votes')
idea = models.ForeignKey(Idea, related_name='votes')
value = models.IntegerField()
timestamp = models.DateTimeField(auto_now_add=True)
def __unicode__(self):
return '%s %s on %s' % (self.user, self.value, self.idea)
class Meta:
unique_together = (('user', 'idea'),)
def update_idea_votes(sender, instance, created, **kwargs):
score = instance.idea.votes.aggregate(score=models.Sum('value'))['score']
instance.idea.score = score
instance.idea.save()
post_save.connect(update_idea_votes, sender=Vote)

View File

@ -1,8 +1,11 @@
from django.conf.urls.defaults import * from django.conf.urls.defaults import *
from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from brainstorm.models import Idea from brainstorm.models import Idea
from brainstorm.feeds import SubsiteFeed from brainstorm.feeds import SubsiteFeed
BRAINSTORM_USE_SECRETBALLOT = getattr(settings, 'BRAINSTORM_USE_SECRETBALLOT', False)
feeds = { feeds = {
'latest': SubsiteFeed, 'latest': SubsiteFeed,
} }
@ -14,22 +17,23 @@ urlpatterns = patterns('',
) )
urlpatterns += patterns('brainstorm.views', urlpatterns += patterns('brainstorm.views',
url(r'^submit_comment/$', 'submit_comment', name='submit_idea_comment'), url(r'^(?P<slug>[\w-]+)/$', 'idea_list', {'ordering': 'most_popular'}, name='ideas_popular'),
url(r'^(?P<slug>[\w-]+)/$', 'idea_list', {'ordering': 'most_popular'}, name='subsite'), url(r'^(?P<slug>[\w-]+)/latest/$', 'idea_list', {'ordering': 'latest'}, name='ideas_latest'),
url(r'^(?P<slug>[\w-]+)/latest/$', 'idea_list', {'ordering': 'latest'}, name='subsite_latest'), url(r'^(?P<slug>[\w-]+)/(?P<id>\d+)/$', 'idea_detail', name='idea_detail'),
url(r'^(?P<slug>[\w-]+)/(?P<id>\d+)/$', 'idea_detail', name='idea'),
url(r'^(?P<slug>[\w-]+)/new_idea/$', 'new_idea', name='new_idea'), url(r'^(?P<slug>[\w-]+)/new_idea/$', 'new_idea', name='new_idea'),
url(r'^vote/$', 'vote', name='idea_vote'),
) )
urlpatterns = patterns('secretballot.views', if BRAINSTORM_USE_SECRETBALLOT:
url(r'^vote_up/(?P<object_id>\d+)/$', 'vote', urlpatterns = patterns('secretballot.views',
{'content_type': ContentType.objects.get_for_model(Idea), 'vote': 1}, url(r'^vote_up/(?P<object_id>\d+)/$', 'vote',
name='vote_up'), {'content_type': ContentType.objects.get_for_model(Idea), 'vote': 1},
url(r'^vote_down/(?P<object_id>\d+)/$', 'vote', name='vote_up'),
{'content_type': ContentType.objects.get_for_model(Idea), 'vote': -1}, url(r'^vote_down/(?P<object_id>\d+)/$', 'vote',
name='vote_down'), {'content_type': ContentType.objects.get_for_model(Idea), 'vote': -1},
url(r'^unvote/(?P<object_id>\d+)/$', 'vote', name='vote_down'),
{'content_type': ContentType.objects.get_for_model(Idea), 'vote': 0}, url(r'^unvote/(?P<object_id>\d+)/$', 'vote',
name='unvote'), {'content_type': ContentType.objects.get_for_model(Idea), 'vote': 0},
) + urlpatterns name='unvote'),
) + urlpatterns

View File

@ -1,77 +1,65 @@
import datetime import datetime
from django.template import RequestContext from django.template import RequestContext
from django.core.paginator import Paginator, InvalidPage, EmptyPage
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404, render_to_response from django.shortcuts import get_object_or_404, render_to_response, redirect
from django.http import HttpResponseRedirect from django.http import HttpResponse
from django.contrib.comments.models import Comment from django.contrib.comments.models import Comment
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.views.generic import list_detail
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from brainstorm.models import Subsite, Idea from django.contrib.auth.decorators import login_required
from django.conf import settings
from brainstorm.models import Subsite, Idea, Vote
def idea_list(request, slug, ordering='-total_upvotes'): def idea_list(request, slug, ordering='-total_upvotes'):
subsite = get_object_or_404(Subsite, pk=slug) ordering_db = {'most_popular': '-score',
ordering_db = {'most_popular': '-total_upvotes',
'latest': '-submit_date'}[ordering] 'latest': '-submit_date'}[ordering]
paginator = Paginator(Idea.objects.from_request(request).filter(subsite=subsite).order_by(ordering_db), qs = Idea.objects.with_user_vote(request.user).filter(subsite__slug=slug).select_related().order_by(ordering_db)
subsite.ideas_per_page) if hasattr(qs, '_gatekeeper'):
qs = qs.approved()
try: return list_detail.object_list(request, queryset=qs,
page = int(request.GET.get('page', '1')) extra_context={'ordering': ordering, 'subsite':slug}, paginate_by=10,
except ValueError: template_object_name='idea')
page = 1
try:
ideas = paginator.page(page)
except (EmptyPage, InvalidPage):
ideas = paginator.page(paginator.num_pages)
return render_to_response('brainstorm/index.html',
{'subsite':subsite, 'ideas': ideas,
'ordering': ordering,
'user_can_post': subsite.user_can_post(request.user)},
context_instance=RequestContext(request))
def idea_detail(request, slug, id): def idea_detail(request, slug, id):
subsite = get_object_or_404(Subsite, pk=slug) idea = get_object_or_404(Idea.objects.with_user_vote(request.user), pk=id, subsite__slug=slug)
idea = get_object_or_404(Idea.objects.from_request(request), return render_to_response('brainstorm/idea_detail.html',
subsite=slug, pk=id) {'idea': idea},
return render_to_response('brainstorm/idea.html',
{'subsite':subsite, 'idea': idea,
'user_can_post': subsite.user_can_post(request.user)},
context_instance=RequestContext(request)) context_instance=RequestContext(request))
@require_POST @require_POST
def new_idea(request, slug): def new_idea(request, slug):
subsite = get_object_or_404(Subsite, pk=slug) subsite = get_object_or_404(Subsite, pk=slug)
if not subsite.user_can_post(request.user): if not subsite.user_can_post(request.user):
return HttpResponseRedirect(subsite.get_absolute_url()) return redirect(subsite.get_absolute_url())
title = request.POST['title'] title = request.POST['title']
description = request.POST['description'] description = request.POST['description']
if request.user.is_anonymous(): if not title.strip() or not description.strip():
user = None return redirect(subsite.get_absolute_url())
else: user = request.user
user = request.user idea = Idea.objects.create(title=title, description=description, user=user,
idea = Idea.objects.create(title=title, description=description, subsite=subsite)
user=user, subsite=subsite) return redirect(idea)
return HttpResponseRedirect(idea.get_absolute_url())
@require_POST @require_POST
def submit_comment(request): @login_required
from django.conf import settings def vote(request):
content_type = ContentType.objects.get_for_model(Idea).id idea_id = int(request.POST.get('idea'))
site = settings.SITE_ID score = int(request.POST.get('score'))
object_pk = request.POST['idea_id'] if score not in (0,1):
name = request.POST.get('name', 'anonymous') score = 0
email = request.POST.get('email', '') idea = get_object_or_404(Idea, pk=idea_id)
url = request.POST.get('url', '') score_diff = score
comment = request.POST['comment'] vote, created = Vote.objects.get_or_create(user=request.user, idea=idea,
date = datetime.datetime.now() defaults={'value':score})
ip = request.META['REMOTE_ADDR'] if not created:
c = Comment.objects.create(user_name=name, user_email=email, user_url=url, new_score = idea.score + (score-vote.value)
comment=comment, submit_date=date, ip_address=ip, vote.value = score
site_id=site, content_type_id=content_type, object_pk=object_pk) vote.save()
idea = Idea.objects.get(pk=object_pk) else:
linkback = '%s#c%s' % (idea.get_absolute_url(), c.id) new_score = idea.score
return HttpResponseRedirect(linkback)
if request.is_ajax():
return HttpResponse("{'score':%d}" % new_score)
return redirect(idea)

View File

@ -4,15 +4,15 @@ long_description = open('README.rst').read()
setup( setup(
name='django-brainstorm', name='django-brainstorm',
version="0.1", version="0.2.1",
package_dir={'brainstorm': 'brainstorm'}, package_dir={'brainstorm': 'brainstorm'},
packages=['brainstorm'], packages=['brainstorm'],
package_data={'brainstorm': ['templates/brainstorm/*.html']}, package_data={'brainstorm': ['templates/brainstorm/*.html']},
description='Django brainstorming site', description='Django brainstorming site',
author='James Turk', author='James Turk',
author_email='jturk@sunlightfoundation.com', author_email='james.p.turk@gmail.com',
license='BSD License', license='BSD License',
url='http://github.com/sunlightlabs/django-brainstorm/', url='http://github.com/jamesturk/django-brainstorm/',
long_description=long_description, long_description=long_description,
platforms=["any"], platforms=["any"],
classifiers=[ classifiers=[