From cc7c89ea8886aa8f3b1f223aab2566e46aef0b83 Mon Sep 17 00:00:00 2001 From: Les Orchard Date: Tue, 2 Aug 2011 14:45:46 -0400 Subject: [PATCH] Adopted Nomination logic from django-badger --- badger_multiplayer/admin.py | 13 ++ badger_multiplayer/forms.py | 4 +- badger_multiplayer/models.py | 145 ++++++++++++++++++++++ badger_multiplayer/signals.py | 14 +++ badger_multiplayer/tests/__init__.py | 1 - badger_multiplayer/tests/test_models.py | 153 ++++++++++++++++++++++++ badger_multiplayer/tests/test_views.py | 9 +- badger_multiplayer/views.py | 8 +- 8 files changed, 336 insertions(+), 11 deletions(-) create mode 100644 badger_multiplayer/signals.py create mode 100644 badger_multiplayer/tests/test_models.py diff --git a/badger_multiplayer/admin.py b/badger_multiplayer/admin.py index e69de29..106aff7 100644 --- a/badger_multiplayer/admin.py +++ b/badger_multiplayer/admin.py @@ -0,0 +1,13 @@ +from django.contrib import admin + +from django import forms +from django.db import models + +from .models import (Badge, Nomination) + + +class NominationAdmin(admin.ModelAdmin): + pass + + +admin.site.register(Nomination, NominationAdmin) diff --git a/badger_multiplayer/forms.py b/badger_multiplayer/forms.py index b431b61..5aa0f4a 100644 --- a/badger_multiplayer/forms.py +++ b/badger_multiplayer/forms.py @@ -11,7 +11,8 @@ try: except ImportError, e: from django.utils.translation import ugettext_lazy as _ -from badger.models import (Badge, Award, Nomination) +from badger.models import (Award) +from badger_multiplayer.models import (Badge, Nomination) class MyModelForm(forms.ModelForm): @@ -62,4 +63,3 @@ class BadgeNewForm(BadgeEditForm): super(BadgeNewForm, self).__init__(*args, **kwargs) #if not settings.RECAPTCHA_PRIVATE_KEY: # del self.fields['captcha'] - diff --git a/badger_multiplayer/models.py b/badger_multiplayer/models.py index e69de29..d527ef8 100644 --- a/badger_multiplayer/models.py +++ b/badger_multiplayer/models.py @@ -0,0 +1,145 @@ +from django.db import models +from django.db.models import signals +from django.core.serializers.json import DjangoJSONEncoder +from django.utils import simplejson as json +from django.contrib.auth.models import User, AnonymousUser + +from django.template.defaultfilters import slugify + +try: + from commons.urlresolvers import reverse +except ImportError, e: + from django.core.urlresolvers import reverse + +import badger.models +from badger.models import (Award, BadgerException, + BadgeAlreadyAwardedException, + get_permissions_for) + +from .signals import (nomination_will_be_approved, nomination_was_approved, + nomination_will_be_accepted, nomination_was_accepted, + user_will_be_nominated, user_was_nominated,) + + +class Badge(badger.models.Badge): + """Enhanced Badge model with multiplayer features""" + class Meta: + proxy = True + + def nominate_for(self, nominee, nominator=None): + """Nominate a nominee for this badge on the nominator's behalf""" + return Nomination.objects.create(badge=self, creator=nominator, + nominee=nominee) + + def is_nominated_for(self, user): + return Nomination.objects.filter(nominee=user, badge=self).count() > 0 + + +class NominationException(BadgerException): + """Nomination model exception""" + + +class NominationApproveNotAllowedException(NominationException): + """Attempt to approve a nomination was disallowed""" + + +class NominationAcceptNotAllowedException(NominationException): + """Attempt to accept a nomination was disallowed""" + + +class NominationManager(models.Manager): + pass + + +class Nomination(models.Model): + """Representation of a user nominated by another user for a badge""" + objects = NominationManager() + + badge = models.ForeignKey(Badge) + nominee = models.ForeignKey(User, related_name="nomination_nominee", + blank=False, null=False) + accepted = models.BooleanField(default=False) + creator = models.ForeignKey(User, related_name="nomination_creator", + blank=True, null=True) + approver = models.ForeignKey(User, related_name="nomination_approver", + blank=True, null=True) + award = models.ForeignKey(Award, null=True, blank=True) + created = models.DateTimeField(auto_now_add=True, blank=False) + modified = models.DateTimeField(auto_now=True, blank=False) + + get_permissions_for = get_permissions_for + + def __unicode__(self): + return u'Nomination for %s to %s by %s' % (self.badge, self.nominee, + self.creator) + + def save(self, *args, **kwargs): + + # Signals and some bits of logic only happen on a new nomination. + is_new = not self.pk + + # Bail if this is an attempt to double-award a unique badge + if (is_new and self.badge.unique and + self.badge.is_awarded_to(self.nominee)): + raise BadgeAlreadyAwardedException() + + if is_new: + user_will_be_nominated.send(sender=self.__class__, + nomination=self) + + if self.is_approved() and self.is_accepted(): + self.award = self.badge.award_to(self.nominee, self.approver) + + super(Nomination, self).save(*args, **kwargs) + + if is_new: + user_was_nominated.send(sender=self.__class__, + nomination=self) + + def allows_approve_by(self, user): + if user.is_staff or user.is_superuser: + return True + if user == self.badge.creator: + return True + return False + + def approve_by(self, approver): + """Approve this nomination. + Also awards, if already accepted.""" + if not self.allows_approve_by(approver): + raise NominationApproveNotAllowedException() + self.approver = approver + nomination_will_be_approved.send(sender=self.__class__, + nomination=self) + self.save() + nomination_was_approved.send(sender=self.__class__, + nomination=self) + return self + + def is_approved(self): + """Has this nomination been approved?""" + return self.approver is not None + + def allows_accept(self, user): + if user.is_staff or user.is_superuser: + return True + if user == self.nominee: + return True + return False + + def accept(self, user): + """Accept this nomination for the nominee. + Also awards, if already approved.""" + if not self.allows_accept(user): + raise NominationAcceptNotAllowedException() + self.accepted = True + nomination_will_be_accepted.send(sender=self.__class__, + nomination=self) + self.save() + nomination_was_accepted.send(sender=self.__class__, + nomination=self) + return self + + def is_accepted(self): + """Has this nomination been accepted?""" + return self.accepted diff --git a/badger_multiplayer/signals.py b/badger_multiplayer/signals.py new file mode 100644 index 0000000..0567045 --- /dev/null +++ b/badger_multiplayer/signals.py @@ -0,0 +1,14 @@ +""" +Signals relating to badges. +""" +from django.dispatch import Signal + + +user_will_be_nominated = Signal(providing_args=["nomination"]) +user_was_nominated = Signal(providing_args=["nomination"]) + +nomination_will_be_approved = Signal(providing_args=["nomination"]) +nomination_was_approved = Signal(providing_args=["nomination"]) + +nomination_will_be_accepted = Signal(providing_args=["nomination"]) +nomination_was_accepted = Signal(providing_args=["nomination"]) diff --git a/badger_multiplayer/tests/__init__.py b/badger_multiplayer/tests/__init__.py index d88837e..407f0e3 100644 --- a/badger_multiplayer/tests/__init__.py +++ b/badger_multiplayer/tests/__init__.py @@ -28,4 +28,3 @@ class BadgerTestCase(test.TestCase): # Restore the settings. settings.INSTALLED_APPS = self._original_installed_apps loading.cache.loaded = False - diff --git a/badger_multiplayer/tests/test_models.py b/badger_multiplayer/tests/test_models.py new file mode 100644 index 0000000..996673a --- /dev/null +++ b/badger_multiplayer/tests/test_models.py @@ -0,0 +1,153 @@ +import logging + +from django.conf import settings + +from django.core.management import call_command +from django.db.models import loading + +from django.http import HttpRequest +from django.test.client import Client + +from nose.tools import assert_equal, with_setup, assert_false, eq_, ok_ +from nose.plugins.attrib import attr + +from django.template.defaultfilters import slugify + +from django.contrib.auth.models import User + +from . import BadgerTestCase + +import badger +import badger_multiplayer + +from badger.models import (Award, Progress, + BadgeAwardNotAllowedException, + BadgeAlreadyAwardedException) + +from badger_multiplayer.models import (Badge, Nomination, + NominationApproveNotAllowedException, + NominationAcceptNotAllowedException) + +from badger_test.models import GuestbookEntry + + +class BadgerMultiplayerBadgeTest(BadgerTestCase): + + def setUp(self): + self.user_1 = self._get_user(username="user_1", + email="user_1@example.com", password="user_1_pass") + + def tearDown(self): + Nomination.objects.all().delete() + Award.objects.all().delete() + Badge.objects.all().delete() + + def test_nominate_badge(self): + """Can nominate a user for a badge""" + badge = self._get_badge() + nominator = self._get_user(username="nominator", + email="nominator@example.com", password="nomnom1") + nominee = self._get_user(username="nominee", + email="nominee@example.com", password="nomnom2") + + ok_(not badge.is_nominated_for(nominee)) + nomination = badge.nominate_for(nominator=nominator, nominee=nominee) + ok_(badge.is_nominated_for(nominee)) + + def test_approve_nomination(self): + """A nomination can be approved""" + nomination = self._create_nomination() + + ok_(not nomination.is_approved()) + nomination.approve_by(nomination.badge.creator) + ok_(nomination.is_approved()) + + def test_accept_nomination(self): + """A nomination can be accepted""" + nomination = self._create_nomination() + + ok_(not nomination.is_accepted()) + nomination.accept(nomination.nominee) + ok_(nomination.is_accepted()) + + def test_accept_nomination(self): + """A nomination that is approved and accepted results in an award""" + nomination = self._create_nomination() + + ok_(not nomination.badge.is_awarded_to(nomination.nominee)) + nomination.approve_by(nomination.badge.creator) + nomination.accept(nomination.nominee) + ok_(nomination.badge.is_awarded_to(nomination.nominee)) + + ct = Award.objects.filter(nomination=nomination).count() + eq_(1, ct, "There should be an award associated with the nomination") + + def test_disallowed_nomination_approval(self): + """By default, only badge creator should be allowed to approve a + nomination.""" + + nomination = self._create_nomination() + other_user = self._get_user(username="other") + + try: + nomination = nomination.approve_by(other_user) + ok_(False, "Nomination should not have succeeded") + except NominationApproveNotAllowedException, e: + ok_(True) + + def test_disallowed_nomination_accept(self): + """By default, only nominee should be allowed to accept a + nomination.""" + + nomination = self._create_nomination() + other_user = self._get_user(username="other") + + try: + nomination = nomination.accept(other_user) + ok_(False, "Nomination should not have succeeded") + except NominationAcceptNotAllowedException, e: + ok_(True) + + def _get_user(self, username="tester", email="tester@example.com", + password="trustno1"): + (user, created) = User.objects.get_or_create(username=username, + defaults=dict(email=email)) + if created: + user.set_password(password) + user.save() + return user + + def test_nomination_badge_already_awarded(self): + """New nomination for an awarded unique badge cannot be created""" + user = self._get_user() + b = Badge.objects.create(slug='one-and-only', title='One and Only', + unique=True, creator=user) + + n = b.nominate_for(user) + n.accept(user) + n.approve_by(user) + + try: + n = Nomination.objects.create(badge=b, nominee=user) + ok_(False, 'BadgeAlreadyAwardedException should have been raised') + except BadgeAlreadyAwardedException, e: + pass + + # Nominations stick around after award. + eq_(1, Nomination.objects.filter(badge=b, nominee=user).count()) + + def _get_badge(self, title="Test Badge", + description="This is a test badge", creator=None): + creator = creator or self.user_1 + (badge, created) = Badge.objects.get_or_create(title=title, + defaults=dict(description=description, creator=creator)) + return badge + + def _create_nomination(self, badge=None, nominator=None, nominee=None): + badge = badge or self._get_badge() + nominator = nominator or self._get_user(username="nominator", + email="nominator@example.com", password="nomnom1") + nominee = nominee or self._get_user(username="nominee", + email="nominee@example.com", password="nomnom2") + nomination = badge.nominate_for(nominator=nominator, nominee=nominee) + return nomination diff --git a/badger_multiplayer/tests/test_views.py b/badger_multiplayer/tests/test_views.py index 83cd3bb..f01e860 100644 --- a/badger_multiplayer/tests/test_views.py +++ b/badger_multiplayer/tests/test_views.py @@ -23,8 +23,10 @@ except ImportError, e: from . import BadgerTestCase -from badger.models import (Badge, Award, Nomination, - BadgeAwardNotAllowedException, +from badger.models import (Award, Progress, + BadgeAwardNotAllowedException) + +from badger_multiplayer.models import (Badge, Nomination, NominationApproveNotAllowedException, NominationAcceptNotAllowedException) @@ -40,7 +42,6 @@ class BadgerViewsTest(BadgerTestCase): Award.objects.all().delete() Badge.objects.all().delete() - @attr('current') def test_create(self): """Can create badge with form""" # Login should be required @@ -136,5 +137,3 @@ class BadgerViewsTest(BadgerTestCase): user.set_password(password) user.save() return user - - diff --git a/badger_multiplayer/views.py b/badger_multiplayer/views.py index 7041ccb..70039da 100644 --- a/badger_multiplayer/views.py +++ b/badger_multiplayer/views.py @@ -29,8 +29,10 @@ from django.views.decorators.http import (require_GET, require_POST, from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User -from badger.models import (Badge, Award, Nomination, - BadgeAwardNotAllowedException, +from badger.models import (Award, Progress, + BadgeAwardNotAllowedException) + +from badger_multiplayer.models import (Badge, Nomination, NominationApproveNotAllowedException, NominationAcceptNotAllowedException) @@ -65,7 +67,7 @@ def edit(request, slug): badge = get_object_or_404(Badge, slug=slug) if not badge.allows_edit_by(request.user): return HttpResponseForbidden() - + if request.method != "POST": form = BadgeEditForm(instance=badge) else: