Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8d1492f9b3 | ||
![]() |
05dee45ee8 | ||
![]() |
60aedff443 | ||
![]() |
0863c41293 | ||
![]() |
07921966b8 | ||
![]() |
4d1c1ce952 | ||
![]() |
3288c2af78 | ||
![]() |
ca6062f5af | ||
![]() |
f3cfc80f15 | ||
![]() |
ef03c3fa74 | ||
![]() |
cda56dba20 | ||
![]() |
104cf2a32a | ||
![]() |
c91028d840 | ||
![]() |
a1b7f2d9fa |
14
CHANGELOG
Normal file
14
CHANGELOG
Normal 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
|
2
LICENSE
2
LICENSE
@ -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.
|
||||||
|
@ -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 *
|
||||||
|
13
README.rst
13
README.rst
@ -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
|
||||||
=====
|
=====
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
__version__ = '0.2.1'
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
6
setup.py
6
setup.py
@ -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=[
|
||||||
|
Loading…
Reference in New Issue
Block a user