├── .gitignore ├── .travis.yml ├── CHANGELOG ├── CODE_OF_CONDUCT.md ├── CONTRIBUTORS ├── LICENSE ├── MANIFEST.in ├── README.rst ├── TODO.md ├── badger ├── __init__.py ├── admin.py ├── feeds.py ├── fixtures │ └── default-badge.png ├── forms.py ├── helpers.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── rebake_awards.py │ │ └── update_badges.py ├── middleware.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── printing.py ├── signals.py ├── south_migrations │ ├── 0001_initial.py │ ├── 0002_auto__add_deferredaward__add_field_badge_nominations_accepted.py │ ├── 0003_auto__add_field_award_claim_code__chg_field_deferredaward_claim_code.py │ ├── 0004_auto__add_nomination.py │ ├── 0005_auto__add_field_award_description.py │ ├── 0006_auto__add_field_nomination_rejecter__add_field_nomination_rejection_re.py │ ├── 0007_auto__add_field_badge_nominations_autoapproved.py │ └── __init__.py ├── templatetags │ ├── __init__.py │ └── badger_tags.py ├── tests │ ├── __init__.py │ ├── test_badges_py.py │ ├── test_feeds.py │ ├── test_middleware.py │ ├── test_models.py │ └── test_views.py ├── urls.py ├── urls_simple.py ├── utils.py ├── validate_jsonp.py └── views.py ├── badger_example ├── __init__.py ├── badges.py ├── fixtures │ └── badger_example_badges.json ├── models.py ├── templates │ ├── badger │ │ ├── award_delete.html │ │ ├── award_detail.html │ │ ├── awards_by_badge.html │ │ ├── awards_by_user.html │ │ ├── awards_list.html │ │ ├── badge_award.html │ │ ├── badge_create.html │ │ ├── badge_delete.html │ │ ├── badge_detail.html │ │ ├── badge_edit.html │ │ ├── badge_nominate_for.html │ │ ├── badges_by_user.html │ │ ├── badges_list.html │ │ ├── base.html │ │ ├── claim_deferred_award.html │ │ ├── claims_list.html │ │ ├── deferred_award_body.txt │ │ ├── deferred_award_subject.txt │ │ ├── home.html │ │ ├── includes │ │ │ ├── award_as_badge.html │ │ │ ├── award_as_user.html │ │ │ ├── award_full.html │ │ │ ├── awards_as_badges_list.html │ │ │ ├── awards_list.html │ │ │ ├── badge_full.html │ │ │ ├── badges_list.html │ │ │ └── pagination.html │ │ ├── manage_claims.html │ │ ├── nomination_detail.html │ │ ├── notification │ │ │ ├── award_accepted │ │ │ │ ├── full.html │ │ │ │ ├── full.txt │ │ │ │ ├── notice.html │ │ │ │ └── short.txt │ │ │ ├── award_declined │ │ │ │ ├── full.html │ │ │ │ ├── full.txt │ │ │ │ ├── notice.html │ │ │ │ └── short.txt │ │ │ ├── award_received │ │ │ │ ├── full.html │ │ │ │ ├── full.txt │ │ │ │ ├── notice.html │ │ │ │ └── short.txt │ │ │ ├── badge_awarded │ │ │ │ ├── full.html │ │ │ │ ├── full.txt │ │ │ │ ├── notice.html │ │ │ │ └── short.txt │ │ │ ├── badge_edited │ │ │ │ ├── full.html │ │ │ │ ├── full.txt │ │ │ │ ├── notice.html │ │ │ │ └── short.txt │ │ │ ├── base.html │ │ │ ├── email_body.txt │ │ │ ├── email_subject.txt │ │ │ ├── full.html │ │ │ ├── full.txt │ │ │ ├── nomination_accepted │ │ │ │ ├── full.html │ │ │ │ ├── full.txt │ │ │ │ ├── notice.html │ │ │ │ └── short.txt │ │ │ ├── nomination_approved │ │ │ │ ├── full.html │ │ │ │ ├── full.txt │ │ │ │ ├── notice.html │ │ │ │ └── short.txt │ │ │ ├── nomination_declined │ │ │ │ ├── full.html │ │ │ │ ├── full.txt │ │ │ │ ├── notice.html │ │ │ │ └── short.txt │ │ │ ├── nomination_received │ │ │ │ ├── full.html │ │ │ │ ├── full.txt │ │ │ │ ├── notice.html │ │ │ │ └── short.txt │ │ │ ├── nomination_rejected │ │ │ │ ├── full.html │ │ │ │ ├── full.txt │ │ │ │ ├── notice.html │ │ │ │ └── short.txt │ │ │ ├── nomination_submitted │ │ │ │ ├── full.html │ │ │ │ ├── full.txt │ │ │ │ ├── notice.html │ │ │ │ └── short.txt │ │ │ ├── notice.html │ │ │ ├── notice_settings.html │ │ │ ├── notices.html │ │ │ ├── short.txt │ │ │ └── single.html │ │ └── staff_tools.html │ └── base.html └── urls.py ├── docs ├── Makefile ├── api.rst ├── changelog.rst ├── conf.py ├── contributing.rst ├── contributors.rst ├── faqs.rst ├── getting_started.rst ├── index.rst ├── overview.rst ├── usage.rst └── views.rst ├── manage.py ├── requirements ├── compiled.txt ├── dev.txt └── prod.txt ├── setup.py ├── test_settings.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw* 2 | *.pyc 3 | build/ 4 | dist/ 5 | *.egg-info 6 | uploads 7 | test-env 8 | test-venv 9 | docs/_build 10 | _static 11 | _templates 12 | .tox 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.6" 5 | - "2.7" 6 | 7 | env: 8 | - DB=mysql DJANGO_VERSION=1.4 9 | - DB=mysql DJANGO_VERSION=1.5 10 | - DB=mysql DJANGO_VERSION=1.6 11 | - DB=mysql DJANGO_VERSION=1.7 12 | - DB=mysql DJANGO_VERSION=1.8 13 | 14 | matrix: 15 | exclude: 16 | - python: "2.6" 17 | env: DB=mysql DJANGO_VERSION=1.8 18 | - python: "2.6" 19 | env: DB=mysql DJANGO_VERSION=1.7 20 | 21 | before_install: 22 | - sudo apt-get update -qq 23 | - sudo apt-get install build-essential python-dev python-pip python-virtualenv libxml2-dev libxslt-dev libjpeg8 libjpeg62-dev libfreetype6 libfreetype6-dev zlib1g-dev sqlite3 24 | - sudo ln -s /usr/lib/`uname -i`-linux-gnu/libfreetype.so /usr/lib 25 | - sudo ln -s /usr/lib/`uname -i`-linux-gnu/libz.so /usr/lib 26 | 27 | install: 28 | - pip install --use-mirrors "Django>=${DJANGO_VERSION},<${DJANGO_VERSION}.99" 29 | - pip install -r requirements/dev.txt --use-mirrors 30 | 31 | script: python manage.py test -v2 badger 32 | 33 | notifications: 34 | email: 35 | - me@lmorchard.com 36 | irc: 37 | channels: 38 | - "irc.mozilla.org#badger" 39 | on_success: always 40 | on_failure: always 41 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | What's new in django-badger 2 | =========================== 3 | 4 | .. contents:: 5 | :local: 6 | 7 | 8 | Version 0.0.1: In development 9 | ============================= 10 | 11 | **Changes** 12 | 13 | * Initial writing 14 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | 10 | 16 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Contributors 2 | ============ 3 | 4 | Les Orchard 5 | Cory Johns 6 | Phoebe B 7 | Victor Rajewski 8 | Patrick Taylor 9 | Ricky Rosario 10 | Will Kahn-Greene 11 | ToxicWar 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2013, Mozilla Foundation 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name of the copyright owner nor the names of its contributors 13 | may be used to endorse or promote products derived from this software 14 | without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include CHANGELOG 3 | include CONTRIBUTORS 4 | include MANIFEST.in 5 | include README.rst 6 | include .travis.yml 7 | include tox.ini 8 | include manage.py 9 | include runtests.py 10 | include setup.py 11 | include test_settings.py 12 | include TODO.md 13 | 14 | recursive-include docs *.py *.rst Makefile 15 | 16 | recursive-include badger *.py *.png *.html *.json 17 | 18 | recursive-include badger_example *.html *.json *.txt 19 | 20 | recursive-include requirements *.txt 21 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-badger is a reusable Django app that supports badges, to track and 2 | award achievements by your users. This can be used to help encourage certain 3 | behaviors, recognize skills, or just generally celebrate members of your 4 | community. 5 | 6 | - `Build status on travis-ci `_ (|build-status|) 7 | - `Latest documentation on Read The Docs `_ 8 | (`source `_) 9 | - `Task board on Huboard `_ 10 | (`source `_) 11 | 12 | .. |build-status| image:: https://secure.travis-ci.org/mozilla/django-badger.png?branch=master 13 | :target: http://travis-ci.org/mozilla/django-badger 14 | 15 | .. vim:set tw=78 ai fo+=n fo-=l ft=rst: 16 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ## TODO 2 | 3 | See: https://github.com/mozilla/django-badger/issues 4 | -------------------------------------------------------------------------------- /badger/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.conf import settings as django_settings 4 | from django.utils.importlib import import_module 5 | from django.utils.module_loading import module_has_submodule 6 | 7 | from badger.models import Badge, Award, Progress 8 | 9 | 10 | # Default settings follow, overridden with BADGER_ prefix in settings.py 11 | TEMPLATE_BASE = 'badger' 12 | 13 | # Skip baking for now (Issue #139) 14 | BAKE_AWARD_IMAGES = False 15 | 16 | # Master switch for wide-open badge creation by all users (multiplayer mode) 17 | ALLOW_ADD_BY_ANYONE = False 18 | 19 | # Page size for badge listings 20 | BADGE_PAGE_SIZE = 50 21 | 22 | # Max # of items shown on home page recent sections 23 | MAX_RECENT = 15 24 | 25 | 26 | class BadgerSettings(object): 27 | """Dirty settings interface that allows defaults from here to be overidden 28 | by app settings 29 | """ 30 | def __getattr__(self, name): 31 | override_name = 'BADGER_%s' % name 32 | if hasattr(django_settings, override_name): 33 | return getattr(django_settings, override_name) 34 | else: 35 | return globals()[name] 36 | 37 | 38 | settings = BadgerSettings() 39 | 40 | 41 | def autodiscover(): 42 | """ 43 | Auto-discover INSTALLED_APPS badges.py modules and fail silently when 44 | not present. 45 | """ 46 | from django.utils.importlib import import_module 47 | for app in django_settings.INSTALLED_APPS: 48 | mod = import_module(app) 49 | try: 50 | badges_mod = import_module('%s.badges' % app) 51 | if hasattr(badges_mod, 'register_signals'): 52 | badges_mod.register_signals() 53 | except ImportError: 54 | if module_has_submodule(mod, 'badges'): 55 | raise 56 | -------------------------------------------------------------------------------- /badger/admin.py: -------------------------------------------------------------------------------- 1 | from urlparse import urljoin 2 | 3 | from django.conf import settings 4 | from django.contrib import admin 5 | 6 | from django import forms 7 | from django.db import models 8 | 9 | try: 10 | from funfactory.urlresolvers import reverse 11 | except ImportError: 12 | from django.core.urlresolvers import reverse 13 | 14 | from .models import (Badge, Award, Nomination, Progress, DeferredAward) 15 | 16 | 17 | UPLOADS_URL = getattr(settings, 'BADGER_MEDIA_URL', 18 | urljoin(getattr(settings, 'MEDIA_URL', '/media/'), 'uploads/')) 19 | 20 | 21 | def show_unicode(obj): 22 | return unicode(obj) 23 | show_unicode.short_description = "Display" 24 | 25 | 26 | def show_image(obj): 27 | if not obj.image: 28 | return 'None' 29 | img_url = "%s%s" % (UPLOADS_URL, obj.image) 30 | return ('' % 31 | (img_url, img_url)) 32 | 33 | show_image.allow_tags = True 34 | show_image.short_description = "Image" 35 | 36 | 37 | def build_related_link(self, model_name, name_single, name_plural, qs): 38 | link = '%s?%s' % ( 39 | reverse('admin:badger_%s_changelist' % model_name, args=[]), 40 | 'badge__exact=%s' % (self.id) 41 | ) 42 | new_link = '%s?%s' % ( 43 | reverse('admin:badger_%s_add' % model_name, args=[]), 44 | 'badge=%s' % (self.id) 45 | ) 46 | count = qs.count() 47 | what = (count == 1) and name_single or name_plural 48 | return ('%s %s (new)' % 49 | (link, count, what, new_link)) 50 | 51 | 52 | def related_deferredawards_link(self): 53 | return build_related_link(self, 'deferredaward', 'deferred', 'deferred', 54 | self.deferredaward_set) 55 | 56 | related_deferredawards_link.allow_tags = True 57 | related_deferredawards_link.short_description = "Deferred Awards" 58 | 59 | 60 | def related_awards_link(self): 61 | return build_related_link(self, 'award', 'award', 'awards', 62 | self.award_set) 63 | 64 | related_awards_link.allow_tags = True 65 | related_awards_link.short_description = "Awards" 66 | 67 | 68 | class BadgeAdmin(admin.ModelAdmin): 69 | list_display = ("id", "title", show_image, "slug", "unique", "creator", 70 | related_awards_link, related_deferredawards_link, "created",) 71 | list_display_links = ('id', 'title',) 72 | search_fields = ("title", "slug", "image", "description",) 73 | filter_horizontal = ('prerequisites', ) 74 | prepopulated_fields = {"slug": ("title",)} 75 | formfield_overrides = { 76 | models.ManyToManyField: { 77 | "widget": forms.widgets.SelectMultiple(attrs={"size": 25}) 78 | } 79 | } 80 | # This prevents Badge from loading all the users on the site 81 | # which could be a very large number, take forever and result 82 | # in a huge page. 83 | raw_id_fields = ("creator",) 84 | 85 | 86 | def badge_link(self): 87 | url = reverse('admin:badger_badge_change', args=[self.badge.id]) 88 | return '%s' % (url, self.badge) 89 | 90 | badge_link.allow_tags = True 91 | badge_link.short_description = 'Badge' 92 | 93 | 94 | class AwardAdmin(admin.ModelAdmin): 95 | list_display = (show_unicode, badge_link, show_image, 'claim_code', 'user', 96 | 'creator', 'created', ) 97 | fields = ('badge', 'description', 'claim_code', 'user', 'creator', ) 98 | search_fields = ("badge__title", "badge__slug", "badge__description", 99 | "description") 100 | raw_id_fields = ('user', 'creator',) 101 | 102 | 103 | class ProgressAdmin(admin.ModelAdmin): 104 | raw_id_fields = ('user',) 105 | 106 | 107 | def claim_code_link(self): 108 | return '%s' % (self.get_claim_url(), self.claim_code) 109 | 110 | claim_code_link.allow_tags = True 111 | claim_code_link.short_description = "Claim Code" 112 | 113 | 114 | class DeferredAwardAdmin(admin.ModelAdmin): 115 | list_display = ('id', claim_code_link, 'claim_group', badge_link, 'email', 116 | 'reusable', 'creator', 'created', 'modified',) 117 | list_display_links = ('id',) 118 | list_filter = ('reusable', ) 119 | fields = ('badge', 'claim_group', 'claim_code', 'email', 'reusable', 120 | 'description',) 121 | readonly_fields = ('created', 'modified') 122 | search_fields = ("badge__title", "badge__slug", "badge__description",) 123 | raw_id_fields = ('creator',) 124 | 125 | 126 | def award_link(self): 127 | url = reverse('admin:badger_award_change', args=[self.award.id]) 128 | return '%s' % (url, self.award) 129 | 130 | award_link.allow_tags = True 131 | award_link.short_description = 'award' 132 | 133 | 134 | class NominationAdmin(admin.ModelAdmin): 135 | list_display = ('id', show_unicode, award_link, 'accepted', 'nominee', 136 | 'approver', 'creator', 'created', 'modified',) 137 | list_filter = ('accepted',) 138 | search_fields = ('badge__title', 'badge__slug', 'badge__description',) 139 | raw_id_fields = ('nominee', 'creator', 'approver', 'rejected_by',) 140 | 141 | 142 | for x in ((Badge, BadgeAdmin), 143 | (Award, AwardAdmin), 144 | (Nomination, NominationAdmin), 145 | (Progress, ProgressAdmin), 146 | (DeferredAward, DeferredAwardAdmin),): 147 | admin.site.register(*x) 148 | -------------------------------------------------------------------------------- /badger/feeds.py: -------------------------------------------------------------------------------- 1 | """Feeds for badge""" 2 | import datetime 3 | import hashlib 4 | import json 5 | import urllib 6 | 7 | from django.contrib.syndication.views import Feed, FeedDoesNotExist 8 | from django.utils.feedgenerator import (SyndicationFeed, Rss201rev2Feed, 9 | Atom1Feed, get_tag_uri) 10 | from django.shortcuts import get_object_or_404 11 | 12 | from django.contrib.auth.models import User 13 | from django.conf import settings 14 | 15 | try: 16 | from tower import ugettext_lazy as _ 17 | except ImportError: 18 | from django.utils.translation import ugettext_lazy as _ 19 | 20 | try: 21 | from commons.urlresolvers import reverse 22 | except ImportError: 23 | from django.core.urlresolvers import reverse 24 | 25 | from . import validate_jsonp 26 | from .models import (Badge, Award, Nomination, Progress, 27 | BadgeAwardNotAllowedException, 28 | DEFAULT_BADGE_IMAGE) 29 | 30 | 31 | MAX_FEED_ITEMS = getattr(settings, 'BADGER_MAX_FEED_ITEMS', 15) 32 | 33 | 34 | class BaseJSONFeedGenerator(SyndicationFeed): 35 | """JSON feed generator""" 36 | # TODO:liberate - Can this class be a generally-useful lib? 37 | 38 | mime_type = 'application/json' 39 | 40 | def _encode_complex(self, obj): 41 | if isinstance(obj, datetime.datetime): 42 | return obj.isoformat() 43 | 44 | def build_item(self, item): 45 | """Simple base item formatter. 46 | Omit some named keys and any keys with false-y values""" 47 | omit_keys = ('obj', 'unique_id', ) 48 | return dict((k, v) for k, v in item.items() 49 | if v and k not in omit_keys) 50 | 51 | def build_feed(self): 52 | """Simple base feed formatter. 53 | Omit some named keys and any keys with false-y values""" 54 | omit_keys = ('obj', 'request', 'id', ) 55 | feed_data = dict((k, v) for k, v in self.feed.items() 56 | if v and k not in omit_keys) 57 | feed_data['items'] = [self.build_item(item) for item in self.items] 58 | return feed_data 59 | 60 | def write(self, outfile, encoding): 61 | request = self.feed['request'] 62 | 63 | # Check for a callback param, validate it before use 64 | callback = request.GET.get('callback', None) 65 | if callback is not None: 66 | if not validate_jsonp.is_valid_jsonp_callback_value(callback): 67 | callback = None 68 | 69 | # Build the JSON string, wrapping it in a callback param if necessary. 70 | json_string = json.dumps(self.build_feed(), 71 | default=self._encode_complex) 72 | if callback: 73 | outfile.write('%s(%s)' % (callback, json_string)) 74 | else: 75 | outfile.write(json_string) 76 | 77 | 78 | class BaseFeed(Feed): 79 | """Base feed for all of badger, allows switchable generator from URL route 80 | and other niceties""" 81 | # TODO:liberate - Can this class be a generally-useful lib? 82 | 83 | json_feed_generator = BaseJSONFeedGenerator 84 | rss_feed_generator = Rss201rev2Feed 85 | atom_feed_generator = Atom1Feed 86 | 87 | def __call__(self, request, *args, **kwargs): 88 | self.request = request 89 | return super(BaseFeed, self).__call__(request, *args, **kwargs) 90 | 91 | def get_object(self, request, format): 92 | self.link = request.build_absolute_uri('/') 93 | if format == 'json': 94 | self.feed_type = self.json_feed_generator 95 | elif format == 'rss': 96 | self.feed_type = self.rss_feed_generator 97 | else: 98 | self.feed_type = self.atom_feed_generator 99 | return super(BaseFeed, self).get_object(request) 100 | 101 | def feed_extra_kwargs(self, obj): 102 | return {'request': self.request, 'obj': obj, } 103 | 104 | def item_extra_kwargs(self, obj): 105 | return {'obj': obj, } 106 | 107 | def item_pubdate(self, obj): 108 | return obj.created 109 | 110 | def item_author_link(self, obj): 111 | if not obj.creator or not hasattr(obj.creator, 'get_absolute_url'): 112 | return None 113 | else: 114 | return self.request.build_absolute_uri( 115 | obj.creator.get_absolute_url()) 116 | 117 | def item_author_name(self, obj): 118 | if not obj.creator: 119 | return None 120 | else: 121 | return '%s' % obj.creator 122 | 123 | def item_description(self, obj): 124 | if obj.image: 125 | image_url = obj.image.url 126 | else: 127 | image_url = '%simg/default-badge.png' % settings.MEDIA_URL 128 | return """ 129 |
130 | %(alt)s 131 |
132 | """ % dict( 133 | alt=unicode(obj), 134 | href=self.request.build_absolute_uri(obj.get_absolute_url()), 135 | image_url=self.request.build_absolute_uri(image_url) 136 | ) 137 | 138 | 139 | class AwardActivityStreamJSONFeedGenerator(BaseJSONFeedGenerator): 140 | pass 141 | 142 | 143 | class AwardActivityStreamAtomFeedGenerator(Atom1Feed): 144 | pass 145 | 146 | 147 | class AwardsFeed(BaseFeed): 148 | """Base class for all feeds listing awards""" 149 | title = _(u'Recently awarded badges') 150 | subtitle = None 151 | 152 | json_feed_generator = AwardActivityStreamJSONFeedGenerator 153 | atom_feed_generator = AwardActivityStreamAtomFeedGenerator 154 | 155 | def item_title(self, obj): 156 | return _(u'{badgetitle} awarded to {username}').format( 157 | badgetitle=obj.badge.title, username=obj.user.username) 158 | 159 | def item_author_link(self, obj): 160 | if not obj.creator: 161 | return None 162 | else: 163 | return self.request.build_absolute_uri( 164 | reverse('badger.views.awards_by_user', 165 | args=(obj.creator.username,))) 166 | 167 | def item_link(self, obj): 168 | return self.request.build_absolute_uri( 169 | reverse('badger.views.award_detail', 170 | args=(obj.badge.slug, obj.pk, ))) 171 | 172 | 173 | class AwardsRecentFeed(AwardsFeed): 174 | """Feed of all recent badge awards""" 175 | 176 | def items(self): 177 | return (Award.objects 178 | .order_by('-created') 179 | .all()[:MAX_FEED_ITEMS]) 180 | 181 | 182 | class AwardsByUserFeed(AwardsFeed): 183 | """Feed of recent badge awards for a user""" 184 | 185 | def get_object(self, request, format, username): 186 | super(AwardsByUserFeed, self).get_object(request, format) 187 | user = get_object_or_404(User, username=username) 188 | self.title = _(u'Badges recently awarded to {username}').format( 189 | username=user.username) 190 | self.link = request.build_absolute_uri( 191 | reverse('badger.views.awards_by_user', args=(user.username,))) 192 | return user 193 | 194 | def items(self, user): 195 | return (Award.objects 196 | .filter(user=user) 197 | .order_by('-created') 198 | .all()[:MAX_FEED_ITEMS]) 199 | 200 | 201 | class AwardsByBadgeFeed(AwardsFeed): 202 | """Feed of recent badge awards for a badge""" 203 | 204 | def get_object(self, request, format, slug): 205 | super(AwardsByBadgeFeed, self).get_object(request, format) 206 | badge = get_object_or_404(Badge, slug=slug) 207 | self.title = _(u'Recent awards of "{badgetitle}"').format( 208 | badgetitle=badge.title) 209 | self.link = request.build_absolute_uri( 210 | reverse('badger.views.awards_by_badge', args=(badge.slug,))) 211 | return badge 212 | 213 | def items(self, badge): 214 | return (Award.objects 215 | .filter(badge=badge).order_by('-created') 216 | .all()[:MAX_FEED_ITEMS]) 217 | 218 | 219 | class BadgesJSONFeedGenerator(BaseJSONFeedGenerator): 220 | pass 221 | 222 | 223 | class BadgesFeed(BaseFeed): 224 | """Base class for all feeds listing badges""" 225 | title = _(u'Recently created badges') 226 | 227 | json_feed_generator = BadgesJSONFeedGenerator 228 | 229 | def item_title(self, obj): 230 | return obj.title 231 | 232 | def item_link(self, obj): 233 | return self.request.build_absolute_uri( 234 | reverse('badger.views.detail', 235 | args=(obj.slug, ))) 236 | 237 | 238 | class BadgesRecentFeed(BadgesFeed): 239 | 240 | def items(self): 241 | return (Badge.objects 242 | .order_by('-created') 243 | .all()[:MAX_FEED_ITEMS]) 244 | 245 | 246 | class BadgesByUserFeed(BadgesFeed): 247 | """Feed of badges recently created by a user""" 248 | 249 | def get_object(self, request, format, username): 250 | super(BadgesByUserFeed, self).get_object(request, format) 251 | user = get_object_or_404(User, username=username) 252 | self.title = _(u'Badges recently created by {username}').format( 253 | username=user.username) 254 | self.link = request.build_absolute_uri( 255 | reverse('badger.views.badges_by_user', args=(user.username,))) 256 | return user 257 | 258 | def items(self, user): 259 | return (Badge.objects 260 | .filter(creator=user) 261 | .order_by('-created') 262 | .all()[:MAX_FEED_ITEMS]) 263 | -------------------------------------------------------------------------------- /badger/fixtures/default-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/django-badger/07c28a8a1065ea7716f9988bf7541242a912fb1d/badger/fixtures/default-badge.png -------------------------------------------------------------------------------- /badger/forms.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | 4 | from django.conf import settings 5 | 6 | from django import forms 7 | from django.db import models 8 | from django.contrib.auth.models import User, AnonymousUser 9 | from django.forms import FileField, CharField, Textarea, ValidationError 10 | from django.core.validators import validate_email 11 | 12 | try: 13 | from tower import ugettext_lazy as _ 14 | except ImportError: 15 | from django.utils.translation import ugettext_lazy as _ 16 | 17 | from badger.models import Award, Badge, Nomination 18 | 19 | try: 20 | from taggit.managers import TaggableManager 21 | except ImportError: 22 | TaggableManager = None 23 | 24 | 25 | EMAIL_SEPARATOR_RE = re.compile(r'[,;\s]+') 26 | 27 | 28 | class MyModelForm(forms.ModelForm): 29 | 30 | required_css_class = "required" 31 | error_css_class = "error" 32 | 33 | def as_ul(self): 34 | """Returns this form rendered as HTML
  • s -- excluding the
      . 35 | """ 36 | # TODO: l10n: This doesn't work for rtl languages 37 | return self._html_output( 38 | normal_row=(u'%(label)s %(field)s' 39 | '%(help_text)s%(errors)s
    • '), 40 | error_row=u'
    • %s
    • ', 41 | row_ender='', 42 | help_text_html=u'

      %s

      ', 43 | errors_on_separate_row=False) 44 | 45 | 46 | class MyForm(forms.Form): 47 | 48 | required_css_class = "required" 49 | error_css_class = "error" 50 | 51 | def as_ul(self): 52 | """Returns this form rendered as HTML
    • s -- excluding the
        . 53 | """ 54 | # TODO: l10n: This doesn't work for rtl languages 55 | return self._html_output( 56 | normal_row=(u'%(label)s %(field)s' 57 | '%(help_text)s%(errors)s
      • '), 58 | error_row=u'
      • %s
      • ', 59 | row_ender='', 60 | help_text_html=u'

        %s

        ', 61 | errors_on_separate_row=False) 62 | 63 | 64 | class MultipleItemsField(forms.Field): 65 | """Form field which accepts multiple text items""" 66 | # Based on https://docs.djangoproject.com/en/dev/ref/forms/validation/ 67 | # #form-field-default-cleaning 68 | widget = Textarea 69 | 70 | def __init__(self, **kwargs): 71 | self.max_items = kwargs.get('max_items', 10) 72 | if 'max_items' in kwargs: 73 | del kwargs['max_items'] 74 | self.separator_re = re.compile(r'[,;\s]+') 75 | if 'separator_re' in kwargs: 76 | del kwargs['separator_re'] 77 | super(MultipleItemsField, self).__init__(**kwargs) 78 | 79 | def to_python(self, value): 80 | """Normalize data to a list of strings.""" 81 | if not value: 82 | return [] 83 | items = self.separator_re.split(value) 84 | return [i.strip() for i in items if i.strip()] 85 | 86 | def validate_item(self, item): 87 | return True 88 | 89 | def validate(self, value): 90 | """Check if value consists only of valid items.""" 91 | super(MultipleItemsField, self).validate(value) 92 | 93 | # Enforce max number of items 94 | if len(value) > self.max_items: 95 | raise ValidationError( 96 | _(u'{num} items entered, only {maxnum} allowed').format( 97 | num=len(value), maxnum=self.max_items)) 98 | 99 | # Validate each of the items 100 | invalid_items = [] 101 | for item in value: 102 | try: 103 | self.validate_item(item) 104 | except ValidationError: 105 | invalid_items.append(item) 106 | 107 | if len(invalid_items) > 0: 108 | # TODO: l10n: Not all languages separate with commas 109 | raise ValidationError( 110 | _(u'These items were invalid: {itemlist}').format( 111 | itemlist=u', '.join(invalid_items))) 112 | 113 | 114 | class MultiEmailField(MultipleItemsField): 115 | """Form field which accepts multiple email addresses""" 116 | def validate_item(self, item): 117 | validate_email(item) 118 | 119 | 120 | class BadgeAwardForm(MyForm): 121 | """Form to create either a real or deferred badge award""" 122 | # TODO: Needs a captcha? 123 | emails = MultiEmailField(max_items=10, 124 | help_text=_(u'Enter up to 10 email addresses for badge award ' 125 | 'recipients')) 126 | description = CharField( 127 | label='Explanation', 128 | widget=Textarea, required=False, 129 | help_text=_(u'Explain why this badge should be awarded')) 130 | 131 | 132 | class DeferredAwardGrantForm(MyForm): 133 | """Form to grant a deferred badge award""" 134 | # TODO: Needs a captcha? 135 | email = forms.EmailField() 136 | 137 | 138 | class MultipleClaimCodesField(MultipleItemsField): 139 | """Form field which accepts multiple DeferredAward claim codes""" 140 | def validate_item(self, item): 141 | from badger.models import DeferredAward 142 | try: 143 | DeferredAward.objects.get(claim_code=item) 144 | return True 145 | except DeferredAward.DoesNotExist: 146 | raise ValidationError(_(u'No such claim code, {claimcode}').format( 147 | claimcode=item)) 148 | 149 | 150 | class DeferredAwardMultipleGrantForm(MyForm): 151 | email = forms.EmailField( 152 | help_text=_(u'Email address to which claims should be granted')) 153 | claim_codes = MultipleClaimCodesField( 154 | help_text=_(u'Comma- or space-separated list of badge claim codes')) 155 | 156 | 157 | class BadgeEditForm(MyModelForm): 158 | 159 | class Meta: 160 | model = Badge 161 | fields = ('title', 'image', 'description',) 162 | try: 163 | # HACK: Add "tags" as a field only if the taggit app is available. 164 | import taggit 165 | fields += ('tags',) 166 | except ImportError: 167 | pass 168 | fields += ('unique', 'nominations_accepted', 169 | 'nominations_autoapproved',) 170 | 171 | required_css_class = "required" 172 | error_css_class = "error" 173 | 174 | def __init__(self, *args, **kwargs): 175 | super(BadgeEditForm, self).__init__(*args, **kwargs) 176 | 177 | # TODO: l10n: Pretty sure this doesn't work for rtl languages. 178 | # HACK: inject new templates into the image field, monkeypatched 179 | # without creating a subclass 180 | self.fields['image'].widget.template_with_clear = u''' 181 |

        %(clear)s 182 |

        183 | ''' 184 | # TODO: l10n: Pretty sure this doesn't work for rtl languages. 185 | self.fields['image'].widget.template_with_initial = u''' 186 |
        187 |

        %(initial_text)s: %(initial)s

        188 | %(clear_template)s 189 |

        %(input_text)s: %(input)s

        190 |
        191 | ''' 192 | 193 | 194 | class BadgeNewForm(BadgeEditForm): 195 | 196 | class Meta(BadgeEditForm.Meta): 197 | pass 198 | 199 | def __init__(self, *args, **kwargs): 200 | super(BadgeNewForm, self).__init__(*args, **kwargs) 201 | 202 | 203 | class BadgeSubmitNominationForm(MyForm): 204 | """Form to submit badge nominations""" 205 | emails = MultiEmailField(max_items=10, 206 | help_text=_( 207 | u'Enter up to 10 email addresses for badge award nominees')) 208 | -------------------------------------------------------------------------------- /badger/helpers.py: -------------------------------------------------------------------------------- 1 | import django 2 | import hashlib 3 | import urllib 4 | import urlparse 5 | 6 | from django.conf import settings 7 | 8 | from django.core.exceptions import ObjectDoesNotExist 9 | from django.utils.html import conditional_escape 10 | 11 | try: 12 | from commons.urlresolvers import reverse 13 | except ImportError: 14 | from django.core.urlresolvers import reverse 15 | 16 | import jingo 17 | import jinja2 18 | from jinja2 import evalcontextfilter, Markup, escape 19 | from jingo import register 20 | 21 | from .models import (Badge, Award, Nomination, Progress, 22 | BadgeAwardNotAllowedException) 23 | 24 | if django.VERSION < (1, 7, 0): 25 | from django.contrib.auth.models import SiteProfileNotAvailable 26 | 27 | if django.VERSION >= (1, 7, 0): 28 | class SiteProfileNotAvailable(Exception): 29 | pass 30 | 31 | 32 | @register.function 33 | def user_avatar(user, secure=False, size=256, rating='pg', default=''): 34 | try: 35 | profile = user.get_profile() 36 | if profile.avatar: 37 | return profile.avatar.url 38 | except AttributeError: 39 | pass 40 | except SiteProfileNotAvailable: 41 | pass 42 | except ObjectDoesNotExist: 43 | pass 44 | 45 | base_url = (secure and 'https://secure.gravatar.com' or 46 | 'http://www.gravatar.com') 47 | m = hashlib.md5(user.email) 48 | return '%(base_url)s/avatar/%(hash)s?%(params)s' % dict( 49 | base_url=base_url, hash=m.hexdigest(), 50 | params=urllib.urlencode(dict( 51 | s=size, d=default, r=rating 52 | )) 53 | ) 54 | 55 | 56 | @register.function 57 | def user_awards(user): 58 | return Award.objects.filter(user=user) 59 | 60 | 61 | @register.function 62 | def user_badges(user): 63 | return Badge.objects.filter(creator=user) 64 | 65 | 66 | @register.function 67 | def badger_allows_add_by(user): 68 | return Badge.objects.allows_add_by(user) 69 | 70 | 71 | @register.function 72 | def qr_code_image(value, alt=None, size=150): 73 | # TODO: Bake our own QR codes, someday soon! 74 | url = conditional_escape("http://chart.apis.google.com/chart?%s" % \ 75 | urllib.urlencode({'chs': '%sx%s' % (size, size), 'cht': 'qr', 'chl': value, 'choe': 'UTF-8'})) 76 | alt = conditional_escape(alt or value) 77 | 78 | return Markup(u"""%s""" % 79 | (url, size, size, alt)) 80 | 81 | 82 | @register.function 83 | def nominations_pending_approval(user): 84 | return Nomination.objects.filter(badge__creator=user, 85 | approver__isnull=True) 86 | 87 | 88 | @register.function 89 | def nominations_pending_acceptance(user): 90 | return Nomination.objects.filter(nominee=user, 91 | approver__isnull=False, 92 | accepted=False) 93 | -------------------------------------------------------------------------------- /badger/management/__init__.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname, basename 2 | 3 | from django.conf import settings 4 | from django.core.management import call_command 5 | from django.core.management.base import BaseCommand, CommandError 6 | from django.db.models import get_apps, get_models, signals 7 | from django.utils.importlib import import_module 8 | from django.utils.module_loading import module_has_submodule 9 | 10 | import badger 11 | import badger.utils 12 | 13 | 14 | if "notification" in settings.INSTALLED_APPS: 15 | from notification import models as notification 16 | from django.utils.translation import ugettext_noop as _ 17 | 18 | def create_notice_types(app, created_models, verbosity, **kwargs): 19 | notices = ( 20 | ("badge_edited", _(u"Badge edited"), 21 | _(u"one of your badges has been edited")), 22 | ("badge_awarded", _(u"Badge awarded"), 23 | _(u"one of your badges has been awarded to someone")), 24 | ("award_received", _(u"Award received"), 25 | _(u"you have been awarded a badge")), 26 | #("award_accepted", _(u"Badge award accepted"), 27 | # _(u"someone has accepted an award for one of your badges")), 28 | #("award_declined", _(u"Badge award declined"), 29 | # _(u"someone has declined an award for one of your badges")), 30 | # TODO: Notification on progress? 31 | ("nomination_submitted", _(u"Nomination submitted"), 32 | _(u"someone has submitted a nomination for one of your badges")), 33 | ("nomination_approved", _(u"Nomination approved"), 34 | _(u"a nomination you submitted for an award has been approved")), 35 | ("nomination_rejected", _(u"Nomination rejected"), 36 | _(u"a nomination you submitted for an award has been rejected")), 37 | ("nomination_received", _(u"Nomination received"), 38 | _(u"a nomination to award you a badge was approved")), 39 | ("nomination_accepted", _(u"Nomination accepted"), 40 | _(u"a nomination you submitted for an award has been accepted")), 41 | ) 42 | for notice in notices: 43 | notification.create_notice_type(*notice) 44 | 45 | signals.post_syncdb.connect(create_notice_types, sender=notification) 46 | 47 | 48 | def update_badges(overwrite=False): 49 | from django.utils.importlib import import_module 50 | 51 | for app in settings.INSTALLED_APPS: 52 | mod = import_module(app) 53 | try: 54 | badges_mod = import_module('%s.badges' % app) 55 | fixture_label = '%s_badges' % app.replace('.','_') 56 | call_command('loaddata', fixture_label, verbosity=1) 57 | if hasattr(badges_mod, 'badges'): 58 | badger.utils.update_badges(badges_mod.badges, overwrite) 59 | if hasattr(badges_mod, 'update_badges'): 60 | badges_mod.update_badges(overwrite) 61 | except ImportError: 62 | if module_has_submodule(mod, 'badges'): 63 | raise 64 | 65 | 66 | signals.post_syncdb.connect(lambda *args, **kwargs: update_badges(), 67 | sender=badger.models) 68 | -------------------------------------------------------------------------------- /badger/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/django-badger/07c28a8a1065ea7716f9988bf7541242a912fb1d/badger/management/commands/__init__.py -------------------------------------------------------------------------------- /badger/management/commands/rebake_awards.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname, basename 2 | 3 | from django.conf import settings 4 | from django.core.management import call_command 5 | from django.core.management.base import BaseCommand, CommandError 6 | from django.db.models import get_apps, get_models, signals 7 | from django.utils.importlib import import_module 8 | 9 | from badger.models import Badge, Award, Progress 10 | from badger.management import update_badges 11 | 12 | 13 | class Command(BaseCommand): 14 | args = '' 15 | help = 'Rebake award images' 16 | 17 | def handle(self, *args, **options): 18 | for award in Award.objects.all(): 19 | award.save() 20 | -------------------------------------------------------------------------------- /badger/management/commands/update_badges.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname, basename 2 | 3 | from django.conf import settings 4 | from django.core.management import call_command 5 | from django.core.management.base import BaseCommand, CommandError 6 | from django.db.models import get_apps, get_models, signals 7 | from django.utils.importlib import import_module 8 | 9 | from badger.models import Badge, Award, Progress 10 | from badger.management import update_badges 11 | 12 | 13 | class Command(BaseCommand): 14 | args = '' 15 | help = 'Update badges from apps' 16 | 17 | def handle(self, *args, **options): 18 | # TODO: overwrite needs to be a command option 19 | update_badges(overwrite=True) 20 | -------------------------------------------------------------------------------- /badger/middleware.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from datetime import datetime 4 | from django.conf import settings 5 | 6 | from .models import (Badge, Award) 7 | 8 | 9 | LAST_CHECK_COOKIE_NAME = getattr(settings, 10 | 'BADGER_LAST_CHECK_COOKIE_NAME', 'badgerLastAwardCheck') 11 | 12 | 13 | class RecentBadgeAwardsList(object): 14 | """Very lazy accessor for recent awards.""" 15 | 16 | def __init__(self, request): 17 | self.request = request 18 | self.was_used = False 19 | self._queryset = None 20 | 21 | # Try to fetch and parse the timestamp of the last award check, fall 22 | # back to None 23 | try: 24 | self.last_check = datetime.fromtimestamp(float( 25 | self.request.COOKIES[LAST_CHECK_COOKIE_NAME])) 26 | except (KeyError, ValueError): 27 | self.last_check = None 28 | 29 | def process_response(self, response): 30 | if (self.request.user.is_authenticated() and 31 | (not self.last_check or self.was_used)): 32 | response.set_cookie(LAST_CHECK_COOKIE_NAME, time.time()) 33 | return response 34 | 35 | def get_queryset(self, last_check=None): 36 | if not last_check: 37 | last_check = self.last_check 38 | 39 | if not (last_check and self.request.user.is_authenticated()): 40 | # No queryset for anonymous users or missing last check timestamp 41 | return None 42 | 43 | if not self._queryset: 44 | self.was_used = True 45 | self._queryset = (Award.objects 46 | .filter(user=self.request.user, 47 | created__gt=last_check) 48 | .exclude(hidden=True)) 49 | 50 | return self._queryset 51 | 52 | def __iter__(self): 53 | qs = self.get_queryset() 54 | if qs is None: 55 | return [] 56 | return qs.iterator() 57 | 58 | def __len__(self): 59 | qs = self.get_queryset() 60 | if qs is None: 61 | return 0 62 | return len(qs) 63 | 64 | 65 | class RecentBadgeAwardsMiddleware(object): 66 | """Middleware that adds ``recent_badge_awards`` to request 67 | 68 | This property is lazy-loading, so if you don't use it, then it 69 | shouldn't have much effect on runtime. 70 | 71 | To use, add this to your ``MIDDLEWARE_CLASSES`` in ``settings.py``:: 72 | 73 | MIDDLEWARE_CLASSES = ( 74 | ... 75 | 'badger.middleware.RecentBadgeAwardsMiddleware', 76 | ... 77 | ) 78 | 79 | 80 | Then in your view code:: 81 | 82 | def awesome_view(request): 83 | for award in request.recent_badge_awards: 84 | do_something_awesome(award) 85 | 86 | """ 87 | 88 | def process_request(self, request): 89 | request.recent_badge_awards = RecentBadgeAwardsList(request) 90 | return None 91 | 92 | def process_response(self, request, response): 93 | if not hasattr(request, 'recent_badge_awards'): 94 | return response 95 | return request.recent_badge_awards.process_response(response) 96 | -------------------------------------------------------------------------------- /badger/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import badger.models 6 | from django.conf import settings 7 | import django.core.files.storage 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Award', 19 | fields=[ 20 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 21 | ('description', models.TextField(help_text=b'Explanation and evidence for the badge award', blank=True)), 22 | ('image', models.ImageField(storage=django.core.files.storage.FileSystemStorage(base_url=b'uploads/', location=b'uploads'), null=True, upload_to=badger.models.UploadTo(b'image', b'png'), blank=True)), 23 | ('claim_code', models.CharField(default=b'', help_text=b'Code used to claim this award', max_length=32, db_index=True, blank=True)), 24 | ('hidden', models.BooleanField(default=False)), 25 | ('created', models.DateTimeField(auto_now_add=True)), 26 | ('modified', models.DateTimeField(auto_now=True)), 27 | ], 28 | options={ 29 | 'ordering': ['-modified', '-created'], 30 | }, 31 | bases=(models.Model,), 32 | ), 33 | migrations.CreateModel( 34 | name='Badge', 35 | fields=[ 36 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 37 | ('title', models.CharField(help_text=b'Short, descriptive title', unique=True, max_length=255)), 38 | ('slug', models.SlugField(help_text=b'Very short name, for use in URLs and links', unique=True)), 39 | ('description', models.TextField(help_text=b'Longer description of the badge and its criteria', blank=True)), 40 | ('image', models.ImageField(help_text=b'Upload an image to represent the badge', storage=django.core.files.storage.FileSystemStorage(base_url=b'uploads/', location=b'uploads'), null=True, upload_to=badger.models.UploadTo(b'image', b'png'), blank=True)), 41 | ('unique', models.BooleanField(default=True, help_text=b'Should awards of this badge be limited to one-per-person?')), 42 | ('nominations_accepted', models.BooleanField(default=True, help_text=b'Should this badge accept nominations from other users?')), 43 | ('nominations_autoapproved', models.BooleanField(default=False, help_text=b'Should all nominations be automatically approved?')), 44 | ('created', models.DateTimeField(auto_now_add=True)), 45 | ('modified', models.DateTimeField(auto_now=True)), 46 | ('creator', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True)), 47 | ('prerequisites', models.ManyToManyField(help_text=b'When all of the selected badges have been awarded, this badge will be automatically awarded.', to='badger.Badge', null=True, blank=True)), 48 | ], 49 | options={ 50 | 'ordering': ['-modified', '-created'], 51 | 'permissions': (('manage_deferredawards', 'Can manage deferred awards for this badge'),), 52 | }, 53 | bases=(models.Model,), 54 | ), 55 | migrations.CreateModel( 56 | name='DeferredAward', 57 | fields=[ 58 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 59 | ('description', models.TextField(blank=True)), 60 | ('reusable', models.BooleanField(default=False)), 61 | ('email', models.EmailField(db_index=True, max_length=75, null=True, blank=True)), 62 | ('claim_code', models.CharField(default=badger.models.make_random_code, unique=True, max_length=32, db_index=True)), 63 | ('claim_group', models.CharField(db_index=True, max_length=32, null=True, blank=True)), 64 | ('created', models.DateTimeField(auto_now_add=True)), 65 | ('modified', models.DateTimeField(auto_now=True)), 66 | ('badge', models.ForeignKey(to='badger.Badge')), 67 | ('creator', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True)), 68 | ], 69 | options={ 70 | 'ordering': ['-modified', '-created'], 71 | 'permissions': (('grant_deferredaward', 'Can grant deferred award to an email address'),), 72 | }, 73 | bases=(models.Model,), 74 | ), 75 | migrations.CreateModel( 76 | name='Nomination', 77 | fields=[ 78 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 79 | ('accepted', models.BooleanField(default=False)), 80 | ('rejected_reason', models.TextField(blank=True)), 81 | ('created', models.DateTimeField(auto_now_add=True)), 82 | ('modified', models.DateTimeField(auto_now=True)), 83 | ('approver', models.ForeignKey(related_name='nomination_approver', blank=True, to=settings.AUTH_USER_MODEL, null=True)), 84 | ('award', models.ForeignKey(blank=True, to='badger.Award', null=True)), 85 | ('badge', models.ForeignKey(to='badger.Badge')), 86 | ('creator', models.ForeignKey(related_name='nomination_creator', blank=True, to=settings.AUTH_USER_MODEL, null=True)), 87 | ('nominee', models.ForeignKey(related_name='nomination_nominee', to=settings.AUTH_USER_MODEL)), 88 | ('rejected_by', models.ForeignKey(related_name='nomination_rejected_by', blank=True, to=settings.AUTH_USER_MODEL, null=True)), 89 | ], 90 | options={ 91 | }, 92 | bases=(models.Model,), 93 | ), 94 | migrations.CreateModel( 95 | name='Progress', 96 | fields=[ 97 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 98 | ('percent', models.FloatField(default=0)), 99 | ('counter', models.FloatField(default=0, null=True, blank=True)), 100 | ('notes', badger.models.JSONField(null=True, blank=True)), 101 | ('created', models.DateTimeField(auto_now_add=True)), 102 | ('modified', models.DateTimeField(auto_now=True)), 103 | ('badge', models.ForeignKey(to='badger.Badge')), 104 | ('user', models.ForeignKey(related_name='progress_user', to=settings.AUTH_USER_MODEL)), 105 | ], 106 | options={ 107 | 'verbose_name_plural': 'Progresses', 108 | }, 109 | bases=(models.Model,), 110 | ), 111 | migrations.AlterUniqueTogether( 112 | name='progress', 113 | unique_together=set([('badge', 'user')]), 114 | ), 115 | migrations.AlterUniqueTogether( 116 | name='badge', 117 | unique_together=set([('title', 'slug')]), 118 | ), 119 | migrations.AddField( 120 | model_name='award', 121 | name='badge', 122 | field=models.ForeignKey(to='badger.Badge'), 123 | preserve_default=True, 124 | ), 125 | migrations.AddField( 126 | model_name='award', 127 | name='creator', 128 | field=models.ForeignKey(related_name='award_creator', blank=True, to=settings.AUTH_USER_MODEL, null=True), 129 | preserve_default=True, 130 | ), 131 | migrations.AddField( 132 | model_name='award', 133 | name='user', 134 | field=models.ForeignKey(related_name='award_user', to=settings.AUTH_USER_MODEL), 135 | preserve_default=True, 136 | ), 137 | ] 138 | -------------------------------------------------------------------------------- /badger/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/django-badger/07c28a8a1065ea7716f9988bf7541242a912fb1d/badger/migrations/__init__.py -------------------------------------------------------------------------------- /badger/printing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Quick and dirty render-to-PDF for badge award claim codes""" 3 | 4 | import logging 5 | import math 6 | import urllib 7 | import urllib2 8 | try: 9 | from cStringIO import cStringIO as StringIO 10 | except ImportError: 11 | from StringIO import StringIO 12 | 13 | from reportlab.pdfgen import canvas 14 | from reportlab.lib import pagesizes 15 | from reportlab.lib.units import inch 16 | from reportlab.platypus import ( 17 | SimpleDocTemplate, BaseDocTemplate, Paragraph, Preformatted, Spacer, 18 | PageBreak, Frame, FrameBreak, PageTemplate, Image, Table) 19 | from reportlab.platypus.doctemplate import LayoutError 20 | from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle 21 | from reportlab.rl_config import defaultPageSize 22 | from reportlab.lib.units import inch 23 | from reportlab.lib.enums import TA_LEFT, TA_CENTER 24 | from reportlab.lib import colors 25 | from reportlab.lib import textsplit 26 | 27 | from reportlab.lib.utils import ImageReader 28 | 29 | from django.conf import settings 30 | 31 | from django.http import (HttpResponseRedirect, HttpResponse, 32 | HttpResponseForbidden, HttpResponseNotFound) 33 | 34 | from django.utils.html import conditional_escape 35 | 36 | 37 | def render_claims_to_pdf(request, slug, claim_group, deferred_awards): 38 | """Currently hard-coded to print to Avery 22805 labels""" 39 | 40 | metrics = dict( 41 | page_width=(8.5 * inch), 42 | page_height=(11.0 * inch), 43 | 44 | top_margin=(0.5 * inch), 45 | left_margin=((25.0 / 32.0) * inch), 46 | 47 | qr_overlap=((1.0 / 32.0) * inch), 48 | padding=((1.0 / 16.0) * inch), 49 | 50 | horizontal_spacing=((5.0 / 16.0) * inch), 51 | vertical_spacing=((13.0 / 64.0) * inch), 52 | 53 | width=(1.5 * inch), 54 | height=(1.5 * inch), 55 | ) 56 | 57 | debug = (request.GET.get('debug', False) is not False) 58 | 59 | pagesize = (metrics['page_width'], metrics['page_height']) 60 | cols = int((metrics['page_width'] - metrics['left_margin']) / 61 | (metrics['width'] + metrics['horizontal_spacing'])) 62 | rows = int((metrics['page_height'] - metrics['top_margin']) / 63 | (metrics['height'] + metrics['vertical_spacing'])) 64 | per_page = (cols * rows) 65 | label_ct = len(deferred_awards) 66 | page_ct = math.ceil(label_ct / per_page) 67 | 68 | pages = [deferred_awards[x:x + (per_page)] 69 | for x in range(0, label_ct, per_page)] 70 | 71 | response = HttpResponse(content_type='application/pdf; charset=utf-8') 72 | if not debug: 73 | # If debugging, don't force download. 74 | response['Content-Disposition'] = ('attachment; filename="%s-%s.pdf"' % 75 | (slug.encode('utf-8', 'replace'), claim_group)) 76 | 77 | badge_img = None 78 | 79 | fout = StringIO() 80 | c = canvas.Canvas(fout, pagesize=pagesize) 81 | 82 | for page in pages: 83 | c.translate(metrics['left_margin'], 84 | metrics['page_height'] - metrics['top_margin']) 85 | 86 | for row in range(0, rows, 1): 87 | c.translate(0.0, 0 - metrics['height']) 88 | c.saveState() 89 | 90 | for col in range(0, cols, 1): 91 | 92 | try: 93 | da = page.pop(0) 94 | except IndexError: 95 | continue 96 | 97 | if not badge_img: 98 | image_fin = da.badge.image.file 99 | image_fin.open() 100 | badge_img = ImageReader(StringIO(image_fin.read())) 101 | 102 | c.saveState() 103 | render_label(request, c, metrics, da, badge_img, debug) 104 | c.restoreState() 105 | 106 | dx = (metrics['width'] + metrics['horizontal_spacing']) 107 | c.translate(dx, 0.0) 108 | 109 | c.restoreState() 110 | c.translate(0.0, 0 - metrics['vertical_spacing']) 111 | 112 | c.showPage() 113 | 114 | c.save() 115 | response.write(fout.getvalue()) 116 | return response 117 | 118 | 119 | def render_label(request, c, metrics, da, badge_img, debug): 120 | """Render a single label""" 121 | badge = da.badge 122 | 123 | badge_image_width = (1.0 + (1.0 / 64.0)) * inch 124 | badge_image_height = (1.0 + (1.0 / 64.0)) * inch 125 | 126 | qr_left = badge_image_width - metrics['qr_overlap'] 127 | qr_bottom = badge_image_height - metrics['qr_overlap'] 128 | qr_width = metrics['width'] - qr_left 129 | qr_height = metrics['height'] - qr_bottom 130 | 131 | if False and debug: 132 | # Draw some layout lines on debug. 133 | c.setLineWidth(0.3) 134 | c.rect(0, 0, metrics['width'], metrics['height']) 135 | c.rect(qr_left, qr_bottom, qr_width, qr_height) 136 | c.rect(0, 0, badge_image_width, badge_image_height) 137 | 138 | fit_text(c, da.badge.title, 139 | 0.0, badge_image_height, 140 | badge_image_width, qr_height) 141 | 142 | c.saveState() 143 | c.rotate(-90) 144 | 145 | code_height = qr_height * (0.45) 146 | claim_height = qr_height - code_height 147 | 148 | c.setFont("Courier", code_height) 149 | c.drawCentredString(0 - (badge_image_width / 2.0), 150 | metrics['height'] - code_height, 151 | da.claim_code) 152 | 153 | text = """ 154 | Claim at %s 155 | """ % (settings.SITE_TITLE) 156 | fit_text(c, text, 157 | 0 - badge_image_height, badge_image_width, 158 | badge_image_width, claim_height) 159 | 160 | c.restoreState() 161 | 162 | # Attempt to build a QR code image for the claim URL 163 | claim_url = request.build_absolute_uri(da.get_claim_url()) 164 | qr_img = None 165 | try: 166 | # Try using PyQRNative: http://code.google.com/p/pyqrnative/ 167 | # badg.us should have this in vendor-local 168 | from PyQRNative import QRCode, QRErrorCorrectLevel 169 | # TODO: Good-enough settings? 170 | if len(claim_url) < 20: 171 | qr = QRCode(3, QRErrorCorrectLevel.L) 172 | elif len(claim_url) < 50: 173 | qr = QRCode(4, QRErrorCorrectLevel.L) 174 | else: 175 | qr = QRCode(10, QRErrorCorrectLevel.L) 176 | qr.addData(claim_url) 177 | qr.make() 178 | qr_img = ImageReader(qr.makeImage()) 179 | 180 | except ImportError: 181 | try: 182 | # Hmm, if we don't have PyQRNative, then try abusing this web 183 | # service. Should be fine for low volumes. 184 | qr_url = ("http://api.qrserver.com/v1/create-qr-code/?%s" % 185 | urllib.urlencode({'size': '%sx%s' % (500, 500), 186 | 'data': claim_url})) 187 | 188 | qr_img = ImageReader(StringIO(urllib2.urlopen(qr_url).read())) 189 | 190 | except Exception: 191 | # Ignore issues in drawing the QR code - maybe show an error? 192 | pass 193 | 194 | if qr_img: 195 | c.drawImage(qr_img, qr_left, qr_bottom, qr_width, qr_height) 196 | 197 | c.drawImage(badge_img, 198 | 0.0 * inch, 0.0 * inch, 199 | badge_image_width, badge_image_height) 200 | 201 | 202 | def fit_text(c, text, x, y, max_w, max_h, font_name='Helvetica', 203 | padding_w=4.5, padding_h=4.5, font_decrement=0.0625): 204 | """Draw text, reducing font size until it fits with a given max width and 205 | height.""" 206 | 207 | max_w -= (padding_w * 2.0) 208 | max_h -= (padding_h * 2.0) 209 | 210 | x += padding_w 211 | y += padding_h 212 | 213 | font_size = max_h 214 | 215 | while font_size > 1.0: 216 | ps = ParagraphStyle(name='text', alignment=TA_CENTER, 217 | fontName=font_name, fontSize=font_size, 218 | leading=font_size) 219 | p = Paragraph(text, ps) 220 | actual_w, actual_h = p.wrapOn(c, max_w, max_h) 221 | if actual_h > max_h or actual_w > max_w: 222 | font_size -= font_decrement 223 | else: 224 | y_pad = (max_h - actual_h) / 2 225 | p.drawOn(c, x, y + y_pad) 226 | return 227 | -------------------------------------------------------------------------------- /badger/signals.py: -------------------------------------------------------------------------------- 1 | """Signals relating to badges. 2 | 3 | For each of these, you can register to receive them using standard 4 | Django methods. 5 | 6 | Let's look at :py:func:`badges.signals.badge_will_be_awarded`. For 7 | example:: 8 | 9 | from badger.signals import badge_will_be_awarded 10 | 11 | @receiver(badge_will_be_awarded) 12 | def my_callback(sender, **kwargs): 13 | award = kwargs['award'] 14 | 15 | print('sender: {0}'.format(sender)) 16 | print('award: {0}'.format(award)) 17 | 18 | 19 | The sender will be :py:class:`badges.models.Award` class. The 20 | ``award`` argument will be the ``Award`` instance that is being 21 | awarded. 22 | 23 | """ 24 | from django.dispatch import Signal 25 | 26 | 27 | def _signal_with_docs(args, doc): 28 | # FIXME - this fixes the docstring, but not the provided arguments 29 | # so the API docs look weird. 30 | signal = Signal(providing_args=args) 31 | signal.__doc__ = doc 32 | return signal 33 | 34 | 35 | badge_will_be_awarded = _signal_with_docs( 36 | ['award'], 37 | """Fires off before badge is awarded 38 | 39 | Signal receiver parameters: 40 | 41 | :arg award: the Award instance 42 | 43 | """) 44 | 45 | badge_was_awarded = _signal_with_docs( 46 | ['award'], 47 | """Fires off after badge is awarded 48 | 49 | Signal receiver parameters: 50 | 51 | :arg award: the Award instance 52 | 53 | """) 54 | 55 | user_will_be_nominated = _signal_with_docs( 56 | ['nomination'], 57 | """Fires off before user is nominated for a badge 58 | 59 | Signal receiver parameters: 60 | 61 | :arg nomination: the Nomination instance 62 | 63 | """) 64 | 65 | user_was_nominated = _signal_with_docs( 66 | ['nomination'], 67 | """Fires off after user is nominated for a badge 68 | 69 | Signal receiver parameters: 70 | 71 | :arg nomination: the Nomination instance 72 | 73 | """) 74 | 75 | nomination_will_be_approved = _signal_with_docs( 76 | ['nomination'], 77 | """Fires off before nomination is approved 78 | 79 | Signal receiver parameters: 80 | 81 | :arg nomination: the Nomination instance being approved 82 | 83 | """) 84 | 85 | nomination_was_approved = _signal_with_docs( 86 | ['nomination'], 87 | """Fires off after nomination is approved 88 | 89 | Signal receiver parameters: 90 | 91 | :arg nomination: the Nomination instance being approved 92 | 93 | """) 94 | 95 | nomination_will_be_accepted = _signal_with_docs( 96 | ['nomination'], 97 | """Fires off before nomination is accepted 98 | 99 | Signal receiver parameters: 100 | 101 | :arg nomination: the Nomination instance being accepted 102 | 103 | """) 104 | 105 | nomination_was_accepted = _signal_with_docs( 106 | ['nomination'], 107 | """Fires off after nomination is accepted 108 | 109 | Signal receiver parameters: 110 | 111 | :arg nomination: the Nomination instance being accepted 112 | 113 | """) 114 | 115 | nomination_will_be_rejected = _signal_with_docs( 116 | ['nomination'], 117 | """Fires off before nomination is rejected 118 | 119 | Signal receiver parameters: 120 | 121 | :arg nomination: the Nomination instance being rejected 122 | 123 | """) 124 | 125 | nomination_was_rejected = _signal_with_docs( 126 | ['nomination'], 127 | """Fires off after nomination is rejected 128 | 129 | Signal receiver parameters: 130 | 131 | :arg nomination: the Nomination instance being rejected 132 | 133 | """) 134 | -------------------------------------------------------------------------------- /badger/south_migrations/0003_auto__add_field_award_claim_code__chg_field_deferredaward_claim_code.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding field 'Award.claim_code' 12 | db.add_column('badger_award', 'claim_code', 13 | self.gf('django.db.models.fields.CharField')(default='', max_length=32, db_index=True, blank=True), 14 | keep_default=False) 15 | 16 | 17 | # Changing field 'DeferredAward.claim_code' 18 | db.alter_column('badger_deferredaward', 'claim_code', self.gf('django.db.models.fields.CharField')(unique=True, max_length=32)) 19 | def backwards(self, orm): 20 | # Deleting field 'Award.claim_code' 21 | db.delete_column('badger_award', 'claim_code') 22 | 23 | 24 | # Changing field 'DeferredAward.claim_code' 25 | db.alter_column('badger_deferredaward', 'claim_code', self.gf('django.db.models.fields.CharField')(max_length=6, unique=True)) 26 | models = { 27 | 'auth.group': { 28 | 'Meta': {'object_name': 'Group'}, 29 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 30 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 31 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 32 | }, 33 | 'auth.permission': { 34 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 35 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 36 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 37 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 38 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 39 | }, 40 | 'auth.user': { 41 | 'Meta': {'object_name': 'User'}, 42 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 43 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 44 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 45 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 46 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 47 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 48 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 49 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 50 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 51 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 52 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 53 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 54 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 55 | }, 56 | 'badger.award': { 57 | 'Meta': {'ordering': "['-modified', '-created']", 'object_name': 'Award'}, 58 | 'badge': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['badger.Badge']"}), 59 | 'claim_code': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'db_index': 'True', 'blank': 'True'}), 60 | 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 61 | 'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'award_creator'", 'null': 'True', 'to': "orm['auth.User']"}), 62 | 'hidden': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 63 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 64 | 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 65 | 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 66 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_user'", 'to': "orm['auth.User']"}) 67 | }, 68 | 'badger.badge': { 69 | 'Meta': {'ordering': "['-modified', '-created']", 'unique_together': "(('title', 'slug'),)", 'object_name': 'Badge'}, 70 | 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 71 | 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), 72 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 73 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 74 | 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 75 | 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 76 | 'nominations_accepted': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 77 | 'prerequisites': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['badger.Badge']", 'null': 'True', 'blank': 'True'}), 78 | 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'}), 79 | 'title': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), 80 | 'unique': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) 81 | }, 82 | 'badger.deferredaward': { 83 | 'Meta': {'ordering': "['-modified', '-created']", 'object_name': 'DeferredAward'}, 84 | 'badge': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['badger.Badge']"}), 85 | 'claim_code': ('django.db.models.fields.CharField', [], {'default': "'yuv7xu'", 'unique': 'True', 'max_length': '32', 'db_index': 'True'}), 86 | 'claim_group': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '32', 'null': 'True', 'blank': 'True'}), 87 | 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 88 | 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), 89 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 90 | 'email': ('django.db.models.fields.EmailField', [], {'db_index': 'True', 'max_length': '75', 'null': 'True', 'blank': 'True'}), 91 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 92 | 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 93 | 'reusable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) 94 | }, 95 | 'badger.progress': { 96 | 'Meta': {'unique_together': "(('badge', 'user'),)", 'object_name': 'Progress'}, 97 | 'badge': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['badger.Badge']"}), 98 | 'counter': ('django.db.models.fields.FloatField', [], {'default': '0', 'null': 'True', 'blank': 'True'}), 99 | 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 100 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 101 | 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 102 | 'notes': ('badger.models.JSONField', [], {'null': 'True', 'blank': 'True'}), 103 | 'percent': ('django.db.models.fields.FloatField', [], {'default': '0'}), 104 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'progress_user'", 'to': "orm['auth.User']"}) 105 | }, 106 | 'contenttypes.contenttype': { 107 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 108 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 109 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 110 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 111 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 112 | }, 113 | 'taggit.tag': { 114 | 'Meta': {'object_name': 'Tag'}, 115 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 116 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 117 | 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'}) 118 | }, 119 | 'taggit.taggeditem': { 120 | 'Meta': {'object_name': 'TaggedItem'}, 121 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), 122 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 123 | 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), 124 | 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}) 125 | } 126 | } 127 | 128 | complete_apps = ['badger'] -------------------------------------------------------------------------------- /badger/south_migrations/0007_auto__add_field_badge_nominations_autoapproved.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding field 'Badge.nominations_autoapproved' 12 | db.add_column('badger_badge', 'nominations_autoapproved', 13 | self.gf('django.db.models.fields.BooleanField')(default=False), 14 | keep_default=False) 15 | 16 | 17 | def backwards(self, orm): 18 | # Deleting field 'Badge.nominations_autoapproved' 19 | db.delete_column('badger_badge', 'nominations_autoapproved') 20 | 21 | 22 | models = { 23 | 'auth.group': { 24 | 'Meta': {'object_name': 'Group'}, 25 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 26 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 27 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 28 | }, 29 | 'auth.permission': { 30 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 31 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 32 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 33 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 34 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 35 | }, 36 | 'auth.user': { 37 | 'Meta': {'object_name': 'User'}, 38 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 39 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 40 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 41 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 42 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 43 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 44 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 45 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 46 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 47 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 48 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 49 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 50 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 51 | }, 52 | 'badger.award': { 53 | 'Meta': {'ordering': "['-modified', '-created']", 'object_name': 'Award'}, 54 | 'badge': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['badger.Badge']"}), 55 | 'claim_code': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'db_index': 'True', 'blank': 'True'}), 56 | 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 57 | 'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'award_creator'", 'null': 'True', 'to': "orm['auth.User']"}), 58 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 59 | 'hidden': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 60 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 61 | 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 62 | 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 63 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_user'", 'to': "orm['auth.User']"}) 64 | }, 65 | 'badger.badge': { 66 | 'Meta': {'ordering': "['-modified', '-created']", 'unique_together': "(('title', 'slug'),)", 'object_name': 'Badge'}, 67 | 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 68 | 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), 69 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 70 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 71 | 'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), 72 | 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 73 | 'nominations_accepted': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 74 | 'nominations_autoapproved': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 75 | 'prerequisites': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['badger.Badge']", 'null': 'True', 'blank': 'True'}), 76 | 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'}), 77 | 'title': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), 78 | 'unique': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) 79 | }, 80 | 'badger.deferredaward': { 81 | 'Meta': {'ordering': "['-modified', '-created']", 'object_name': 'DeferredAward'}, 82 | 'badge': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['badger.Badge']"}), 83 | 'claim_code': ('django.db.models.fields.CharField', [], {'default': "'m34huu'", 'unique': 'True', 'max_length': '32', 'db_index': 'True'}), 84 | 'claim_group': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '32', 'null': 'True', 'blank': 'True'}), 85 | 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 86 | 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), 87 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 88 | 'email': ('django.db.models.fields.EmailField', [], {'db_index': 'True', 'max_length': '75', 'null': 'True', 'blank': 'True'}), 89 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 90 | 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 91 | 'reusable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) 92 | }, 93 | 'badger.nomination': { 94 | 'Meta': {'object_name': 'Nomination'}, 95 | 'accepted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 96 | 'approver': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'nomination_approver'", 'null': 'True', 'to': "orm['auth.User']"}), 97 | 'award': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['badger.Award']", 'null': 'True', 'blank': 'True'}), 98 | 'badge': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['badger.Badge']"}), 99 | 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 100 | 'creator': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'nomination_creator'", 'null': 'True', 'to': "orm['auth.User']"}), 101 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 102 | 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 103 | 'nominee': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'nomination_nominee'", 'to': "orm['auth.User']"}), 104 | 'rejected_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'nomination_rejected_by'", 'null': 'True', 'to': "orm['auth.User']"}), 105 | 'rejected_reason': ('django.db.models.fields.TextField', [], {'blank': 'True'}) 106 | }, 107 | 'badger.progress': { 108 | 'Meta': {'unique_together': "(('badge', 'user'),)", 'object_name': 'Progress'}, 109 | 'badge': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['badger.Badge']"}), 110 | 'counter': ('django.db.models.fields.FloatField', [], {'default': '0', 'null': 'True', 'blank': 'True'}), 111 | 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 112 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 113 | 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 114 | 'notes': ('badger.models.JSONField', [], {'null': 'True', 'blank': 'True'}), 115 | 'percent': ('django.db.models.fields.FloatField', [], {'default': '0'}), 116 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'progress_user'", 'to': "orm['auth.User']"}) 117 | }, 118 | 'contenttypes.contenttype': { 119 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 120 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 121 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 122 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 123 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 124 | } 125 | } 126 | 127 | complete_apps = ['badger'] -------------------------------------------------------------------------------- /badger/south_migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/django-badger/07c28a8a1065ea7716f9988bf7541242a912fb1d/badger/south_migrations/__init__.py -------------------------------------------------------------------------------- /badger/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/django-badger/07c28a8a1065ea7716f9988bf7541242a912fb1d/badger/templatetags/__init__.py -------------------------------------------------------------------------------- /badger/templatetags/badger_tags.py: -------------------------------------------------------------------------------- 1 | # django 2 | import django 3 | from django import template 4 | from django.conf import settings 5 | from django.shortcuts import get_object_or_404 6 | from badger.models import Award, Badge 7 | 8 | from django.core.exceptions import ObjectDoesNotExist 9 | from django.core.urlresolvers import reverse 10 | 11 | import hashlib 12 | import urllib 13 | 14 | from django.utils.translation import ugettext_lazy as _ 15 | 16 | if django.VERSION < (1, 7, 0): 17 | from django.contrib.auth.models import SiteProfileNotAvailable 18 | 19 | if django.VERSION >= (1, 7, 0): 20 | class SiteProfileNotAvailable(Exception): 21 | pass 22 | 23 | register = template.Library() 24 | 25 | 26 | @register.filter 27 | def permissions_for(obj, user): 28 | try: 29 | return obj.get_permissions_for(user) 30 | except: 31 | return {} 32 | 33 | 34 | @register.filter 35 | def key(obj, name): 36 | try: 37 | return obj[name] 38 | except: 39 | return None 40 | 41 | 42 | @register.simple_tag 43 | def user_avatar(user, secure=False, size=256, rating='pg', default=''): 44 | 45 | try: 46 | profile = user.get_profile() 47 | if profile.avatar: 48 | return profile.avatar.url 49 | except SiteProfileNotAvailable: 50 | pass 51 | except ObjectDoesNotExist: 52 | pass 53 | except AttributeError: 54 | pass 55 | 56 | base_url = (secure and 'https://secure.gravatar.com' or 57 | 'http://www.gravatar.com') 58 | m = hashlib.md5(user.email) 59 | return '%(base_url)s/avatar/%(hash)s?%(params)s' % dict( 60 | base_url=base_url, hash=m.hexdigest(), 61 | params=urllib.urlencode(dict( 62 | s=size, d=default, r=rating 63 | )) 64 | ) 65 | 66 | 67 | @register.simple_tag 68 | def award_image(award): 69 | if award.image: 70 | img_url = award.image.url 71 | elif award.badge.image: 72 | img_url = award.badge.image.url 73 | else: 74 | img_url = "/media/img/default-badge.png" 75 | 76 | return img_url 77 | 78 | 79 | @register.simple_tag 80 | def user_award_list(badge, user): 81 | if badge.allows_award_to(user): 82 | return '
      • %s
      • ' % (reverse('badger.views.award_badge', args=[badge.slug, ]), _(u'Issue award')) 83 | else: 84 | return '' 85 | -------------------------------------------------------------------------------- /badger/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from contextlib import contextmanager 4 | 5 | from django.conf import settings 6 | from django.core.management import call_command 7 | from django.db.models import loading 8 | from django.contrib.auth.models import User 9 | from django import test 10 | from django.utils.translation import get_language 11 | 12 | try: 13 | from funfactory.urlresolvers import (get_url_prefix, Prefixer, reverse, 14 | set_url_prefix) 15 | from tower import activate 16 | from django.test.client import RequestFactory 17 | except ImportError: 18 | from django.core.urlresolvers import reverse 19 | get_url_prefix = None 20 | 21 | import badger 22 | 23 | from badger.models import (Badge, Award, Progress, DeferredAward) 24 | 25 | 26 | class SettingDoesNotExist: 27 | pass 28 | 29 | 30 | @contextmanager 31 | def patch_settings(**kwargs): 32 | """Contextmanager to temporarily change settings. 33 | See: http://stackoverflow.com/a/3519955""" 34 | from django.conf import settings 35 | old_settings = [] 36 | del_settings = [] 37 | for key, new_value in kwargs.items(): 38 | old_value = getattr(settings, key, SettingDoesNotExist) 39 | old_settings.append((key, old_value)) 40 | setattr(settings, key, new_value) 41 | yield 42 | for key, old_value in old_settings: 43 | if old_value is SettingDoesNotExist: 44 | delattr(settings, key) 45 | else: 46 | setattr(settings, key, old_value) 47 | 48 | 49 | class BadgerTestCase(test.TestCase): 50 | """Ensure test app and models are set up before tests""" 51 | 52 | def _pre_setup(self): 53 | loading.cache.loaded = False 54 | call_command('update_badges', verbosity=0) 55 | badger.autodiscover() 56 | 57 | if get_url_prefix: 58 | # If we're in funfactoryland, make sure a locale prefix is 59 | # set for urlresolvers 60 | locale = 'en-US' 61 | self.old_prefix = get_url_prefix() 62 | self.old_locale = get_language() 63 | rf = RequestFactory() 64 | set_url_prefix(Prefixer(rf.get('/%s/' % (locale,)))) 65 | 66 | # Create a default user for tests 67 | self.user_1 = self._get_user(username="user_1", 68 | email="user_1@example.com", 69 | password="user_1_pass") 70 | 71 | # Call the original method that does the fixtures etc. 72 | super(test.TestCase, self)._pre_setup() 73 | 74 | def _post_teardown(self): 75 | # Call the original method. 76 | super(test.TestCase, self)._post_teardown() 77 | 78 | Award.objects.all().delete() 79 | Badge.objects.all().delete() 80 | 81 | loading.cache.loaded = False 82 | 83 | if get_url_prefix: 84 | # If we're in funfactoryland, back out of the locale tweaks 85 | set_url_prefix(self.old_prefix) 86 | 87 | def _get_user(self, username="tester", email="tester@example.com", 88 | password="trustno1", is_staff=False, is_superuser=False): 89 | (user, created) = User.objects.get_or_create(username=username, 90 | defaults=dict(email=email)) 91 | if created: 92 | user.is_superuser = is_superuser 93 | user.is_staff = is_staff 94 | user.set_password(password) 95 | user.save() 96 | return user 97 | 98 | def _get_badge(self, title="Test Badge", 99 | description="This is a test badge", creator=None, unique=True): 100 | if creator is None: 101 | creator = self.user_1 102 | elif creator is False: 103 | creator = None 104 | (badge, created) = Badge.objects.get_or_create(title=title, 105 | defaults=dict(description=description, creator=creator, 106 | unique=unique)) 107 | return badge 108 | -------------------------------------------------------------------------------- /badger/tests/test_badges_py.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.conf import settings 4 | from django.core.management import call_command 5 | from django.template.defaultfilters import slugify 6 | from django.contrib.auth.models import User 7 | 8 | from nose.tools import assert_equal, with_setup, assert_false, eq_, ok_ 9 | from nose.plugins.attrib import attr 10 | 11 | from . import BadgerTestCase 12 | 13 | import badger 14 | from badger.utils import get_badge, award_badge 15 | 16 | from badger.models import (Badge, Award, Progress, 17 | BadgeAwardNotAllowedException, 18 | BadgeAlreadyAwardedException) 19 | 20 | from badger_example.models import GuestbookEntry 21 | 22 | 23 | class BadgesPyTest(BadgerTestCase): 24 | 25 | def setUp(self): 26 | self.user_1 = self._get_user(username="user_1", 27 | email="user_1@example.com", password="user_1_pass") 28 | Award.objects.all().delete() 29 | 30 | def tearDown(self): 31 | Award.objects.all().delete() 32 | Badge.objects.all().delete() 33 | 34 | def test_badges_from_fixture(self): 35 | """Badges can be created via fixture""" 36 | b = get_badge("test-1") 37 | eq_("Test #1", b.title) 38 | b = get_badge("button-clicker") 39 | eq_("Button Clicker", b.title) 40 | b = get_badge("first-post") 41 | eq_("First post!", b.title) 42 | 43 | def test_badges_from_code(self): 44 | """Badges can be created in code""" 45 | b = get_badge("test-2") 46 | eq_("Test #2", b.title) 47 | b = get_badge("awesomeness") 48 | eq_("Awesomeness (you have it)", b.title) 49 | b = get_badge("250-words") 50 | eq_("250 Words", b.title) 51 | b = get_badge("master-badger") 52 | eq_("Master Badger", b.title) 53 | 54 | def test_badge_awarded_on_model_create(self): 55 | """A badge should be awarded on first guestbook post""" 56 | user = self._get_user() 57 | post = GuestbookEntry(message="This is my first post", creator=user) 58 | post.save() 59 | b = get_badge('first-post') 60 | ok_(b.is_awarded_to(user)) 61 | 62 | # "first-post" badge should be unique 63 | post = GuestbookEntry(message="This is my first post", creator=user) 64 | post.save() 65 | eq_(1, Award.objects.filter(user=user, badge=b).count()) 66 | 67 | def test_badge_awarded_on_content(self): 68 | """A badge should be awarded upon 250 words worth of guestbook posts 69 | created""" 70 | user = self._get_user() 71 | 72 | b = get_badge('250-words') 73 | 74 | # Post 5 words in progress... 75 | GuestbookEntry.objects.create(creator=user, 76 | message="A few words to start") 77 | ok_(not b.is_awarded_to(user)) 78 | eq_(5, b.progress_for(user).counter) 79 | 80 | # Post 5 more words in progress... 81 | GuestbookEntry.objects.create(creator=user, 82 | message="A few more words posted") 83 | ok_(not b.is_awarded_to(user)) 84 | eq_(10, b.progress_for(user).counter) 85 | 86 | # Post 90 more... 87 | msg = ' '.join('lots of words that repeat' for x in range(18)) 88 | GuestbookEntry.objects.create(creator=user, message=msg) 89 | ok_(not b.is_awarded_to(user)) 90 | eq_(100, b.progress_for(user).counter) 91 | 92 | # And 150 more for the finale... 93 | msg = ' '.join('lots of words that repeat' for x in range(30)) 94 | GuestbookEntry.objects.create(creator=user, message=msg) 95 | 96 | # Should result in a badge award and reset progress. 97 | ok_(b.is_awarded_to(user)) 98 | eq_(0, b.progress_for(user).counter) 99 | 100 | # But, just checking the reset counter shouldn't create a new DB row. 101 | eq_(0, Progress.objects.filter(user=user, badge=b).count()) 102 | 103 | def test_badge_awarded_on_content_percent(self): 104 | """A badge should be awarded upon 250 words worth of guestbook posts 105 | created, but the tracking is done via percentage""" 106 | user = self._get_user() 107 | 108 | b = get_badge('250-words-by-percent') 109 | 110 | # Post 5 words in progress... 111 | GuestbookEntry.objects.create(creator=user, 112 | message="A few words to start") 113 | ok_(not b.is_awarded_to(user)) 114 | eq_((5.0 / 250.0) * 100.0, b.progress_for(user).percent) 115 | 116 | # Post 5 more words in progress... 117 | GuestbookEntry.objects.create(creator=user, 118 | message="A few more words posted") 119 | ok_(not b.is_awarded_to(user)) 120 | eq_((10.0 / 250.0) * 100.0, b.progress_for(user).percent) 121 | 122 | # Post 90 more... 123 | msg = ' '.join('lots of words that repeat' for x in range(18)) 124 | GuestbookEntry.objects.create(creator=user, message=msg) 125 | ok_(not b.is_awarded_to(user)) 126 | eq_((100.0 / 250.0) * 100.0, b.progress_for(user).percent) 127 | 128 | # And 150 more for the finale... 129 | msg = ' '.join('lots of words that repeat' for x in range(30)) 130 | GuestbookEntry.objects.create(creator=user, message=msg) 131 | 132 | # Should result in a badge award and reset progress. 133 | ok_(b.is_awarded_to(user)) 134 | eq_(0, b.progress_for(user).percent) 135 | 136 | # But, just checking the reset percent shouldn't create a new DB row. 137 | eq_(0, Progress.objects.filter(user=user, badge=b).count()) 138 | 139 | def test_metabadge_awarded(self): 140 | """Upon completing collection of badges, award a meta-badge""" 141 | user = self._get_user() 142 | 143 | ok_(not get_badge('master-badger').is_awarded_to(user)) 144 | 145 | # Cover a few bases on award creation... 146 | award_badge('test-1', user) 147 | award_badge('test-2', user) 148 | a = Award(badge=get_badge('button-clicker'), user=user) 149 | a.save() 150 | 151 | get_badge('awesomeness').award_to(user) 152 | eq_(1, Award.objects.filter(badge=get_badge("master-badger"), 153 | user=user).count()) 154 | 155 | ok_(get_badge('master-badger').is_awarded_to(user)) 156 | 157 | def test_progress_quiet_save(self): 158 | """Progress will not raise a BadgeAlreadyAwardedException unless told""" 159 | b = self._get_badge('imunique') 160 | b.unique = True 161 | b.save() 162 | 163 | user = self._get_user() 164 | 165 | b.progress_for(user).update_percent(50) 166 | b.progress_for(user).update_percent(75) 167 | b.progress_for(user).update_percent(100) 168 | 169 | ok_(b.is_awarded_to(user)) 170 | 171 | try: 172 | b.progress_for(user).update_percent(50) 173 | b.progress_for(user).update_percent(75) 174 | b.progress_for(user).update_percent(100) 175 | except BadgeAlreadyAwardedException: 176 | ok_(False, "Exception should not have been raised") 177 | 178 | try: 179 | b.progress_for(user).update_percent(50, raise_exception=True) 180 | b.progress_for(user).update_percent(75, raise_exception=True) 181 | b.progress_for(user).update_percent(100, raise_exception=True) 182 | ok_(False, "Exception should have been raised") 183 | except BadgeAlreadyAwardedException: 184 | pass 185 | 186 | def _get_user(self, username="tester", email="tester@example.com", 187 | password="trustno1"): 188 | (user, created) = User.objects.get_or_create(username=username, 189 | defaults=dict(email=email)) 190 | if created: 191 | user.set_password(password) 192 | user.save() 193 | return user 194 | 195 | def _get_badge(self, title="Test Badge", 196 | description="This is a test badge", creator=None): 197 | creator = creator or self.user_1 198 | (badge, created) = Badge.objects.get_or_create(title=title, 199 | defaults=dict(description=description, creator=creator)) 200 | return badge 201 | -------------------------------------------------------------------------------- /badger/tests/test_feeds.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import feedparser 3 | 4 | from django.conf import settings 5 | 6 | from django.http import HttpRequest 7 | from django.test.client import Client 8 | 9 | from pyquery import PyQuery as pq 10 | 11 | from nose.tools import assert_equal, with_setup, assert_false, eq_, ok_ 12 | from nose.plugins.attrib import attr 13 | 14 | from django.template.defaultfilters import slugify 15 | 16 | from django.contrib.auth.models import User 17 | 18 | try: 19 | from commons.urlresolvers import reverse 20 | except ImportError: 21 | from django.core.urlresolvers import reverse 22 | 23 | from . import BadgerTestCase 24 | 25 | from badger.models import (Badge, Award, Progress, 26 | BadgeAwardNotAllowedException) 27 | from badger.utils import get_badge, award_badge 28 | 29 | 30 | class BadgerFeedsTest(BadgerTestCase): 31 | 32 | def setUp(self): 33 | self.testuser = self._get_user() 34 | self.client = Client() 35 | Award.objects.all().delete() 36 | 37 | def tearDown(self): 38 | Award.objects.all().delete() 39 | Badge.objects.all().delete() 40 | 41 | def test_award_feeds(self): 42 | """Can view award detail""" 43 | user = self._get_user() 44 | user2 = self._get_user(username='tester2') 45 | 46 | b1, created = Badge.objects.get_or_create(creator=user, title="Code Badge #1") 47 | award = b1.award_to(user2) 48 | 49 | # The award should show up in each of these feeds. 50 | feed_urls = ( 51 | reverse('badger.feeds.awards_recent', 52 | args=('atom', )), 53 | reverse('badger.feeds.awards_by_badge', 54 | args=('atom', b1.slug, )), 55 | reverse('badger.feeds.awards_by_user', 56 | args=('atom', user2.username,)), 57 | ) 58 | 59 | # Check each of the feeds 60 | for feed_url in feed_urls: 61 | r = self.client.get(feed_url, follow=True) 62 | 63 | # The feed should be parsed without issues by feedparser 64 | feed = feedparser.parse(r.content) 65 | eq_(0, feed.bozo) 66 | 67 | # Look through entries for the badge title 68 | found_it = False 69 | for entry in feed.entries: 70 | if b1.title in entry.title and user2.username in entry.title: 71 | found_it = True 72 | 73 | ok_(found_it) 74 | 75 | def _get_user(self, username="tester", email="tester@example.com", 76 | password="trustno1"): 77 | (user, created) = User.objects.get_or_create(username=username, 78 | defaults=dict(email=email)) 79 | if created: 80 | user.set_password(password) 81 | user.save() 82 | return user 83 | -------------------------------------------------------------------------------- /badger/tests/test_middleware.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import time 4 | 5 | from nose.tools import assert_equal, with_setup, assert_false, eq_, ok_ 6 | from nose.plugins.attrib import attr 7 | 8 | from django.contrib.auth.models import AnonymousUser 9 | from django.core.urlresolvers import reverse 10 | from django.http import HttpRequest, HttpResponse 11 | from django.test.client import Client 12 | 13 | from . import BadgerTestCase 14 | 15 | import badger 16 | 17 | from badger.models import (Badge, Award, Nomination, Progress, DeferredAward) 18 | from badger.middleware import (RecentBadgeAwardsMiddleware, 19 | LAST_CHECK_COOKIE_NAME) 20 | 21 | 22 | class RecentBadgeAwardsMiddlewareTest(BadgerTestCase): 23 | 24 | def setUp(self): 25 | self.creator = self._get_user(username='creator') 26 | self.testuser = self._get_user() 27 | self.mw = RecentBadgeAwardsMiddleware() 28 | 29 | self.badges = [] 30 | for n in ('test1','test2','test3'): 31 | badge = Badge(title=n, creator=self.creator) 32 | badge.save() 33 | self.badges.append(badge) 34 | 35 | self.awards = [] 36 | for b in self.badges: 37 | self.awards.append(b.award_to(self.testuser)) 38 | 39 | def tearDown(self): 40 | Award.objects.all().delete() 41 | Badge.objects.all().delete() 42 | 43 | def test_no_process_request(self): 44 | """If something skips our process_request, the process_response hook 45 | should do nothing.""" 46 | request = HttpRequest() 47 | response = HttpResponse() 48 | self.mw.process_response(request, response) 49 | 50 | def test_anonymous(self): 51 | """No recent awards for anonymous user""" 52 | request = HttpRequest() 53 | request.user = AnonymousUser() 54 | self.mw.process_request(request) 55 | ok_(hasattr(request, 'recent_badge_awards')) 56 | eq_(0, len(request.recent_badge_awards)) 57 | 58 | def test_no_cookie(self): 59 | """No recent awards without a last-check cookie, but set the cookie""" 60 | request = HttpRequest() 61 | request.user = self.testuser 62 | self.mw.process_request(request) 63 | ok_(hasattr(request, 'recent_badge_awards')) 64 | eq_(0, len(request.recent_badge_awards)) 65 | eq_(False, request.recent_badge_awards.was_used) 66 | 67 | response = HttpResponse() 68 | self.mw.process_response(request, response) 69 | ok_(LAST_CHECK_COOKIE_NAME in response.cookies) 70 | 71 | def test_unused_recent_badge_awards(self): 72 | """Recent awards offered with cookie, but cookie not updated if unused""" 73 | request = HttpRequest() 74 | request.user = self.testuser 75 | request.COOKIES[LAST_CHECK_COOKIE_NAME] = '1156891591.492586' 76 | self.mw.process_request(request) 77 | ok_(hasattr(request, 'recent_badge_awards')) 78 | 79 | response = HttpResponse() 80 | self.mw.process_response(request, response) 81 | ok_(LAST_CHECK_COOKIE_NAME not in response.cookies) 82 | 83 | def test_used_recent_badge_awards(self): 84 | """Recent awards offered with cookie, cookie updated if set used""" 85 | old_time = '1156891591.492586' 86 | request = HttpRequest() 87 | request.user = self.testuser 88 | request.COOKIES[LAST_CHECK_COOKIE_NAME] = old_time 89 | self.mw.process_request(request) 90 | ok_(hasattr(request, 'recent_badge_awards')) 91 | 92 | # Use the recent awards set by checking length and contents 93 | eq_(3, len(request.recent_badge_awards)) 94 | for ra in request.recent_badge_awards: 95 | ok_(ra in self.awards) 96 | 97 | response = HttpResponse() 98 | self.mw.process_response(request, response) 99 | ok_(LAST_CHECK_COOKIE_NAME in response.cookies) 100 | ok_(response.cookies[LAST_CHECK_COOKIE_NAME].value != old_time) 101 | -------------------------------------------------------------------------------- /badger/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | 3 | from django.conf import settings 4 | 5 | from .feeds import (AwardsRecentFeed, AwardsByUserFeed, AwardsByBadgeFeed, 6 | BadgesRecentFeed, BadgesByUserFeed) 7 | from . import views 8 | 9 | 10 | urlpatterns = patterns('badger.views', 11 | url(r'^$', 'badges_list', name='badger.badges_list'), 12 | url(r'^staff_tools$', 'staff_tools', 13 | name='badger.staff_tools'), 14 | url(r'^tag/(?P.+)/?$', 'badges_list', 15 | name='badger.badges_list'), 16 | url(r'^awards/?', 'awards_list', 17 | name='badger.awards_list'), 18 | url(r'^badge/(?P[^/]+)/awards/?$', 'awards_list', 19 | name='badger.awards_list_for_badge'), 20 | url(r'^badge/(?P[^/]+)/awards/(?P\d+)\.json$', 'award_detail', 21 | kwargs=dict(format="json"), 22 | name='badger.award_detail_json'), 23 | url(r'^badge/(?P[^/]+)/awards/(?P\d+)/?$', 'award_detail', 24 | name='badger.award_detail'), 25 | url(r'^badge/(?P[^/]+)/awards/(?P\d+)/delete$', 'award_delete', 26 | name='badger.award_delete'), 27 | url(r'^badge/(?P[^/]+)/claims/(?P.+)\.pdf$', 'claims_list', 28 | kwargs=dict(format='pdf'), 29 | name='badger.claims_list_pdf'), 30 | url(r'^badge/(?P[^/]+)/claims/(?P[^/]+)/?$', 'claims_list', 31 | name='badger.claims_list'), 32 | url(r'^claim/(?P[^/]+)/?$', 'claim_deferred_award', 33 | name='badger.claim_deferred_award'), 34 | url(r'^claim/?$', 'claim_deferred_award', 35 | name='badger.claim_deferred_award_form'), 36 | url(r'^badge/(?P[^/]+)/award', 'award_badge', 37 | name='badger.award_badge'), 38 | url(r'^badge/(?P.+)\.json$', 'detail', 39 | kwargs=dict(format="json"), 40 | name='badger.detail_json'), 41 | url(r'^badge/(?P[^/]+)/?$', 'detail', 42 | name='badger.detail'), 43 | url(r'^badge/(?P[^/]+)/awards/?$', 'awards_by_badge', 44 | name='badger.awards_by_badge'), 45 | url(r'^users/(?P[^/]+)/awards/?$', 'awards_by_user', 46 | name='badger.awards_by_user'), 47 | 48 | url(r'^create$', 'create', 49 | name='badger.create_badge'), 50 | url(r'^badge/(?P[^/]+)/nominate$', 'nominate_for', 51 | name='badger.nominate_for'), 52 | url(r'^badge/(?P[^/]+)/edit$', 'edit', 53 | name='badger.badge_edit'), 54 | url(r'^badge/(?P[^/]+)/delete$', 'delete', 55 | name='badger.badge_delete'), 56 | url(r'^badge/(?P[^/]+)/nominations/(?P\d+)/?$', 'nomination_detail', 57 | name='badger.nomination_detail'), 58 | url(r'^users/(?P[^/]+)/badges/?$', 'badges_by_user', 59 | name='badger.badges_by_user'), 60 | 61 | url(r'^feeds/(?P[^/]+)/badges/?$', BadgesRecentFeed(), 62 | name="badger.feeds.badges_recent"), 63 | url(r'^feeds/(?P[^/]+)/users/(?P[^/]+)/badges/?$', 64 | BadgesByUserFeed(), 65 | name="badger.feeds.badges_by_user"), 66 | 67 | url(r'^feeds/(?P[^/]+)/awards/?$', 68 | AwardsRecentFeed(), name="badger.feeds.awards_recent"), 69 | url(r'^feeds/(?P[^/]+)/badge/(?P[^/]+)/awards/?$', 70 | AwardsByBadgeFeed(), name="badger.feeds.awards_by_badge"), 71 | url(r'^feeds/(?P[^/]+)/users/(?P[^/]+)/awards/?$', 72 | AwardsByUserFeed(), name="badger.feeds.awards_by_user"), 73 | ) 74 | -------------------------------------------------------------------------------- /badger/urls_simple.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is a simplified URLs list that omits any of the multiplayer features, 3 | assuming that all badges will be managed from the admin interface, and most 4 | badges will be awarded in badges.py 5 | """ 6 | from django.conf.urls import patterns, include, url 7 | 8 | from django.conf import settings 9 | 10 | from .feeds import (AwardsRecentFeed, AwardsByUserFeed, AwardsByBadgeFeed, 11 | BadgesRecentFeed, BadgesByUserFeed) 12 | from . import views 13 | 14 | 15 | urlpatterns = patterns('badger.views', 16 | url(r'^$', 'badges_list', name='badger.badges_list'), 17 | url(r'^tag/(?P.+)/?$', 'badges_list', 18 | name='badger.badges_list'), 19 | url(r'^awards/?', 'awards_list', 20 | name='badger.awards_list'), 21 | url(r'^badge/(?P[^/]+)/awards/?$', 'awards_list', 22 | name='badger.awards_list_for_badge'), 23 | url(r'^badge/(?P[^/]+)/awards/(?P[^\.]+)\.json$', 'award_detail', 24 | kwargs=dict(format="json"), 25 | name='badger.award_detail_json'), 26 | url(r'^badge/(?P[^/]+)/awards/(?P[^/]+)/?$', 'award_detail', 27 | name='badger.award_detail'), 28 | url(r'^badge/(?P[^/]+)/awards/(?P[^/]+)/delete$', 'award_delete', 29 | name='badger.award_delete'), 30 | url(r'^badge/(?P[^\.]+)\.json$', 'detail', 31 | kwargs=dict(format="json"), 32 | name='badger.detail_json'), 33 | url(r'^badge/(?P[^/]+)/?$', 'detail', 34 | name='badger.detail'), 35 | url(r'^badge/(?P[^/]+)/awards/?$', 'awards_by_badge', 36 | name='badger.awards_by_badge'), 37 | url(r'^users/(?P[^/]+)/awards/?$', 'awards_by_user', 38 | name='badger.awards_by_user'), 39 | url(r'^feeds/(?P[^/]+)/badges/?$', BadgesRecentFeed(), 40 | name="badger.feeds.badges_recent"), 41 | url(r'^feeds/(?P[^/]+)/awards/?$', 42 | AwardsRecentFeed(), name="badger.feeds.awards_recent"), 43 | url(r'^feeds/(?P[^/]+)/badge/(?P[^/]+)/awards/?$', 44 | AwardsByBadgeFeed(), name="badger.feeds.awards_by_badge"), 45 | url(r'^feeds/(?P[^/]+)/users/(?P[^/]+)/awards/?$', 46 | AwardsByUserFeed(), name="badger.feeds.awards_by_user"), 47 | ) 48 | -------------------------------------------------------------------------------- /badger/utils.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db.models import signals 3 | 4 | import badger 5 | from badger.models import Badge, Award, Progress 6 | 7 | 8 | def update_badges(badge_data, overwrite=False): 9 | """Creates or updates list of badges 10 | 11 | :arg badge_data: list of dicts. keys in the dict correspond to the 12 | Badge model class. Also, you can pass in ``prerequisites``. 13 | :arg overwrite: whether or not to overwrite the existing badge 14 | 15 | :returns: list of Badge instances---one per dict passed in 16 | 17 | """ 18 | badges = [] 19 | for data in badge_data: 20 | badges.append(update_badge(data, overwrite=overwrite)) 21 | return badges 22 | 23 | 24 | def update_badge(data_in, overwrite=False): 25 | # Clone the data, because we might delete fields 26 | data = dict(**data_in) 27 | 28 | # If there are prerequisites, ensure they're real badges and remove 29 | # from the set of data fields. 30 | if 'prerequisites' not in data: 31 | prerequisites = None 32 | else: 33 | prerequisites = [get_badge(n) 34 | for n in data.get('prerequisites', [])] 35 | del data['prerequisites'] 36 | 37 | badge, created = Badge.objects.get_or_create(title=data['title'], 38 | defaults=data) 39 | 40 | # If overwriting, and not just created, then save with current fields. 41 | if overwrite and not created: 42 | for k, v in data.items(): 43 | setattr(badge, k, v) 44 | badge.save() 45 | 46 | # Set prerequisites if overwriting, or badge is newly created. 47 | if (overwrite or created) and prerequisites: 48 | badge.prerequisites.clear() 49 | badge.prerequisites.add(*prerequisites) 50 | 51 | return badge 52 | 53 | 54 | def get_badge(slug_or_badge): 55 | """Return badge specified by slug or by instance 56 | 57 | :arg slug_or_badge: slug or Badge instance 58 | 59 | :returns: Badge instance 60 | 61 | """ 62 | if isinstance(slug_or_badge, Badge): 63 | b = slug_or_badge 64 | else: 65 | b = Badge.objects.get(slug=slug_or_badge) 66 | return b 67 | 68 | 69 | def award_badge(slug_or_badge, awardee, awarder=None): 70 | """Award a badge to an awardee, with optional awarder 71 | 72 | :arg slug_or_badge: slug or Badge instance to award 73 | :arg awardee: User this Badge is awarded to 74 | :arg awarder: User who awarded this Badge 75 | 76 | :returns: Award instance 77 | 78 | :raise BadgeAwardNotAllowedexception: ? 79 | 80 | :raise BadgeAlreadyAwardedException: if the badge is unique and 81 | has already been awarded to this user 82 | 83 | """ 84 | b = get_badge(slug_or_badge) 85 | return b.award_to(awardee=awardee, awarder=awarder) 86 | 87 | 88 | def get_progress(slug_or_badge, user): 89 | """Get a progress record for a badge and awardee 90 | 91 | :arg slug_or_badge: slug or Badge instance 92 | :arg user: User to check progress for 93 | 94 | :returns: Progress instance 95 | 96 | """ 97 | b = get_badge(slug_or_badge) 98 | return b.progress_for(user) 99 | -------------------------------------------------------------------------------- /badger/validate_jsonp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # see also: http://github.com/tav/scripts/raw/master/validate_jsonp.py 3 | # Placed into the Public Domain by tav 4 | 5 | """Validate Javascript Identifiers for use as JSON-P callback parameters.""" 6 | 7 | import re 8 | 9 | from unicodedata import category 10 | 11 | # ------------------------------------------------------------------------------ 12 | # javascript identifier unicode categories and "exceptional" chars 13 | # ------------------------------------------------------------------------------ 14 | 15 | valid_jsid_categories_start = frozenset([ 16 | 'Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Nl' 17 | ]) 18 | 19 | valid_jsid_categories = frozenset([ 20 | 'Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Nl', 'Mn', 'Mc', 'Nd', 'Pc' 21 | ]) 22 | 23 | valid_jsid_chars = ('$', '_') 24 | 25 | # ------------------------------------------------------------------------------ 26 | # regex to find array[index] patterns 27 | # ------------------------------------------------------------------------------ 28 | 29 | array_index_regex = re.compile(r'\[[0-9]+\]$') 30 | 31 | has_valid_array_index = array_index_regex.search 32 | replace_array_index = array_index_regex.sub 33 | 34 | # ------------------------------------------------------------------------------ 35 | # javascript reserved words -- including keywords and null/boolean literals 36 | # ------------------------------------------------------------------------------ 37 | 38 | is_reserved_js_word = frozenset([ 39 | 40 | 'abstract', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class', 41 | 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'double', 42 | 'else', 'enum', 'export', 'extends', 'false', 'final', 'finally', 'float', 43 | 'for', 'function', 'goto', 'if', 'implements', 'import', 'in', 'instanceof', 44 | 'int', 'interface', 'long', 'native', 'new', 'null', 'package', 'private', 45 | 'protected', 'public', 'return', 'short', 'static', 'super', 'switch', 46 | 'synchronized', 'this', 'throw', 'throws', 'transient', 'true', 'try', 47 | 'typeof', 'var', 'void', 'volatile', 'while', 'with', 48 | 49 | # potentially reserved in a future version of the ES5 standard 50 | # 'let', 'yield' 51 | 52 | ]).__contains__ 53 | 54 | # ------------------------------------------------------------------------------ 55 | # the core validation functions 56 | # ------------------------------------------------------------------------------ 57 | 58 | 59 | def is_valid_javascript_identifier(identifier, escape=r'\u', ucd_cat=category): 60 | """Return whether the given ``id`` is a valid Javascript identifier.""" 61 | 62 | if not identifier: 63 | return False 64 | 65 | if not isinstance(identifier, unicode): 66 | try: 67 | identifier = unicode(identifier, 'utf-8') 68 | except UnicodeDecodeError: 69 | return False 70 | 71 | if escape in identifier: 72 | 73 | new = []; add_char = new.append 74 | split_id = identifier.split(escape) 75 | add_char(split_id.pop(0)) 76 | 77 | for segment in split_id: 78 | if len(segment) < 4: 79 | return False 80 | try: 81 | add_char(unichr(int('0x' + segment[:4], 16))) 82 | except Exception: 83 | return False 84 | add_char(segment[4:]) 85 | 86 | identifier = u''.join(new) 87 | 88 | if is_reserved_js_word(identifier): 89 | return False 90 | 91 | first_char = identifier[0] 92 | 93 | if not ((first_char in valid_jsid_chars) or 94 | (ucd_cat(first_char) in valid_jsid_categories_start)): 95 | return False 96 | 97 | for char in identifier[1:]: 98 | if not ((char in valid_jsid_chars) or 99 | (ucd_cat(char) in valid_jsid_categories)): 100 | return False 101 | 102 | return True 103 | 104 | 105 | def is_valid_jsonp_callback_value(value): 106 | """Return whether the given ``value`` can be used as a JSON-P callback.""" 107 | 108 | for identifier in value.split(u'.'): 109 | while '[' in identifier: 110 | if not has_valid_array_index(identifier): 111 | return False 112 | identifier = replace_array_index(u'', identifier) 113 | if not is_valid_javascript_identifier(identifier): 114 | return False 115 | 116 | return True 117 | 118 | # ------------------------------------------------------------------------------ 119 | # test 120 | # ------------------------------------------------------------------------------ 121 | 122 | 123 | def test(): 124 | """ 125 | The function ``is_valid_javascript_identifier`` validates a given identifier 126 | according to the latest draft of the ECMAScript 5 Specification: 127 | 128 | >>> is_valid_javascript_identifier('hello') 129 | True 130 | 131 | >>> is_valid_javascript_identifier('alert()') 132 | False 133 | 134 | >>> is_valid_javascript_identifier('a-b') 135 | False 136 | 137 | >>> is_valid_javascript_identifier('23foo') 138 | False 139 | 140 | >>> is_valid_javascript_identifier('foo23') 141 | True 142 | 143 | >>> is_valid_javascript_identifier('$210') 144 | True 145 | 146 | >>> is_valid_javascript_identifier(u'Stra\u00dfe') 147 | True 148 | 149 | >>> is_valid_javascript_identifier(r'\u0062') # u'b' 150 | True 151 | 152 | >>> is_valid_javascript_identifier(r'\u62') 153 | False 154 | 155 | >>> is_valid_javascript_identifier(r'\u0020') 156 | False 157 | 158 | >>> is_valid_javascript_identifier('_bar') 159 | True 160 | 161 | >>> is_valid_javascript_identifier('some_var') 162 | True 163 | 164 | >>> is_valid_javascript_identifier('$') 165 | True 166 | 167 | But ``is_valid_jsonp_callback_value`` is the function you want to use for 168 | validating JSON-P callback parameter values: 169 | 170 | >>> is_valid_jsonp_callback_value('somevar') 171 | True 172 | 173 | >>> is_valid_jsonp_callback_value('function') 174 | False 175 | 176 | >>> is_valid_jsonp_callback_value(' somevar') 177 | False 178 | 179 | It supports the possibility of '.' being present in the callback name, e.g. 180 | 181 | >>> is_valid_jsonp_callback_value('$.ajaxHandler') 182 | True 183 | 184 | >>> is_valid_jsonp_callback_value('$.23') 185 | False 186 | 187 | As well as the pattern of providing an array index lookup, e.g. 188 | 189 | >>> is_valid_jsonp_callback_value('array_of_functions[42]') 190 | True 191 | 192 | >>> is_valid_jsonp_callback_value('array_of_functions[42][1]') 193 | True 194 | 195 | >>> is_valid_jsonp_callback_value('$.ajaxHandler[42][1].foo') 196 | True 197 | 198 | >>> is_valid_jsonp_callback_value('array_of_functions[42]foo[1]') 199 | False 200 | 201 | >>> is_valid_jsonp_callback_value('array_of_functions[]') 202 | False 203 | 204 | >>> is_valid_jsonp_callback_value('array_of_functions["key"]') 205 | False 206 | 207 | Enjoy! 208 | 209 | """ 210 | 211 | if __name__ == '__main__': 212 | import doctest 213 | doctest.testmod() 214 | -------------------------------------------------------------------------------- /badger_example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/django-badger/07c28a8a1065ea7716f9988bf7541242a912fb1d/badger_example/__init__.py -------------------------------------------------------------------------------- /badger_example/badges.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.db.models import Sum 3 | from django.db.models.signals import post_save 4 | 5 | from .models import GuestbookEntry 6 | 7 | import badger 8 | import badger.utils 9 | from badger.utils import get_badge, award_badge, get_progress 10 | from badger.models import Badge, Award, Progress 11 | from badger.signals import badge_was_awarded 12 | 13 | 14 | badges = [ 15 | 16 | dict(slug="test-2", 17 | title="Test #2", 18 | description="Second badge"), 19 | 20 | dict(slug="awesomeness", 21 | title="Awesomeness (you have it)", 22 | description="Badge with a slug not derived from title."), 23 | 24 | dict(slug="250-words", 25 | title="250 Words", 26 | description="You've posted 250 words to my guestbook!"), 27 | 28 | dict(slug="250-words-by-percent", 29 | title="100% of 250 Words", 30 | description="You've posted 100% of 250 words to my guestbook!"), 31 | 32 | ] 33 | 34 | 35 | def update_badges(overwrite=False): 36 | badges = [ 37 | dict(slug="master-badger", 38 | title="Master Badger", 39 | description="You've collected all badges", 40 | prerequisites=('test-1', 'test-2', 'awesomeness', 41 | 'button-clicker')), 42 | ] 43 | 44 | return badger.utils.update_badges(badges, overwrite) 45 | 46 | 47 | def on_guestbook_post(sender, **kwargs): 48 | o = kwargs['instance'] 49 | created = kwargs['created'] 50 | 51 | if created: 52 | award_badge('first-post', o.creator) 53 | 54 | # Increment progress counter and track the completion condition ourselves. 55 | b = get_badge('250-words') 56 | p = b.progress_for(o.creator).increment_by(o.word_count) 57 | if p.counter >= 250: 58 | b.award_to(o.creator) 59 | 60 | # Update percentage from total word count, and Progress will award on 100% 61 | total_word_count = (GuestbookEntry.objects.filter(creator=o.creator) 62 | .aggregate(s=Sum('word_count'))['s']) 63 | (get_progress("250-words-by-percent", o.creator) 64 | .update_percent(total_word_count, 250)) 65 | 66 | 67 | def on_badge_award(sender, signal, award, **kwargs): 68 | pass 69 | 70 | 71 | def register_signals(): 72 | post_save.connect(on_guestbook_post, sender=GuestbookEntry) 73 | badge_was_awarded.connect(on_badge_award, sender=Badge) 74 | -------------------------------------------------------------------------------- /badger_example/fixtures/badger_example_badges.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": "1", 4 | "model": "badger.badge", 5 | "fields": { 6 | "title": "Test #1", 7 | "slug": "test-1", 8 | "description": "This is the first test badge", 9 | "created": "2010-12-01 00:00:00", 10 | "modified": "2010-12-01 00:00:00" 11 | } 12 | }, 13 | { 14 | "pk": "2", 15 | "model": "badger.badge", 16 | "fields": { 17 | "title": "Button Clicker", 18 | "slug": "button-clicker", 19 | "description": "You clicked the button 10 times!", 20 | "created": "2010-12-01 00:00:00", 21 | "modified": "2010-12-01 00:00:00" 22 | } 23 | }, 24 | { 25 | "pk": "3", 26 | "model": "badger.badge", 27 | "fields": { 28 | "title": "First post!", 29 | "slug": "first-post", 30 | "unique": true, 31 | "description": "You made the first post!", 32 | "created": "2010-12-01 00:00:00", 33 | "modified": "2010-12-01 00:00:00" 34 | } 35 | } 36 | ] 37 | -------------------------------------------------------------------------------- /badger_example/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | 4 | from django.template.defaultfilters import slugify 5 | 6 | 7 | class GuestbookEntry(models.Model): 8 | """Representation of a guestbook entry""" 9 | message = models.TextField(blank=True) 10 | creator = models.ForeignKey(User, blank=False) 11 | created = models.DateTimeField(auto_now_add=True, blank=False) 12 | modified = models.DateTimeField(auto_now=True, blank=False) 13 | word_count = models.IntegerField(default=0, blank=True) 14 | 15 | def save(self, *args, **kwargs): 16 | self.word_count = len(self.message.split(' ')) 17 | super(GuestbookEntry, self).save(*args, **kwargs) 18 | -------------------------------------------------------------------------------- /badger_example/templates/badger/award_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "badger/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load badger_tags %} 5 | 6 | {% block pageid %}award_delete{% endblock %} 7 | 8 | {% block content %} 9 | 12 |
        13 | 14 |
        15 | {% include "badger/includes/badge_full.html" %} 16 |
        17 | 18 |
        19 | 20 |
        21 | {{ csrf }} 22 |

        {{ _("Delete this award, are you sure?") }}

        23 |
          24 |
        • 25 |
        26 |
        27 | 28 | {% include "badger/includes/award_full.html" %} 29 |
        30 | 31 |
        32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /badger_example/templates/badger/award_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "badger/base.html" %} 2 | {% load url from future %} 3 | 4 | {% block pageid %}award_detail{% endblock %} 5 | 6 | {% block extrahead %} 7 | 10 | {% endblock %} 11 | 12 | {% block content %} 13 | 14 |

        Award detail

        15 | 16 |
        17 | {% include "badger/includes/award_full.html" %} 18 |
        19 | 20 |
        21 |

        Badge

        22 | {% include "badger/includes/badge_full.html" %} 23 |
        24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /badger_example/templates/badger/awards_by_badge.html: -------------------------------------------------------------------------------- 1 | {% extends "badger/base.html" %} 2 | {% load url from future %} 3 | 4 | {% block pageid %}badge_awards_by_badge{% endblock %} 5 | 6 | {% block extrahead %} 7 | 10 | {% endblock %} 11 | 12 | {% block content %} 13 | 14 |

        Badge detail for {{badge.title}}

        15 | 16 | {% include "badger/includes/badge_full.html" %} 17 | 18 |

        Awards

        19 | 20 |
          21 | {% for award in awards %} 22 |
        • 23 | {% include "badger/includes/award_as_user.html" %} 24 |
        • 25 | {% endfor %} 26 |
        27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /badger_example/templates/badger/awards_by_user.html: -------------------------------------------------------------------------------- 1 | {% extends "badger/base.html" %} 2 | {% load url from future %} 3 | 4 | {% block pageid %}badge_awards_by_user{% endblock %} 5 | 6 | {% block extrahead %} 7 | 10 | {% endblock %} 11 | 12 | {% block content %} 13 |
        14 |

        Awards for {{ user }}

        15 | {% include "badger/includes/awards_as_badges_list.html" %} 16 |
        17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /badger_example/templates/badger/awards_list.html: -------------------------------------------------------------------------------- 1 | {% extends "badger/base.html" %} 2 | {% load url from future %} 3 | 4 | {% block pageid %}awards{% endblock %} 5 | 6 | {% block extrahead %} 7 | 10 | {% endblock %} 11 | 12 | {% block content %} 13 | 14 |
        15 | {% if badge %} 16 |

        Awards for {{ badge }}

        17 | {% else %} 18 |

        Recent Awards

        19 | {% endif %} 20 | {% include "badger/includes/awards_list.html" %} 21 |
        22 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /badger_example/templates/badger/badge_award.html: -------------------------------------------------------------------------------- 1 | {% extends "badger/base.html" %} 2 | 3 | {% block pageid %}badge_award{% endblock %} 4 | 5 | {% block content %} 6 |

        Award a badge

        7 |
        8 | {{ csrf }} 9 |
          10 | {{ form.as_ul }} 11 |
        • 12 |
        13 |
        14 | 15 |
        16 |

        Badge

        17 | {% include "badger/includes/badge_full.html" %} 18 |
        19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /badger_example/templates/badger/badge_create.html: -------------------------------------------------------------------------------- 1 | {% extends "badger/base.html" %} 2 | 3 | {% block pageid %}badge_create{% endblock %} 4 | 5 | {% block content %} 6 |
        7 |
        8 |

        Create a badge

        9 |
        10 |
        11 | {{ csrf }} 12 |
          13 | {{ form.as_ul }} 14 |
        • 15 |
        16 |
        17 |
        18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /badger_example/templates/badger/badge_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "badger/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load badger_tags %} 5 | 6 | {% block pageid %}badge_delete{% endblock %} 7 | 8 | {% block content %} 9 | 10 |
        11 | 14 | {% include "badger/includes/badge_full.html" %} 15 |
        16 | 17 |
        18 | 21 | 22 |
        23 | {{ csrf }} 24 | {% if awards_count %} 25 |

        {% blocktrans with badge_url=badge.get_absolute_url awards_count=awards_count %} 26 | Note: 27 | Deleting this badge will also delete 28 | {{awards_count}} award(s) 29 | {% endblocktrans %}

        30 | {% else %} 31 |

        {% blocktrans %} 32 | Note: 33 | No one has been awarded this badge, yet. 34 | {% endblocktrans %}

        35 | {% endif %} 36 |

        {{ _("Are you sure?") }}

        37 |
          38 |
        • 39 |
        40 |
        41 |
        42 | 43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /badger_example/templates/badger/badge_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "badger/base.html" %} 2 | {% load url from future %} 3 | {% load badger_tags %} 4 | 5 | {% block pageid %}badge_detail{% endblock %} 6 | 7 | {% block extrahead %} 8 | 11 | 14 | {% endblock %} 15 | 16 | {% block content %} 17 | 18 |
        19 |

        Badge detail

        20 | {% include "badger/includes/badge_full.html" %} 21 |
        22 | 23 | {% if award_list %} 24 |
        25 |

        Recent awards

        26 | 32 | 42 |
        43 | {% endif %} 44 | 45 | {% endblock %} 46 | -------------------------------------------------------------------------------- /badger_example/templates/badger/badge_edit.html: -------------------------------------------------------------------------------- 1 | {% extends "badger/base.html" %} 2 | 3 | {% block pageid %}badge_edit{% endblock %} 4 | 5 | {% block content %} 6 | 7 |
        8 | 11 | 12 |
        14 | {{ csrf }} 15 |
          16 | {{ form.as_ul }} 17 |
        • 18 |
        19 |
        20 |
        21 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /badger_example/templates/badger/badge_nominate_for.html: -------------------------------------------------------------------------------- 1 | {% extends "badger/base.html" %} 2 | 3 | {% block pageid %}badge_nominate{% endblock %} 4 | 5 | {% block content %} 6 | 7 |
        8 | 9 |
        10 | 13 | {% include "badger/includes/badge_full.html" %} 14 |
        15 | 16 |
        17 | 20 | 21 |
        22 | {{ csrf }} 23 |
          24 | {{ form.as_ul }} 25 |
        • 26 |
        27 |
        28 |
        29 | 30 |
        31 | 32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /badger_example/templates/badger/badges_by_user.html: -------------------------------------------------------------------------------- 1 | {% extends "badger/base.html" %} 2 | 3 | {% block pageid %}badges_by_user{% endblock %} 4 | 5 | {% block extrahead %} 6 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 |
        13 |

        {{ _("Badges created by {user}") | f(user=user) }}

        14 | {% include "badger/includes/badges_list.html" %} 15 |
        16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /badger_example/templates/badger/badges_list.html: -------------------------------------------------------------------------------- 1 | {% extends "badger/base.html" %} 2 | 3 | {% block pageid %}badges{% endblock %} 4 | 5 | {% block content %} 6 |
        7 |

        Badges

        8 | {% include "badger/includes/badges_list.html" %} 9 |
        10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /badger_example/templates/badger/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block extra_styles %} 4 | 5 | 6 | {% endblock extra_styles %} 7 | 8 | -------------------------------------------------------------------------------- /badger_example/templates/badger/claim_deferred_award.html: -------------------------------------------------------------------------------- 1 | {% extends "badger/base.html" %} 2 | 3 | {% load badger_tags %} 4 | {% load i18n %} 5 | 6 | {% block pageid %}claim_badge{% endblock %} 7 | 8 | {% block content %} 9 | 10 |
        11 | 14 | {% include "badger/includes/badge_full.html" %} 15 |
        16 | 17 |
        18 | 19 | 22 | 23 |
        24 | {% if deferred_award.email %} 25 |
        Email address:
        26 |
        27 | {% endif %} 28 |
        Claim code:
        29 |
        {{ deferred_award.claim_code }}
        30 |
        31 | 32 | {% if user.is_anonymous %} 33 |
        34 |

        {{ _("Claim") }}

        35 |

        {{ _("Sign in to claim this badge award") }}

        36 |
        37 | {% endif %} 38 | 39 | {% if deferred_award|permissions_for:request.user|key:'grant_by' %} 40 |
        41 | 42 | {{ csrf }} 43 |

        {{ _("Grant") }}

        44 |

        {{ _("You can grant this award to someone else") }} 45 | {{ grant_form.as_p }} 46 | 47 |

        48 | {% endif %} 49 | 50 | {% if deferred_award|permissions_for:request.user|key:'claim_by' %} 51 |
        52 | {{ csrf }} 53 |

        {{ _("Claim") }}

        54 | {% if deferred_award.email and request.user.email != deferred_award.email %} 55 | {# This isn't enforced, since an awardee might have multiple email addresses #} 56 | {% blocktrans %} 57 |

        Note: 58 | This award may not be intended for you. 59 | Your email address does not match this award claim. 60 |

        61 | {% endblocktrans %} 62 | {% else %} 63 |

        {{ _("You can claim this award for yourself") }}

        64 | {% endif %} 65 | 66 |
        67 | {% endif %} 68 | 69 |
        70 | 71 | {% endblock %} 72 | -------------------------------------------------------------------------------- /badger_example/templates/badger/claims_list.html: -------------------------------------------------------------------------------- 1 | {% extends "badger/base.html" %} 2 | 3 | {% block pageid %}claims_list{% endblock %} 4 | 5 | {% block extrahead %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
        10 | 11 |
        12 | 15 |
          16 | {% for deferred_award in deferred_awards %} 17 |
        • 18 |

          {{ badge.title }}

          19 | 20 | {% set self_url = request.build_absolute_uri(deferred_award.get_claim_url()) %} 21 | {{ qr_code_image(self_url, alt="Scan to claim!", size="200") }} 22 | {{ deferred_award.claim_code }} 23 | {{ request.build_absolute_uri('/') }} 24 |
        • 25 | {% endfor %} 26 |
        27 |
        28 | 29 |
        30 | 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /badger_example/templates/badger/deferred_award_body.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans with domain=current_site.domain badge_url=badge.get_absolute_url badge_title=badge.title claim_url=deferred_award.get_claim_url protocol=protocol|default:"http" %} 3 | 4 | You have received an award for the badge {{ badge_title }}. 5 | 6 | You can claim it, here: 7 | 8 | {{ protocol }}://{{ domain }}{{ claim_url }} 9 | 10 | {% endblocktrans %} 11 | -------------------------------------------------------------------------------- /badger_example/templates/badger/deferred_award_subject.txt: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {% blocktrans with site_name=current_site.name badge_title=badge.title %}[{{ site_name }}] Claim your award for the badge "{{ badge_title }}"{% endblocktrans %} 3 | -------------------------------------------------------------------------------- /badger_example/templates/badger/home.html: -------------------------------------------------------------------------------- 1 | {% extends "badger/base.html" %} 2 | {% load url from future %} 3 | 4 | {% block pageid %}home{% endblock %} 5 | 6 | {% block extrahead %} 7 | 10 | 13 | {% endblock %} 14 | 15 | {% block content %} 16 | 17 | {% if request.user.is_authenticated() %} 18 |
        19 |

        Things to Do

        20 | 23 |
        24 | {% endif %} 25 | 26 |
        27 |

        Recent badges

        28 | 34 | {% include "badger/includes/badges_list.html" %} 35 |
        36 | 37 |
        38 |

        Recent awards

        39 | 45 | {% include "badger/includes/awards_list.html" %} 46 |
        47 | 48 | {% endblock %} 49 | -------------------------------------------------------------------------------- /badger_example/templates/badger/includes/award_as_badge.html: -------------------------------------------------------------------------------- 1 | {% set badge = award.badge %} 2 |
        3 | {% include "badger/includes/badge_full.html" %} 4 | Award details 5 |
        6 | -------------------------------------------------------------------------------- /badger_example/templates/badger/includes/award_as_user.html: -------------------------------------------------------------------------------- 1 | {% set user = award.user %} 2 |
        3 | {{ user }} 4 |
        5 | -------------------------------------------------------------------------------- /badger_example/templates/badger/includes/award_full.html: -------------------------------------------------------------------------------- 1 | {% load url from future %} 2 | {% load badger_tags %} 3 | 4 |
        5 |
        Awarded to:
        6 |
        {{ award.user }}
        8 | {% if award.description %} 9 |
        Explanation:
        10 |
        {{ award.description }}
        11 | {% endif %} 12 | {% if award.nomination %} 13 |
        Nominated by:
        14 |
        {{ award.nomination.creator }}
        16 |
        Nomination approved by:
        17 | {% if award.badge.nominations_autoapproved %} 18 |
        (auto-approved)
        19 | {% elif award.nomination.approver %} 20 |
        {{ award.nomination.approver }}
        22 | {% endif %} 23 | {% else %} 24 |
        Awarded by:
        25 |
        {{ award.creator }}
        27 | {% endif %} 28 |
        Issued on:
        29 |
        {{ award.created }}
        30 |
        31 | {% if award|permissions_for:request.user|key:'delete_by' %} 32 |
        33 |
        Actions:
        34 |
        37 |
        38 | {% endif %} 39 | -------------------------------------------------------------------------------- /badger_example/templates/badger/includes/awards_as_badges_list.html: -------------------------------------------------------------------------------- 1 | {% load badger_tags %} 2 | 13 | {% include "badger/includes/pagination.html" %} 14 | -------------------------------------------------------------------------------- /badger_example/templates/badger/includes/awards_list.html: -------------------------------------------------------------------------------- 1 | {% load badger_tags %} 2 | 3 | 24 | {% include "badger/includes/pagination.html" %} 25 | -------------------------------------------------------------------------------- /badger_example/templates/badger/includes/badge_full.html: -------------------------------------------------------------------------------- 1 | {% load url from future %} 2 | {% load badger_tags %} 3 | 4 | {% if not award %} 5 |
          6 | {% if badge|permissions_for:request.user|key:'edit_by' %} 7 |
        • {{ _('Edit badge') }}
        • 8 | {% endif %} 9 | {% if badge|permissions_for:request.user|key:'delete_by' %} 10 |
        • {{ _('Delete badge') }}
        • 11 | {% endif %} 12 |
        13 | {% endif %} 14 | 15 |
        16 | 17 |
        18 |
        Title:
        19 |
        {{ badge.title }}
        20 | {% if award and award.image %} 21 |
        Image:
        22 |
        23 | {% else %} 24 | {% if badge.image %} 25 |
        Image:
        26 |
        27 | {% endif %} 28 | {% endif %} 29 | {% if badge.description %} 30 |
        Description:
        31 |
        {{ badge.description }}
        32 | {% endif %} 33 |
        Creator:
        34 |
        {{ badge.creator }}
        35 |
        Created:
        36 |
        {{ badge.created }}
        37 |
        Modified:
        38 |
        {{ badge.modified }}
        39 |
        Actions:
        40 |
          41 | {% user_award_list badge request.user %} 42 | 43 |
        44 |
        45 | -------------------------------------------------------------------------------- /badger_example/templates/badger/includes/badges_list.html: -------------------------------------------------------------------------------- 1 | 17 | {% include "badger/includes/pagination.html" %} 18 | -------------------------------------------------------------------------------- /badger_example/templates/badger/includes/pagination.html: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /badger_example/templates/badger/manage_claims.html: -------------------------------------------------------------------------------- 1 | {% extends "badger/base.html" %} 2 | 3 | {% block pageid %}manage_claims{% endblock %} 4 | 5 | {% block extrahead %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
        10 | 11 |
        12 | 15 | {% include "badger/includes/badge_full.html" %} 16 |
        17 | 18 |
        19 | 22 |
        23 | {{ csrf() }} 24 | 25 | 31 | 32 |
        33 | 62 |
        63 | 64 |
        65 | 66 | 67 | {% endblock %} 68 | -------------------------------------------------------------------------------- /badger_example/templates/badger/nomination_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "badger/base.html" %} 2 | 3 | {% block pageid %}nomination_detail{% endblock %} 4 | 5 | {% block content %} 6 |
        7 | 8 |
        9 | 12 | {% include "badger/includes/badge_full.html" %} 13 |
        14 | 15 |
        16 | 19 |
        20 | {% set show_approve = not nomination.is_approved and nomination.allows_approve_by(request.user) %} 21 | {% set show_accept = nomination.is_approved and not nomination.is_accepted and nomination.allows_accept(request.user) %} 22 | {% set show_reject = nomination.allows_reject_by(request.user) %} 23 | {% if show_approve or show_accept %} 24 |
        {{ _("Actions:") }}
          25 | {% if show_approve %} 26 |
        • 27 | {{ csrf() }} 28 | 29 | 30 |
        • 31 | {% endif %} 32 | {% if show_accept %} 33 |
        • 34 | {{ csrf() }} 35 | 36 | 43 |
        • 44 | {% endif %} 45 | {% if show_approve %} 46 |
        • 47 | {{ csrf() }} 48 | 49 | 50 |
        • 51 | {% endif %} 52 |
        53 | {% endif %} 54 | {% if nomination.award %} 55 |
        {{ _("Award:") }}
        56 |
        {{ nomination.award }}
        57 | {% endif %} 58 |
        {{ _("Nominee:") }}
        59 |
        {{ nomination.nominee }}{{ nomination.nominee }}
        60 |
        {{ _("Accepted?:") }}
        61 |
        {{ nomination.accepted and _("Yes") or _("No") }}
        62 |
        {{ _("Nominator:") }}
        63 |
        {{ nomination.creator }}{{ nomination.creator }}
        64 | {% if nomination.approver %} 65 |
        {{ _("Approver:") }}
        66 |
        {{ nomination.approver }}{{ nomination.approver }}
        67 | {% else %} 68 |
        {{ _("Approved?:") }}
        69 |
        {{ _("No") }}
        70 | {% endif %} 71 | {% if nomination.rejected_by %} 72 |
        {{ _("Rejected by:") }}
        73 |
        {{ nomination.rejected_by }}{{ nomination.rejected_by }}
        74 | {% if nomination.rejected_reason %} 75 |
        {{ _("Rejected reason:") }}
        76 |
        {{ nomination.rejected_reason }}
        77 | {% endif %} 78 | {% endif %} 79 |
        {{ _("Created:") }}
        80 |
        {{ nomination.created }}
        81 |
        {{ _("Modified:") }}
        82 |
        {{ nomination.modified }}
        83 |
        84 |
        85 | 86 |
        87 | 88 | {% endblock %} 89 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/award_accepted/full.html: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/award_accepted/full.txt: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/award_accepted/notice.html: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/award_accepted/short.txt: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/award_declined/full.html: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/award_declined/full.txt: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/award_declined/notice.html: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/award_declined/short.txt: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/award_received/full.html: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/award_received/full.txt: -------------------------------------------------------------------------------- 1 | {% set badge_title = award.badge.title %} 2 | {% set protocol = (protocol or 'http') %} 3 | {% set domain = current_site.domain %} 4 | {% set award_url = award.get_absolute_url() %} 5 | {% trans %}You have received an award for the badge "{{ badge_title }}".{% endtrans %} 6 | 7 | 8 | {% trans %}Check out the details, here:{% endtrans %} 9 | 10 | {{ protocol }}://{{ domain }}{{ award_url }} 11 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/award_received/notice.html: -------------------------------------------------------------------------------- 1 | {% trans badge_url=award.badge.get_absolute_url(), 2 | badge_title=award.badge.title, 3 | award_url=award.get_absolute_url(), 4 | user_name=award.user, 5 | user_url=award.user.get_absolute_url() %} 6 | You were awarded the badge 7 | {{ badge_title }}. 8 | {% endtrans %} 9 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/award_received/short.txt: -------------------------------------------------------------------------------- 1 | {% trans badge_title=award.badge.title %}Award received: {{ badge_title }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/badge_awarded/full.html: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/badge_awarded/full.txt: -------------------------------------------------------------------------------- 1 | {% set badge_title=award.badge.title %} 2 | {% set user_name=award.user %} 3 | {% set domain=current_site.domain %} 4 | {% set protocol=(protocol or 'http') %} 5 | {% set award_url=award.get_absolute_url() %} 6 | {% trans %}The badge "{{ badge_title }}" was awarded to {{ user_name }}.{% endtrans %} 7 | 8 | 9 | {% trans %}Check out the details, here:{% endtrans %} 10 | 11 | {{ protocol }}://{{ domain }}{{ award_url }} 12 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/badge_awarded/notice.html: -------------------------------------------------------------------------------- 1 | {% trans badge_url=award.badge.get_absolute_url(), 2 | badge_title=award.badge.title, 3 | award_url=award.get_absolute_url(), 4 | user_name=award.user, 5 | user_url=award.user.get_absolute_url() %} 6 | 7 | {{ badge_title }} was 8 | awarded to 9 | {{ user_name }}. 10 | 11 | {% endtrans %} 12 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/badge_awarded/short.txt: -------------------------------------------------------------------------------- 1 | {% trans badge_title=award.badge.title %}Badge awarded: {{ badge_title }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/badge_edited/full.html: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/badge_edited/full.txt: -------------------------------------------------------------------------------- 1 | {% set domain=current_site.domain %} 2 | {% set protocol=(protocol or 'http') %} 3 | {% set badge_url=badge.get_absolute_url() %} 4 | {% set badge_title=badge.title %} 5 | {% trans %}The badge "{{ badge_title }}" was edited.{% endtrans %} 6 | 7 | 8 | {% trans %}Review the changes here:{% endtrans %} 9 | 10 | {{ protocol }}://{{ domain }}{{ badge_url }} 11 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/badge_edited/notice.html: -------------------------------------------------------------------------------- 1 | {% trans badge_url=badge.get_absolute_url(), 2 | badge_title=badge.title %} 3 | 4 | {{ badge_title }} was edited. 5 | 6 | {% endtrans %} 7 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/badge_edited/short.txt: -------------------------------------------------------------------------------- 1 | {% trans badge_title=badge.title %}Badge edited: {{ badge_title }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {# Broken for now, and seems not-so-useful because needs auth 4 | {% block extrahead %} 5 | 7 | {% endblock %} 8 | #} 9 | 10 | {% block bodyclass %}notices{% endblock %} 11 | 12 | {# 13 | {% block subnav %} 14 | {% if user.is_authenticated %} 15 | 19 | {% else %} 20 |   21 | {% endif %} 22 | {% endblock %} 23 | #} 24 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/email_body.txt: -------------------------------------------------------------------------------- 1 | {{ message|safe }} 2 | 3 | -- 4 | {% trans %}To see other notices or change how you receive notifications, 5 | please go to {{ notices_url }}{% endtrans %} 6 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/email_subject.txt: -------------------------------------------------------------------------------- 1 | {% trans site_name=current_site.name %}[{{ site_name }}] {{ message }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/full.html: -------------------------------------------------------------------------------- 1 | {% load i18n %}{% blocktrans %}{{ notice }}{% endblocktrans %} -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/full.txt: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_accepted/full.html: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_accepted/full.txt: -------------------------------------------------------------------------------- 1 | {% set domain=current_site.domain %} 2 | {% set protocol=(protocol or 'http') %} 3 | {% set badge_title=nomination.badge.title %} 4 | {% set nomination_url=nomination.get_absolute_url() %} 5 | {% set user_name=nomination.nominee %} 6 | {% trans %}{{ user_name }} accepted a nomination for the badge "{{ badge_title }}".{% endtrans %} 7 | 8 | 9 | {% trans %}Check out the details, here:{% endtrans %} 10 | 11 | {{ protocol }}://{{ domain }}{{ nomination_url }} 12 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_accepted/notice.html: -------------------------------------------------------------------------------- 1 | {% trans domain=current_site.domain, 2 | badge_url=nomination.badge.get_absolute_url(), 3 | badge_title=nomination.badge.title, 4 | nomination_url=nomination.get_absolute_url(), 5 | user_name=nomination.nominee, 6 | user_url=nomination.nominee.get_absolute_url() %} 7 | 8 | {{ user_name }} accepted a 9 | nomination for the badge 10 | {{ badge_title }}. 11 | 12 | {% endtrans %} 13 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_accepted/short.txt: -------------------------------------------------------------------------------- 1 | {% trans badge_title=nomination.badge.title %}Nomination accepted: {{ badge_title }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_approved/full.html: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_approved/full.txt: -------------------------------------------------------------------------------- 1 | {% set approver_user_name=nomination.approver %} 2 | {% set user_name=nomination.nominee %} 3 | {% set badge_title=nomination.badge.title %} 4 | {% set domain=current_site.domain %} 5 | {% set protocol=(protocol or 'http') %} 6 | {% set nomination_url=nomination.get_absolute_url() %} 7 | {% trans %}{{ approver_user_name }} approved the nomination of {{ user_name }} 8 | for the badge "{{ badge_title }}".{% endtrans %} 9 | 10 | 11 | {% trans %}Check out the details, here:{% endtrans %} 12 | 13 | {{ protocol }}://{{ domain }}{{ nomination_url }} 14 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_approved/notice.html: -------------------------------------------------------------------------------- 1 | {% trans domain=current_site.domain, 2 | badge_url=nomination.badge.get_absolute_url(), 3 | badge_title=nomination.badge.title, 4 | nomination_url=nomination.get_absolute_url(), 5 | user_name=nomination.nominee, 6 | user_url=nomination.nominee.get_absolute_url(), 7 | approver_user_name=nomination.approver, 8 | approver_url=nomination.approver.get_absolute_url() %} 9 | 10 | {{ approver_user_name }} approved the 11 | nomination of 12 | {{ user_name }} for the badge 13 | {{ badge_title }}. 14 | 15 | {% endtrans %} 16 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_approved/short.txt: -------------------------------------------------------------------------------- 1 | {% trans badge_title=nomination.badge.title %}Nomination approved: {{ badge_title }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_declined/full.html: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_declined/full.txt: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_declined/notice.html: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_declined/short.txt: -------------------------------------------------------------------------------- 1 | {% trans badge_title=nomination.badge.title %}Nomination declined: {{ badge_title }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_received/full.html: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_received/full.txt: -------------------------------------------------------------------------------- 1 | {% set domain=current_site.domain %} 2 | {% set protocol=(protocol or 'http') %} 3 | {% set badge_url=nomination.badge.get_absolute_url() %} 4 | {% set badge_title=nomination.badge.title %} 5 | {% set nomination_url=nomination.get_absolute_url() %} 6 | {% set user_name=nomination.nominee %} 7 | {% set user_url=nomination.nominee.get_absolute_url() %} 8 | {% trans %}You were nominated for the badge "{{ badge_title }}".{% endtrans %} 9 | 10 | 11 | {% trans %}You can accept the nomination here:{% endtrans %} 12 | 13 | {{ protocol }}://{{ domain }}{{ nomination_url }} 14 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_received/notice.html: -------------------------------------------------------------------------------- 1 | {% trans badge_url=nomination.badge.get_absolute_url(), 2 | badge_title=nomination.badge.title, 3 | nomination_url=nomination.get_absolute_url(), 4 | user_name=nomination.nominee, 5 | user_url=nomination.nominee.get_absolute_url() %} 6 | 7 | You were nominated 8 | for the badge {{ badge_title }}. 9 | Want to accept? 10 | 11 | {% endtrans %} 12 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_received/short.txt: -------------------------------------------------------------------------------- 1 | {% trans badge_title=nomination.badge.title %}Nomination received: {{ badge_title }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_rejected/full.html: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_rejected/full.txt: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_rejected/notice.html: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_rejected/short.txt: -------------------------------------------------------------------------------- 1 | {% trans badge_title=nomination.badge.title %}Nomination rejected: {{ badge_title }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_submitted/full.html: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_submitted/full.txt: -------------------------------------------------------------------------------- 1 | {% set domain=current_site.domain %} 2 | {% set protocol=(protocol or 'http') %} 3 | {% set badge_title=nomination.badge.title %} 4 | {% set nomination_url=nomination.get_absolute_url() %} 5 | {% set user_name=nomination.nominee %} 6 | {% set creator_user_name=nomination.creator %} 7 | {% trans %}{{ user_name }} was nominated by {{ creator_user_name }} for the 8 | badge "{{ badge_title }}".{% endtrans %} 9 | 10 | 11 | {% trans %}Check out the details, here:{% endtrans %} 12 | 13 | {{ protocol }}://{{ domain }}{{ nomination_url }} 14 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_submitted/notice.html: -------------------------------------------------------------------------------- 1 | {% if nomination.creator %} 2 | {% trans domain=current_site.domain, 3 | badge_url=nomination.badge.get_absolute_url(), 4 | badge_title=nomination.badge.title, 5 | nomination_url=nomination.get_absolute_url(), 6 | user_name=nomination.nominee, 7 | user_url=nomination.nominee.get_absolute_url(), 8 | creator_user_name=nomination.creator, 9 | creator_url=nomination.creator.get_absolute_url() %} 10 | 11 | {{ user_name }} 12 | was nominated 13 | by {{ creator_user_name }} 14 | for the badge {{ badge_title }}. 15 | Want to approve it? 16 | 17 | {% endtrans %} 18 | {% else %} 19 | {% trans domain=current_site.domain, 20 | badge_url=nomination.badge.get_absolute_url(), 21 | badge_title=nomination.badge.title, 22 | nomination_url=nomination.get_absolute_url(), 23 | user_name=nomination.nominee, 24 | user_url=nomination.nominee.get_absolute_url() %} 25 | 26 | {{ user_name }} 27 | was nominated 28 | for the badge {{ badge_title }}. 29 | Want to approve it? 30 | 31 | {% endtrans %} 32 | {% endif %} 33 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/nomination_submitted/short.txt: -------------------------------------------------------------------------------- 1 | {% trans badge_title=nomination.badge.title %}Nomination submitted: {{ badge_title }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/notice.html: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/notice_settings.html: -------------------------------------------------------------------------------- 1 | {% extends "notification/base.html" %} 2 | 3 | {% block headtitle %}{{ _("Notification Settings") }}{% endblock %} 4 | 5 | {% block content %} 6 | 7 |
        8 | 11 | 12 |

        13 | {{ _("Primary email") }}:
        14 | {{ user.email }}
        15 |

        16 | 17 |
        18 | {{ csrf() }} 19 | 20 | 21 | 22 | {% for header in notice_settings.column_headers %} 23 | 24 | {% endfor %} 25 | 26 | {% for row in notice_settings.rows %} 27 | 28 | 31 | {% for cell in row.cells %} 32 | 35 | {% endfor %} 36 | 37 | {% endfor %} 38 | 39 | 40 | 41 |
        {{ _("Notification Type") }}{{ header }}
        {{ _(row.notice_type.display) }}
        29 | {{ _(row.notice_type.description) }} 30 |
        33 | 34 |
        42 |
        43 |
        44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/notices.html: -------------------------------------------------------------------------------- 1 | {% extends "notification/base.html" %} 2 | 3 | {% block headtitle %}{{ _("Notices") }}{% endblock %} 4 | 5 | {% block content %} 6 |
        7 | 16 | 17 |
          18 | {% if notices %} 19 | {% for notice in notices[:50] %}{# TODO: Paginate #} 20 |
        • 21 | {{ notice.added }} 22 | {{ _(notice.notice_type.display) }} 23 | {{ notice.message|safe }} 24 |
        • 25 | {% endfor %} 26 | {% else %} 27 |
        • {{ _("No notices.") }}

        • 28 | {% endif %} 29 |
        30 | 31 |
        32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/short.txt: -------------------------------------------------------------------------------- 1 | {% trans %}{{ notice }}{% endtrans %} 2 | -------------------------------------------------------------------------------- /badger_example/templates/badger/notification/single.html: -------------------------------------------------------------------------------- 1 | {% extends "notification/base.html" %} 2 | 3 | {% block headtitle %}{{ _("Notices") }}{% endblock %} 4 | 5 | {% block content %} 6 |
        7 | 15 | 16 |
        17 | {{ _(notice.notice_type.display) }} 18 | {{ notice.message|safe }} 19 | {{ notice.added }} 20 |
        21 | 22 |
        23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /badger_example/templates/badger/staff_tools.html: -------------------------------------------------------------------------------- 1 | {% extends "badger/base.html" %} 2 | 3 | {% block pageid %}staff_tools{% endblock %} 4 | 5 | {% block extrahead %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
        10 | 13 |
        14 | {{ csrf }} 15 | 16 |
          17 | {{ grant_form.as_ul }} 18 |
        • 19 |
        20 |
        21 |
        22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /badger_example/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% block extrahead %}{% endblock %} 4 | 5 | 6 | {% block content %}{% endblock %} 7 | 8 | 9 | -------------------------------------------------------------------------------- /badger_example/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | 3 | from django.contrib import admin 4 | admin.autodiscover() 5 | 6 | import badger 7 | badger.autodiscover() 8 | 9 | urlpatterns = patterns('', 10 | # Examples: 11 | # url(r'^$', 'badger_example.views.home', name='home'), 12 | # url(r'^badger_example/', include('badger_example.foo.urls')), 13 | 14 | url(r'^admin/', include(admin.site.urls)), 15 | url(r'^badges/', include('badger.urls')), 16 | ) 17 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-badger.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-badger.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-badger" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-badger" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _chapter-api: 2 | 3 | API 4 | === 5 | 6 | Models 7 | ------ 8 | 9 | .. autoclass:: badger.models.Award 10 | :members: get_absolute_url, get_upload_meta, bake_obi_image, 11 | nomination 12 | 13 | .. autoclass:: badger.models.Badge 14 | :members: get_absolute_url, get_upload_meta, clean, 15 | generate_deferred_awards, get_claim_group, 16 | delete_claim_group, claim_groups, award_to, 17 | check_prerequisites, is_awarded_to, progress_for, 18 | allows_nominate_for, nominate_for, is_nominated_for, 19 | as_obi_serialization 20 | 21 | .. autoclass:: badger.models.DeferredAward 22 | :members: get_claim_url, claim, grant_to 23 | 24 | .. autoclass:: badger.models.Nomination 25 | :members: get_absolute_url, is_approved, is_accepted, accept, 26 | is_rejected, reject_by 27 | 28 | .. autoclass:: badger.models.Progress 29 | :members: update_percent, increment_by, decrement_by 30 | 31 | 32 | Signals 33 | ------- 34 | 35 | .. automodule:: badger.signals 36 | 37 | .. autofunction:: badger.signals.badge_will_be_awarded 38 | 39 | .. autofunction:: badger.signals.badge_was_awarded 40 | 41 | .. autofunction:: badger.signals.user_will_be_nominated 42 | 43 | .. autofunction:: badger.signals.user_was_nominated 44 | 45 | .. autofunction:: badger.signals.nomination_will_be_approved 46 | 47 | .. autofunction:: badger.signals.nomination_was_approved 48 | 49 | .. autofunction:: badger.signals.nomination_will_be_accepted 50 | 51 | .. autofunction:: badger.signals.nomination_was_accepted 52 | 53 | .. autofunction:: badger.signals.nomination_will_be_rejected 54 | 55 | .. autofunction:: badger.signals.nomination_was_rejected 56 | 57 | 58 | Middleware 59 | ---------- 60 | 61 | .. autoclass:: badger.middleware.RecentBadgeAwardsMiddleware 62 | 63 | 64 | Utility functions 65 | ----------------- 66 | 67 | .. autofunction:: badger.utils.award_badge 68 | 69 | .. autofunction:: badger.utils.get_badge 70 | 71 | .. autofunction:: badger.utils.get_progress 72 | 73 | .. autofunction:: badger.utils.update_badges 74 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # django-badger documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Feb 14 09:51:15 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | sys.path.insert(0, os.path.abspath('..')) 21 | # sys.path.insert(0, os.path.abspath('.')) 22 | os.environ['DJANGO_SETTINGS_MODULE'] = 'test_settings' 23 | 24 | # -- General configuration ----------------------------------------------------- 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be extensions 30 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 31 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.ifconfig', 32 | 'sphinx.ext.viewcode'] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ['_templates'] 36 | 37 | # The suffix of source filenames. 38 | source_suffix = '.rst' 39 | 40 | # The encoding of source files. 41 | #source_encoding = 'utf-8-sig' 42 | 43 | # The master toctree document. 44 | master_doc = 'index' 45 | 46 | # General information about the project. 47 | project = u'django-badger' 48 | copyright = u'2013, Les Orchard' 49 | 50 | # The version info for the project you're documenting, acts as replacement for 51 | # |version| and |release|, also used in various other places throughout the 52 | # built documents. 53 | # 54 | # The short X.Y version. 55 | version = '0.0.1' 56 | # The full version, including alpha/beta/rc tags. 57 | release = '0.0.1' 58 | 59 | # The language for content autogenerated by Sphinx. Refer to documentation 60 | # for a list of supported languages. 61 | #language = None 62 | 63 | # There are two options for replacing |today|: either, you set today to some 64 | # non-false value, then it is used: 65 | #today = '' 66 | # Else, today_fmt is used as the format for a strftime call. 67 | #today_fmt = '%B %d, %Y' 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | exclude_patterns = ['_build'] 72 | 73 | # The reST default role (used for this markup: `text`) to use for all documents. 74 | #default_role = None 75 | 76 | # If true, '()' will be appended to :func: etc. cross-reference text. 77 | #add_function_parentheses = True 78 | 79 | # If true, the current module name will be prepended to all description 80 | # unit titles (such as .. function::). 81 | #add_module_names = True 82 | 83 | # If true, sectionauthor and moduleauthor directives will be shown in the 84 | # output. They are ignored by default. 85 | #show_authors = False 86 | 87 | # The name of the Pygments (syntax highlighting) style to use. 88 | pygments_style = 'sphinx' 89 | 90 | # A list of ignored prefixes for module index sorting. 91 | #modindex_common_prefix = [] 92 | 93 | 94 | # -- Options for HTML output --------------------------------------------------- 95 | 96 | # The theme to use for HTML and HTML Help pages. See the documentation for 97 | # a list of builtin themes. 98 | html_theme = 'default' 99 | 100 | # Theme options are theme-specific and customize the look and feel of a theme 101 | # further. For a list of options available for each theme, see the 102 | # documentation. 103 | #html_theme_options = {} 104 | 105 | # Add any paths that contain custom themes here, relative to this directory. 106 | #html_theme_path = [] 107 | 108 | # The name for this set of Sphinx documents. If None, it defaults to 109 | # " v documentation". 110 | #html_title = None 111 | 112 | # A shorter title for the navigation bar. Default is the same as html_title. 113 | #html_short_title = None 114 | 115 | # The name of an image file (relative to this directory) to place at the top 116 | # of the sidebar. 117 | #html_logo = None 118 | 119 | # The name of an image file (within the static path) to use as favicon of the 120 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 121 | # pixels large. 122 | #html_favicon = None 123 | 124 | # Add any paths that contain custom static files (such as style sheets) here, 125 | # relative to this directory. They are copied after the builtin static files, 126 | # so a file named "default.css" will overwrite the builtin "default.css". 127 | html_static_path = ['_static'] 128 | 129 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 130 | # using the given strftime format. 131 | #html_last_updated_fmt = '%b %d, %Y' 132 | 133 | # If true, SmartyPants will be used to convert quotes and dashes to 134 | # typographically correct entities. 135 | #html_use_smartypants = True 136 | 137 | # Custom sidebar templates, maps document names to template names. 138 | #html_sidebars = {} 139 | 140 | # Additional templates that should be rendered to pages, maps page names to 141 | # template names. 142 | #html_additional_pages = {} 143 | 144 | # If false, no module index is generated. 145 | #html_domain_indices = True 146 | 147 | # If false, no index is generated. 148 | #html_use_index = True 149 | 150 | # If true, the index is split into individual pages for each letter. 151 | #html_split_index = False 152 | 153 | # If true, links to the reST sources are added to the pages. 154 | #html_show_sourcelink = True 155 | 156 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 157 | #html_show_sphinx = True 158 | 159 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 160 | #html_show_copyright = True 161 | 162 | # If true, an OpenSearch description file will be output, and all pages will 163 | # contain a tag referring to it. The value of this option must be the 164 | # base URL from which the finished HTML is served. 165 | #html_use_opensearch = '' 166 | 167 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 168 | #html_file_suffix = None 169 | 170 | # Output file base name for HTML help builder. 171 | htmlhelp_basename = 'django-badgerdoc' 172 | 173 | 174 | # -- Options for LaTeX output -------------------------------------------------- 175 | 176 | # The paper size ('letter' or 'a4'). 177 | #latex_paper_size = 'letter' 178 | 179 | # The font size ('10pt', '11pt' or '12pt'). 180 | #latex_font_size = '10pt' 181 | 182 | # Grouping the document tree into LaTeX files. List of tuples 183 | # (source start file, target name, title, author, documentclass [howto/manual]). 184 | latex_documents = [ 185 | ('index', 'django-badger.tex', u'django-badger Documentation', 186 | u'Les Orchard', 'manual'), 187 | ] 188 | 189 | # The name of an image file (relative to this directory) to place at the top of 190 | # the title page. 191 | #latex_logo = None 192 | 193 | # For "manual" documents, if this is true, then toplevel headings are parts, 194 | # not chapters. 195 | #latex_use_parts = False 196 | 197 | # If true, show page references after internal links. 198 | #latex_show_pagerefs = False 199 | 200 | # If true, show URL addresses after external links. 201 | #latex_show_urls = False 202 | 203 | # Additional stuff for the LaTeX preamble. 204 | #latex_preamble = '' 205 | 206 | # Documents to append as an appendix to all manuals. 207 | #latex_appendices = [] 208 | 209 | # If false, no module index is generated. 210 | #latex_domain_indices = True 211 | 212 | 213 | # -- Options for manual page output -------------------------------------------- 214 | 215 | # One entry per manual page. List of tuples 216 | # (source start file, name, description, authors, manual section). 217 | man_pages = [ 218 | ('index', 'django-badger', u'django-badger Documentation', 219 | [u'Les Orchard'], 1) 220 | ] 221 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Installing 5 | ---------- 6 | 7 | To get the source code and all the requirements for django-badger 8 | tests to run, do this:: 9 | 10 | $ git clone https://github.com/mozilla/django-badger 11 | $ mkvirtualenv badger 12 | $ pip install -r requirements/dev.txt 13 | 14 | 15 | Running Tests 16 | ------------- 17 | 18 | django-badger uses `django-nose 19 | `_ which is a Django-centric 20 | test runner that uses `nose 21 | `_. 22 | 23 | To run the tests, do this:: 24 | 25 | $ python manager.py test 26 | 27 | 28 | You can also run the tests with `tox `_ 29 | which will run the tests in Python 2.6 and 2.7 for Django 1.4 and 1.5. to do 30 | that, do this:: 31 | 32 | $ tox 33 | 34 | 35 | Contact us 36 | ---------- 37 | 38 | We hang out on ``#badger`` on irc.mozilla.org. 39 | 40 | 41 | Pull Requests Welcome! 42 | ---------------------- 43 | 44 | .. TODO 45 | 46 | .. vim:set tw=78 ai fo+=n fo-=l ft=rst: 47 | -------------------------------------------------------------------------------- /docs/contributors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTORS 2 | -------------------------------------------------------------------------------- /docs/faqs.rst: -------------------------------------------------------------------------------- 1 | Frequently Asked Questions 2 | ========================== 3 | 4 | .. TODO 5 | 6 | .. vim:set tw=78 ai fo+=n fo-=l ft=rst: 7 | -------------------------------------------------------------------------------- /docs/getting_started.rst: -------------------------------------------------------------------------------- 1 | .. _chapter-getting-started: 2 | 3 | Getting Started 4 | =============== 5 | 6 | Setup 7 | ----- 8 | 9 | 1. Install django-badger. Currently there's no released version, so 10 | you need to install it from git:: 11 | 12 | pip install -e https://github.com/mozilla/django-badger/zipball/master#egg=django-badger 13 | 14 | 2. (Optional) Install django-notification. 15 | 3. Configure django-badger. See :ref:`chapter-getting-started-conf`. 16 | 4. Add settings to your Django settings file. See the Configuration 17 | section. 18 | 5. (Optional) Add ``badges.py`` files to your Django apps. FIXME - 19 | need docs for this. 20 | 6. Set up string extraction so that django-badger strings are 21 | translated. 22 | 23 | See `badg.us `_ for an example 24 | site setup. 25 | 26 | 27 | .. _chapter-getting-started-conf: 28 | 29 | Configuration 30 | ------------- 31 | 32 | BADGER_TEMPLATE_BASE 33 | String. The prefix for template files. Default is "badger". 34 | 35 | For example, if the value is "foo", then the django-badger home 36 | view renders the template "foo/home.html". 37 | 38 | BADGER_SITE_ISSUER 39 | Dict. Specifies the issuer of the badges. Example:: 40 | 41 | BADGER_SITE_ISSUER = { 42 | 'origin': SITE_URL, 43 | 'name': 'Name of my site', 44 | 'org': 'Name of my org', 45 | 'contact': 'contact@example.com', 46 | } 47 | 48 | Defaults to:: 49 | 50 | BADGER_SITE_ISSUER = { 51 | 'origin': 'http://mozilla.org', 52 | 'name': 'Badger', 53 | 'org': 'Mozilla', 54 | 'contact': 'lorchard@mozilla.com' 55 | } 56 | 57 | BADGER_ALLOW_ADD_BY_ANYONE 58 | Boolean. Master switch for wide-open badge creation by all 59 | users. This is also known as "multiplayer mode". 60 | 61 | BADGER_BADGE_PAGE_SIZE 62 | Integer. Page size for badge listings. Default is 50. 63 | 64 | BADGER_MAX_RECENT 65 | Integer. Number of items shown on home page recent 66 | sections. Default is 15. 67 | 68 | BADGER_IMG_MAX_SIZE 69 | Tuple of (Integer, Integer). Specifies the maximum height and 70 | width for badge images. Defaults to (256, 256). 71 | 72 | 73 | See `badg.us `_ for an example 74 | site setup 75 | 76 | See ``badger_example/badges.py`` in the source code for another 77 | example. 78 | 79 | 80 | Setting up views and/or using the API 81 | ------------------------------------- 82 | 83 | See :ref:`chapter-views` for documentation on the views that come with 84 | django-badger and how to use them. 85 | 86 | See :ref:`chapter-api` for documentation on the django-badger API. 87 | 88 | .. vim:set tw=78 ai fo+=n fo-=l ft=rst: 89 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. django-badger documentation master file, created by 2 | sphinx-quickstart on Thu Feb 14 09:51:15 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to django-badger's documentation! 7 | ========================================= 8 | 9 | django-badger is a reusable Django app that supports badges, to track and 10 | award achievements by your users. This can be used to help encourage certain 11 | behaviors, recognize skills, or just generally celebrate members of your 12 | community. 13 | 14 | - `Build status on travis-ci `_ (|build-status|) 15 | - `Latest documentation on Read The Docs `_ 16 | (`source for docs `_) 17 | - `Task board on Huboard `_ 18 | (`source for issues `_) 19 | 20 | .. |build-status| image:: https://secure.travis-ci.org/mozilla/django-badger.png?branch=master 21 | :target: http://travis-ci.org/mozilla/django-badger 22 | 23 | Contents: 24 | 25 | .. toctree:: 26 | :maxdepth: 2 27 | 28 | overview 29 | changelog 30 | getting_started 31 | usage 32 | views 33 | api 34 | faqs 35 | contributing 36 | contributors 37 | 38 | 39 | Indices and tables 40 | ================== 41 | 42 | * :ref:`genindex` 43 | * :ref:`modindex` 44 | * :ref:`search` 45 | 46 | .. vim:set tw=78 ai fo+=n fo-=l ft=rst: 47 | -------------------------------------------------------------------------------- /docs/overview.rst: -------------------------------------------------------------------------------- 1 | Overview 2 | ======== 3 | 4 | django-badger is a reusable Django app that supports badges, to track and 5 | award achievements by your users. This can be used to help encourage certain 6 | behaviors, recognize skills, or just generally celebrate members of your 7 | community. 8 | 9 | This app aims to provide features such as: 10 | 11 | - Basic badges, managed by the site owner in code and via Django admin. 12 | - Badge awards, triggered in response to signal-based events with code 13 | collected in per-app ``badges.py`` modules. 14 | - Views and models that enable users to create their own badges and nominate 15 | each other for awards. 16 | - Meta-badges, for which an award is automatically issued when a complete set 17 | of prerequisite badge awards have been collected. 18 | - Progress tracking, for which an award is issued when a user metric reaches 19 | 100% of some goal, or in response to some other custom logic. 20 | - Activity streams of badge awards. 21 | 22 | For more about the thinking behind this project, check out this essay: 23 | `Why does Mozilla need a Badger? `_ 24 | 25 | If you want to federate or share badges, you should check out 26 | the `Mozilla Open Badges `_ project. 27 | 28 | .. vim:set tw=78 ai fo+=n fo-=l ft=rst: 29 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | .. TODO 5 | 6 | Creating badges 7 | --------------- 8 | 9 | - TBD, see ``badger_example/badges.py`` in the source code for an example. 10 | 11 | Awarding badges 12 | --------------- 13 | 14 | - TBD, see ``badger_example/badges.py`` in the source code for an example. 15 | 16 | .. vim:set tw=78 ai fo+=n fo-=l ft=rst: 17 | -------------------------------------------------------------------------------- /docs/views.rst: -------------------------------------------------------------------------------- 1 | .. _chapter-views: 2 | 3 | Views 4 | ===== 5 | 6 | django-badger provides a series of views for: 7 | 8 | * listing badges 9 | * showing details for a badge 10 | * creating, editing and deleting badges 11 | * awarding, unawarding, recently awarded list and showing badge award details 12 | * claiming awarded badges 13 | * listing unclaimed badges 14 | * listing all the awarded badges for a user 15 | * listing all the users a badge has been awarded to 16 | * listing all the badges created by a user 17 | * nominating a user for a badge, listing nominations and showing 18 | nomination details 19 | 20 | You can use these views by adding the relevant urls to your Django 21 | project and writing the templates that they use. 22 | 23 | The location of the templates is determined by the 24 | ``BADGER_TEMPLATE_BASE`` setting. This defaults to ``badger``. 25 | 26 | .. Note:: 27 | 28 | The view code is in flux, so it's probably best to look at the code 29 | for implementing your templates. 30 | 31 | 32 | Adding urls 33 | ----------- 34 | 35 | There are two urls files. If you want to use the views, you should add 36 | one of them: 37 | 38 | ``badger/urls_simplified.py`` 39 | Holds url routes for all the views except the "multiplayer 40 | ones". This assumes that badges will be managed primarily in the 41 | admin and badges will be awarded by triggers in ``badges.py`` 42 | files. 43 | 44 | ``badger/urls.py`` 45 | Everything in ``badger/urls_simplified.py`` plus url routes for 46 | "multiplayer views" allowing users to create awards, nominate 47 | others for awards and all that. 48 | 49 | 50 | Views 51 | ----- 52 | 53 | .. autofunction:: badger.views.home 54 | 55 | .. autofunction:: badger.views.badges_list 56 | 57 | .. autofunction:: badger.views.detail 58 | 59 | .. autofunction:: badger.views.create 60 | 61 | .. autofunction:: badger.views.edit 62 | 63 | .. autofunction:: badger.views.delete 64 | 65 | .. autofunction:: badger.views.award_badge 66 | 67 | .. autofunction:: badger.views.awards_list 68 | 69 | .. autofunction:: badger.views.award_detail 70 | 71 | .. autofunction:: badger.views.award_delete 72 | 73 | .. autofunction:: badger.views.claim_deferred_award 74 | 75 | .. autofunction:: badger.views.claims_list 76 | 77 | .. autofunction:: badger.views.awards_by_user 78 | 79 | .. autofunction:: badger.views.awards_by_badge 80 | 81 | .. autofunction:: badger.views.badges_by_user 82 | 83 | .. autofunction:: badger.views.nomination_detail 84 | 85 | .. autofunction:: badger.views.nominate_for 86 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_settings") 6 | 7 | 8 | def nose_collector(): 9 | import nose 10 | return nose.collector() 11 | 12 | 13 | if __name__ == "__main__": 14 | from django.core.management import execute_from_command_line 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /requirements/compiled.txt: -------------------------------------------------------------------------------- 1 | Pillow==2.7.0 2 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | # install django-badger for editing 2 | -e . 3 | 4 | # install compiled requirements 5 | -r compiled.txt 6 | 7 | # for building docs 8 | Sphinx 9 | 10 | # for tests 11 | nose 12 | tox 13 | django_nose 14 | South 15 | feedparser 16 | pyquery==1.2.4 17 | cssselect==0.7.1 18 | lxml==3.1beta1 19 | -------------------------------------------------------------------------------- /requirements/prod.txt: -------------------------------------------------------------------------------- 1 | -e git://github.com/jtauber/django-notification.git#egg=django-notification 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import find_packages, setup 3 | try: 4 | import multiprocessing 5 | except ImportError: 6 | pass 7 | 8 | 9 | setup( 10 | name='django-badger', 11 | version='0.0.1', 12 | description='Django app for managing and awarding badgers', 13 | long_description=open('README.rst').read(), 14 | author='Leslie Michael Orchard', 15 | author_email='me@lmorchard.com', 16 | url='http://github.com/mozilla/django-badger', 17 | license='BSD', 18 | packages=find_packages(), 19 | include_package_data=True, 20 | classifiers=[ 21 | 'Development Status :: 4 - Beta', 22 | 'Framework :: Django', 23 | 'Intended Audience :: Developers', 24 | 'License :: OSI Approved :: BSD License', 25 | 'Operating System :: OS Independent', 26 | 'Programming Language :: Python', 27 | 'Programming Language :: Python :: 2.6', 28 | 'Programming Language :: Python :: 2.7', 29 | 'Topic :: Software Development :: Libraries :: Python Modules', 30 | ], 31 | install_requires=[ 32 | 'django>=1.4', 33 | 'pillow', 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /test_settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for badger_example project. 2 | import django 3 | 4 | 5 | DEBUG = True 6 | TEMPLATE_DEBUG = DEBUG 7 | 8 | SITE_ID = 1 9 | 10 | ADMINS = ( 11 | # ('Your Name', 'your_email@example.com'), 12 | ) 13 | 14 | TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' 15 | 16 | MANAGERS = ADMINS 17 | 18 | DATABASES = { 19 | 'default': { 20 | 'NAME': 'test-badger.db', 21 | 'ENGINE': 'django.db.backends.sqlite3', 22 | } 23 | } 24 | 25 | # Local time zone for this installation. Choices can be found here: 26 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 27 | # although not all choices may be available on all operating systems. 28 | # In a Windows environment this must be set to your system time zone. 29 | TIME_ZONE = 'America/Chicago' 30 | 31 | # Language code for this installation. All choices can be found here: 32 | # http://www.i18nguy.com/unicode/language-identifiers.html 33 | LANGUAGE_CODE = 'en-us' 34 | 35 | SITE_ID = 1 36 | 37 | # If you set this to False, Django will make some optimizations so as not 38 | # to load the internationalization machinery. 39 | USE_I18N = True 40 | 41 | # If you set this to False, Django will not format dates, numbers and 42 | # calendars according to the current locale. 43 | USE_L10N = True 44 | 45 | # If you set this to False, Django will not use timezone-aware datetimes. 46 | USE_TZ = False # True 47 | 48 | # Absolute filesystem path to the directory that will hold user-uploaded files. 49 | # Example: "/home/media/media.lawrence.com/media/" 50 | MEDIA_ROOT = '' 51 | 52 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 53 | # trailing slash. 54 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 55 | MEDIA_URL = '' 56 | 57 | # Absolute path to the directory static files should be collected to. 58 | # Don't put anything in this directory yourself; store your static files 59 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 60 | # Example: "/home/media/media.lawrence.com/static/" 61 | STATIC_ROOT = '' 62 | 63 | # URL prefix for static files. 64 | # Example: "http://media.lawrence.com/static/" 65 | STATIC_URL = '/static/' 66 | 67 | # Additional locations of static files 68 | STATICFILES_DIRS = ( 69 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 70 | # Always use forward slashes, even on Windows. 71 | # Don't forget to use absolute paths, not relative paths. 72 | ) 73 | 74 | # List of finder classes that know how to find static files in 75 | # various locations. 76 | STATICFILES_FINDERS = ( 77 | 'django.contrib.staticfiles.finders.FileSystemFinder', 78 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 79 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 80 | ) 81 | 82 | # Make this unique, and don't share it with anybody. 83 | SECRET_KEY = 'e-=ohaa)s7s3+qeye9^l2!qb&^-ak5o4sty69=vhv@fnxjn7_q' 84 | 85 | # List of callables that know how to import templates from various sources. 86 | TEMPLATE_LOADERS = ( 87 | #'jingo.Loader', 88 | 'django.template.loaders.filesystem.Loader', 89 | 'django.template.loaders.app_directories.Loader', 90 | # 'django.template.loaders.eggs.Loader', 91 | ) 92 | 93 | TEMPLATE_CONTEXT_PROCESSORS = ( 94 | 'django.contrib.auth.context_processors.auth', 95 | 'django.core.context_processors.debug', 96 | 'django.core.context_processors.media', 97 | 'django.core.context_processors.request', 98 | 'django.contrib.messages.context_processors.messages', 99 | ) 100 | 101 | MIDDLEWARE_CLASSES = ( 102 | 'django.middleware.common.CommonMiddleware', 103 | 'django.contrib.sessions.middleware.SessionMiddleware', 104 | 'django.middleware.csrf.CsrfViewMiddleware', 105 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 106 | 'django.contrib.messages.middleware.MessageMiddleware', 107 | 'badger.middleware.RecentBadgeAwardsMiddleware', 108 | ) 109 | 110 | ROOT_URLCONF = 'badger_example.urls' 111 | 112 | TEMPLATE_DIRS = ( 113 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 114 | # Always use forward slashes, even on Windows. 115 | # Don't forget to use absolute paths, not relative paths. 116 | ) 117 | 118 | INSTALLED_APPS = [ 119 | 'django.contrib.auth', 120 | 'django.contrib.contenttypes', 121 | 'django.contrib.sessions', 122 | 'django.contrib.sites', 123 | 'django.contrib.messages', 124 | 'django.contrib.staticfiles', 125 | 'django.contrib.admin', 126 | 127 | 'django_nose', 128 | 'badger_example', 129 | 'badger', 130 | ] 131 | 132 | if django.VERSION[:2] < (1, 7): 133 | INSTALLED_APPS = INSTALLED_APPS + ['south'] 134 | 135 | MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' 136 | 137 | LOGGING = { 138 | 'version': 1, 139 | 'disable_existing_loggers': False, 140 | 'filters': { 141 | }, 142 | 'handlers': { 143 | 'mail_admins': { 144 | 'level': 'ERROR', 145 | 'filters': [], 146 | 'class': 'django.utils.log.AdminEmailHandler' 147 | } 148 | }, 149 | 'loggers': { 150 | 'django.request': { 151 | 'handlers': ['mail_admins'], 152 | 'level': 'ERROR', 153 | 'propagate': True, 154 | }, 155 | } 156 | } 157 | 158 | BADGER_TEMPLATE_BASE = 'badger' 159 | 160 | ABSOLUTE_URL_OVERRIDES = { 161 | 'auth.user': lambda u: u'/users/{0}'.format(u.username) 162 | } 163 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (http://tox.testrun.org/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [testenv:py26_dj14] 7 | basepython = python2.6 8 | commands = pip install "Django>=1.4,<1.4.99" 9 | pip install -r requirements/dev.txt 10 | {envpython} manage.py test badger 11 | 12 | [testenv:py27_dj14] 13 | basepython = python2.7 14 | commands = pip install "Django>=1.4,<1.4.99" 15 | pip install -r requirements/dev.txt 16 | {envpython} manage.py test badger 17 | 18 | [testenv:py26_dj15] 19 | basepython = python2.6 20 | commands = pip install "Django>=1.5,<1.5.99" 21 | pip install -r requirements/dev.txt 22 | {envpython} manage.py test badger 23 | 24 | [testenv:py27_dj15] 25 | basepython = python2.7 26 | commands = pip install "Django>=1.5,<1.5.99" 27 | pip install -r requirements/dev.txt 28 | {envpython} manage.py test badger 29 | 30 | [testenv:py26_dj16] 31 | basepython = python2.6 32 | commands = pip install "Django>=1.6,<1.6.99" 33 | pip install -r requirements/dev.txt 34 | {envpython} manage.py test badger 35 | 36 | [testenv:py27_dj16] 37 | basepython = python2.7 38 | commands = pip install "Django>=1.6,<1.6.99" 39 | pip install -r requirements/dev.txt 40 | {envpython} manage.py test badger 41 | 42 | [testenv:py27_dj17] 43 | basepython = python2.7 44 | commands = pip install "Django>=1.7,<1.7.99" 45 | pip install -r requirements/dev.txt 46 | {envpython} manage.py test badger 47 | 48 | 49 | [testenv:py27_dj18] 50 | basepython = python2.7 51 | commands = pip install "Django>=1.8,<1.8.99" 52 | pip install -r requirements/dev.txt 53 | {envpython} manage.py test badger 54 | --------------------------------------------------------------------------------