Mercurial > public > sg101
changeset 540:51fa1e0ca218
For #243, create a contests application.
author | Brian Neal <bgneal@gmail.com> |
---|---|
date | Mon, 09 Jan 2012 01:13:08 +0000 |
parents | 2f0a372c92b4 |
children | 06300c08bf81 |
files | gpp/contests/__init__.py gpp/contests/admin.py gpp/contests/models.py gpp/contests/static/js/contests/contests.js gpp/contests/static/js/contests/contests_admin.js gpp/contests/tests/__init__.py gpp/contests/tests/model_tests.py gpp/contests/tests/view_tests.py gpp/contests/urls.py gpp/contests/views.py gpp/settings/base.py gpp/templates/base.html gpp/templates/contests/contest_detail.html gpp/templates/contests/contest_list.html gpp/urls.py |
diffstat | 14 files changed, 631 insertions(+), 1 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gpp/contests/admin.py Mon Jan 09 01:13:08 2012 +0000 @@ -0,0 +1,50 @@ +""" +Admin definitions for the contest application. + +""" +from django.contrib import admin +from django.conf import settings + +from contests.models import Contest + + +class ContestAdmin(admin.ModelAdmin): + list_display = ['title', 'is_public', 'creation_date', 'end_date', + 'contestant_count', 'winner'] + list_editable = ['is_public'] + date_hierarchy = 'creation_date' + search_fields = ['title', 'description'] + prepopulated_fields = {'slug': ['title']} + raw_id_fields = ['winner', 'contestants'] + actions = ['pick_winner'] + + class Media: + js = (['js/contests/contests_admin.js'] + + settings.GPP_THIRD_PARTY_JS['tiny_mce']) + + def contestant_count(self, obj): + return obj.contestants.count() + contestant_count.short_description = '# Entries' + + def pick_winner(self, request, qs): + """ + Picks a winner on the contests selected by the admin. Note that for + safety reasons, we only update those contests that don't have winners + already. + + """ + count = 0 + for contest in qs: + if not contest.winner: + contest.pick_winner() + contest.save() + count += 1 + + self.message_user(request, "%d of %d winners picked" % (count, + qs.count())) + + pick_winner.short_description = "Pick winners for selected contests" + + + +admin.site.register(Contest, ContestAdmin)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gpp/contests/models.py Mon Jan 09 01:13:08 2012 +0000 @@ -0,0 +1,93 @@ +""" +Models for the contest application. + +""" +import random +import datetime + +from django.db import models +from django.contrib.auth.models import User + + +class PublicContestManager(models.Manager): + """ + The manager for all public contests. + + """ + def get_query_set(self): + return super(PublicContestManager, self).get_query_set().filter(is_public=True) + + +class Contest(models.Model): + """ + A model to represent contests where users sign up to win something. + + """ + title = models.CharField(max_length=64) + slug = models.SlugField(max_length=64) + description = models.TextField() + is_public = models.BooleanField(db_index=True) + creation_date = models.DateTimeField(blank=True) + end_date = models.DateTimeField() + contestants = models.ManyToManyField(User, related_name='contests', + null=True, blank=True) + winner = models.ForeignKey(User, null=True, blank=True, + related_name='winning_contests') + win_date = models.DateTimeField(null=True, blank=True) + meta_description = models.TextField() + + objects = models.Manager() + public_objects = PublicContestManager() + + class Meta: + ordering = ['-creation_date'] + + def __unicode__(self): + return self.title + + @models.permalink + def get_absolute_url(self): + return ('contests-contest', [], {'slug': self.slug}) + + def save(self, *args, **kwargs): + if not self.pk and not self.creation_date: + self.creation_date = datetime.datetime.now() + + super(Contest, self).save(*args, **kwargs) + + def is_active(self): + """ + Returns True if the contest is still active. + + """ + now = datetime.datetime.now() + return self.creation_date <= now < self.end_date + + def can_enter(self): + """ + Returns True if the contest is still active and does not have a winner. + + """ + return not self.winner and self.is_active() + + def pick_winner(self): + """ + This function randomly picks a winner from all the contestants. + + """ + user_ids = self.contestants.values_list('id', flat=True) + winner_id = random.choice(user_ids) + self.winner = User.objects.get(id=winner_id) + self.win_date = datetime.datetime.now() + + def ogp_tags(self): + """ + Returns a dict of Open Graph Protocol meta tags. + + """ + return { + 'og:title': self.title, + 'og:type': 'game', + 'og:url': self.get_absolute_url(), + 'og:description': self.meta_description, + }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gpp/contests/static/js/contests/contests.js Mon Jan 09 01:13:08 2012 +0000 @@ -0,0 +1,30 @@ +$(function() { + var $button = $('#contest-button'); + $button.click(function() { + var buttonLabel = $button.text(); + $button.attr('disabled', 'disabled').val('Please wait...'); + + $.ajax({ + url: '/contests/enter/', + type: 'POST', + data: { + contest_id : contest_id + }, + dataType: 'json', + success: function (data, textStatus) { + var classname = data.entered ? 'success' : 'info'; + var $p = $('#contest-entry'); + $p.hide(); + $p.addClass(classname); + $p.html(data.msg); + $p.fadeIn(3000); + }, + error: function (xhr, textStatus, ex) { + alert('Oops, an error occurred. ' + xhr.statusText + ' - ' + + xhr.responseText); + $button.removeAttr('disabled').text(buttonLabel); + } + }); + return false; + }); +});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gpp/contests/static/js/contests/contests_admin.js Mon Jan 09 01:13:08 2012 +0000 @@ -0,0 +1,3 @@ +django.jQuery(document).ready(function() { + django.jQuery('#id_meta_description').addClass('mceNoEditor'); +});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gpp/contests/tests/__init__.py Mon Jan 09 01:13:08 2012 +0000 @@ -0,0 +1,2 @@ +from model_tests import * +from view_tests import *
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gpp/contests/tests/model_tests.py Mon Jan 09 01:13:08 2012 +0000 @@ -0,0 +1,166 @@ +""" +Model tests for the contests application. + +""" +import datetime + +from django.test import TestCase +from django.contrib.auth.models import User + +from contests.models import Contest + + +class ContestTestCase(TestCase): + + def test_creation_date(self): + + c = Contest(title='test', + slug='test', + description='test', + is_public=False, + end_date=datetime.datetime.now() + datetime.timedelta(days=30)) + + c.save() + + self.assertTrue(c.creation_date) + self.assertTrue(datetime.datetime.now() - c.creation_date < + datetime.timedelta(seconds=1)) + + def test_is_active(self): + + now = datetime.datetime.now() + start = now + datetime.timedelta(days=7) + end = start + datetime.timedelta(days=30) + + c = Contest(title='test', + slug='test', + description='test', + is_public=False, + creation_date=start, + end_date=end) + + self.failIf(c.is_active()) + + start = now - datetime.timedelta(days=7) + end = start + datetime.timedelta(days=30) + + c = Contest(title='test', + slug='test', + description='test', + is_public=True, + creation_date=start, + end_date=end) + + self.assertTrue(c.is_active()) + + start = now - datetime.timedelta(days=7) + end = start - datetime.timedelta(days=3) + + c = Contest(title='test', + slug='test', + description='test', + is_public=True, + creation_date=start, + end_date=end) + + self.failIf(c.is_active()) + + def test_can_enter(self): + + now = datetime.datetime.now() + start = now + datetime.timedelta(days=7) + end = start + datetime.timedelta(days=30) + + c = Contest(title='test', + slug='test', + description='test', + is_public=False, + creation_date=start, + end_date=end) + + self.failIf(c.can_enter()) + + start = now - datetime.timedelta(days=7) + end = start + datetime.timedelta(days=30) + + c = Contest(title='test', + slug='test', + description='test', + is_public=True, + creation_date=start, + end_date=end) + + self.assertTrue(c.can_enter()) + + start = now - datetime.timedelta(days=7) + end = start - datetime.timedelta(days=3) + + c = Contest(title='test', + slug='test', + description='test', + is_public=True, + creation_date=start, + end_date=end) + + self.failIf(c.can_enter()) + + start = now - datetime.timedelta(days=7) + end = start + datetime.timedelta(days=30) + + user = User.objects.create_user('test_user', '', 'password') + user.save() + + c = Contest(title='test', + slug='test', + description='test', + is_public=True, + creation_date=start, + end_date=end, + winner=user, + win_date=now) + + self.failIf(c.can_enter()) + + start = now - datetime.timedelta(days=7) + end = start - datetime.timedelta(days=3) + + c = Contest(title='test', + slug='test', + description='test', + is_public=True, + creation_date=start, + end_date=end, + winner=user, + win_date=end + datetime.timedelta(days=1)) + + self.failIf(c.can_enter()) + + def test_pick_winner(self): + + now = datetime.datetime.now() + start = now - datetime.timedelta(days=7) + end = start - datetime.timedelta(days=3) + + c = Contest(title='test', + slug='test', + description='test', + is_public=False, + creation_date=start, + end_date=end) + c.save() + + user1 = User.objects.create_user('test_user1', '', 'password') + user1.save() + user2 = User.objects.create_user('test_user2', '', 'password') + user2.save() + user3 = User.objects.create_user('test_user3', '', 'password') + user3.save() + + c.contestants.add(user1, user2, user3) + + c.pick_winner() + + self.assertTrue(datetime.datetime.now() - c.win_date < + datetime.timedelta(seconds=1)) + self.assertTrue(c.winner.id in [user1.id, user2.id, user3.id]) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gpp/contests/tests/view_tests.py Mon Jan 09 01:13:08 2012 +0000 @@ -0,0 +1,123 @@ +""" +View tests for the contests application. + +""" +import datetime +from django.test import TestCase +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from django.utils import simplejson + +from contests.models import Contest + + +class NoConstestsTestCase(TestCase): + + def test_no_contests(self): + response = self.client.get(reverse('contests-index')) + self.assertEqual(response.status_code, 200) + + url = reverse('contests-contest', kwargs={'slug': 'test'}) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + +class ConstestsTestCase(TestCase): + + def setUp(self): + now = datetime.datetime.now() + start = now - datetime.timedelta(days=7) + end = start - datetime.timedelta(days=3) + + user = User.objects.create_user('test_user', '', 'password') + user.save() + + c = Contest(title='test', + slug='test', + description='test', + is_public=True, + creation_date=start, + end_date=end, + winner=user, + win_date=end + datetime.timedelta(days=1)) + c.save() + self.contest_id = c.id + + def test_contests(self): + response = self.client.get(reverse('contests-index')) + self.assertEqual(response.status_code, 200) + + url = reverse('contests-contest', kwargs={'slug': 'test'}) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + +class ContestEntryTestCase(TestCase): + + def setUp(self): + self.username = 'test_user' + self.pw = 'password' + self.user = User.objects.create_user(self.username, '', self.pw) + self.user.save() + self.assertTrue(self.client.login(username=self.username, + password=self.pw)) + + now = datetime.datetime.now() + start = now - datetime.timedelta(days=7) + end = now + datetime.timedelta(days=3) + + c = Contest(title='test', + slug='test', + description='test', + is_public=True, + creation_date=start, + end_date=end) + c.save() + self.contest_id = c.id + + def test_entry_toggle(self): + response = self.client.post(reverse('contests-enter'), + {'contest_id': self.contest_id}, + HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + json = simplejson.loads(response.content) + self.assertTrue(json['entered']) + + contest = Contest.objects.get(pk=self.contest_id) + self.assertTrue(self.user in contest.contestants.all()) + + response = self.client.post(reverse('contests-enter'), + {'contest_id': self.contest_id}, + HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + json = simplejson.loads(response.content) + self.failIf(json['entered']) + + contest = Contest.objects.get(pk=self.contest_id) + self.failIf(self.user in contest.contestants.all()) + + +class NoPublicConstestsTestCase(TestCase): + + def setUp(self): + now = datetime.datetime.now() + start = now - datetime.timedelta(days=7) + end = start - datetime.timedelta(days=3) + + c = Contest(title='test', + slug='test', + description='test', + is_public=False, + creation_date=start, + end_date=end) + c.save() + + def test_contests(self): + response = self.client.get(reverse('contests-index')) + self.assertEqual(response.status_code, 200) + + url = reverse('contests-contest', kwargs={'slug': 'test'}) + response = self.client.get(url) + self.assertEqual(response.status_code, 404)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gpp/contests/urls.py Mon Jan 09 01:13:08 2012 +0000 @@ -0,0 +1,27 @@ +""" +Url patterns for the contests application. + +""" +from django.conf.urls.defaults import patterns, url +from django.views.generic import DetailView, ListView + +from contests.models import Contest + + +urlpatterns = patterns('', + url(r'^$', + ListView.as_view( + context_object_name='contests', + queryset=Contest.public_objects.all()), + name='contests-index'), + + url(r'^enter/$', + 'contests.views.enter', + name='contests-enter'), + + url(r'^c/(?P<slug>[\w-]+)/$', + DetailView.as_view( + context_object_name='contest', + queryset=Contest.public_objects.all().select_related('winner')), + name='contests-contest'), +)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gpp/contests/views.py Mon Jan 09 01:13:08 2012 +0000 @@ -0,0 +1,46 @@ +""" +Views for the contests application. + +""" +from django.http import (HttpResponse, HttpResponseForbidden, + HttpResponseBadRequest) +from django.shortcuts import get_object_or_404 +from django.utils import simplejson +from django.views.decorators.http import require_POST + +from contests.models import Contest + + +@require_POST +def enter(request): + """ + This view is an AJAX view that is used to enter or withdraw a user from a + given contest. This function toggles the user's entered state in the + contest. + + """ + if not request.user.is_authenticated(): + return HttpResponseForbidden("Please login first") + + contest_id = request.POST.get('contest_id') + if not contest_id: + return HttpResponseBadRequest("Missing contest_id") + + contest = get_object_or_404(Contest, pk=contest_id) + if not contest.can_enter(): + return HttpResponseForbidden("Contest is over") + + # Toggle the user's state in the contest + + result = {} + if request.user in contest.contestants.all(): + contest.contestants.remove(request.user) + result['entered'] = False + result['msg'] = 'You have been withdrawn from this contest.' + else: + contest.contestants.add(request.user) + result['entered'] = True + result['msg'] = 'You have been entered into this contest!' + + json = simplejson.dumps(result) + return HttpResponse(json, content_type='application/json')
--- a/gpp/settings/base.py Sat Jan 07 02:12:52 2012 +0000 +++ b/gpp/settings/base.py Mon Jan 09 01:13:08 2012 +0000 @@ -125,6 +125,7 @@ 'bulletins', 'comments', 'contact', + 'contests', 'core', 'custom_search', 'donations',
--- a/gpp/templates/base.html Sat Jan 07 02:12:52 2012 +0000 +++ b/gpp/templates/base.html Mon Jan 09 01:13:08 2012 +0000 @@ -18,7 +18,7 @@ <meta http-equiv="Content-Language" content="en-US" /> <meta name="robots" content="all" /> <meta name="Author" content="Brian Neal" /> -<meta name="copyright" content="© 2006 - 2011 Brian Neal" /> +<meta name="copyright" content="(C) Copyright 2006 - 2012 Brian Neal" /> {% block custom_meta %}{% endblock %} <link rel="stylesheet" href="{{ STATIC_URL }}css/blueprint/screen.css" type="text/css" media="screen, projection" /> <link rel="stylesheet" href="{{ STATIC_URL }}css/blueprint/print.css" type="text/css" media="print" /> @@ -56,6 +56,7 @@ <li><a href="{% url 'news-submit' %}">Submit News</a></li> <li><a href="{% url 'gcalendar-index' %}">Calendar</a></li> <li><a href="{% url 'contact-form' %}">Contact</a></li> + <li><a href="{% url 'contests-index' %}">Contests</a></li> <li><a href="{% url 'donations-index' %}">Donations</a></li> <li><a href="{% url 'downloads-index' %}">Downloads</a></li> <li><a href="{% url 'forums-index' %}">Forums</a></li>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gpp/templates/contests/contest_detail.html Mon Jan 09 01:13:08 2012 +0000 @@ -0,0 +1,68 @@ +{% extends 'base.html' %} +{% load url from future %} +{% load bio_tags %} +{% load core_tags %} +{% block custom_meta %} +{% open_graph_meta_tags contest %} +{% endblock %} +{% block title %}Contests: {{ contest.title }}{% endblock %} +{% block custom_js %} +<script type="text/javascript"> + var contest_id = {{ contest.id }}; +</script> +<script type="text/javascript" src="{{ STATIC_URL }}js/contests/contests.js"></script> +{% endblock %} + +{% block begin_body %}{% include 'core/social_sharing_begin.html' %}{% endblock %} +{% block content %} +<h2><a href="{% url 'contests-index' %}">Contests</a></h2> +<h3><a href="{% url 'contests-contest' slug=contest.slug %}">{{ contest.title }}</a></h3> +<div> + {{ contest.description|safe }} +</div> + +<h4>Contest Dates</h4> +<p> +{% if contest.is_active %} +This contest began on {{ contest.creation_date|date:"F d, Y" }} and ends on +{{ contest.end_date|date:"F d, Y" }}. +{% else %} +This contest ran from {{ contest.creation_date|date:"F d, Y" }} to +{{ contest.end_date|date:"F d, Y" }}. +{% endif %} +</p> + +<h4>Contest Entry</h4> +<p id="contest-entry"> +{% if contest.can_enter and user.is_authenticated %} + {% if user in contest.contestants.all %} + You are currently entered into this contest.<br /> + <button id="contest-button">Withdraw from this contest</button> + {% else %} + You are not participating in this contest.<br /> + <button id="contest-button">Please enter me into this contest!</button> + {% endif %} +{% else %} + {% if contest.can_enter %} + Please <a href="{% url 'accounts-login' %}?next={{ contest.get_absolute_url }}">login</a> + to enter this contest. + {% else %} + Sorry, we are no longer accepting entries to this contest. + {% endif %} +{% endif %} +</p> + +<h4>Contest Winner</h4> +<p> +{% if contest.winner %} +The winner of this contest, selected on {{ contest.win_date|date:"l, F d, Y" }}, is +{% profile_link contest.winner.username %}. Congratulations to <strong>{{ contest.winner.username }}</strong>! +{% else %} +A contest winner will be determined sometime on or after {{ contest.end_date|date:"l, F d, Y" }}. +Please check back later. +{% endif %} +</p> +<hr /> +{% social_sharing contest.title contest.get_absolute_url %} +{% endblock %} +{% block end_body %}{% include 'core/social_sharing_end.html' %}{% endblock %}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gpp/templates/contests/contest_list.html Mon Jan 09 01:13:08 2012 +0000 @@ -0,0 +1,19 @@ +{% extends 'base.html' %} +{% block title %}Contests{% endblock %} +{% block content %} +<h2>Contests</h2> +<p>From time to time, SurfGuitar101.com may hold drawings and contests. This page lists +the current and past contests on the site.</p> +{% if object_list %} +<ul> + {% for contest in contests %} + <li><a href="{{ contest.get_absolute_url }}">{{ contest.title }}</a> - + {{ contest.creation_date|date:"F d, Y" }} to {{ contest.end_date|date:"F d, Y" }} + {% if contest.is_active %}<img src="{{ STATIC_URL }}icons/new.png" alt="New" />{% else %}<img src="{{ STATIC_URL }}icons/lock.png" alt="Old" />{% endif %} + </li> + {% endfor %} +</ul> +{% else %} +<p>No contests at this time.</p> +{% endif %} +{% endblock %}
--- a/gpp/urls.py Sat Jan 07 02:12:52 2012 +0000 +++ b/gpp/urls.py Mon Jan 09 01:13:08 2012 +0000 @@ -21,6 +21,7 @@ (r'^calendar/', include('gcalendar.urls')), (r'^comments/', include('comments.urls')), (r'^contact/', include('contact.urls')), + (r'^contests/', include('contests.urls')), (r'^core/', include('core.urls')), (r'^donations/', include('donations.urls')), (r'^downloads/', include('downloads.urls')),