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="&copy; 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')),