Browse files

Merge branch 'add-categories'

  • Loading branch information...
2 parents 875473c + 2cf4055 commit bd68c0f1ebc0c1a2e3b6932e1a81ae82835ef5af @paltman paltman committed Oct 22, 2011
View
2 agon_ratings/__init__.py
@@ -1 +1 @@
-__version__ = "0.1.2"
+__version__ = "0.2"
View
28 agon_ratings/categories.py
@@ -0,0 +1,28 @@
+from django.conf import settings
+
+
+RATING_CATEGORY_CHOICES_DICT = getattr(settings, "AGON_RATINGS_CATEGORY_CHOICES", {})
+RATING_CATEGORY_CHOICES = []
+RATING_CATEGORY_LOOKUP = {}
+if len(RATING_CATEGORY_CHOICES_DICT.keys()) > 0:
+ for model_str in RATING_CATEGORY_CHOICES_DICT.keys():
+ for choice in RATING_CATEGORY_CHOICES_DICT[model_str].keys():
+ slug = "%s-%s" % (model_str, choice)
+ val = len(RATING_CATEGORY_CHOICES) + 1
+ RATING_CATEGORY_CHOICES.append((val, slug))
+ RATING_CATEGORY_LOOKUP[slug] = val
+
+
+def category_label(obj, choice):
+ obj_str = "%s.%s" % (obj._meta.app_label, obj._meta.object_name)
+ return RATING_CATEGORY_CHOICES_DICT.get(obj_str, {}).get(choice)
+
+
+def is_valid_category(obj, choice):
+ return category_label(obj, choice) is not None
+
+
+def category_value(obj, choice):
+ return RATING_CATEGORY_LOOKUP.get(
+ "%s.%s-%s" % (obj._meta.app_label, obj._meta.object_name, choice)
+ )
View
12 agon_ratings/managers.py
@@ -2,13 +2,21 @@
from django.contrib.contenttypes.models import ContentType
+from agon_ratings.categories import category_value
+
class OverallRatingManager(models.Manager):
- def top_rated(self, klass):
+ def top_rated(self, klass, category=None):
+
+ if category:
+ cat = category_value(klass, category)
+ else:
+ cat = None
return self.filter(
- content_type=ContentType.objects.get_for_model(klass)
+ content_type=ContentType.objects.get_for_model(klass),
+ category=cat
).extra(
select={
"sortable_rating": "COALESCE(rating, 0)"
View
9 agon_ratings/models.py
@@ -8,6 +8,7 @@
from django.contrib.contenttypes.generic import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
+from agon_ratings.categories import RATING_CATEGORY_CHOICES
from agon_ratings.managers import OverallRatingManager
@@ -18,11 +19,13 @@ class OverallRating(models.Model):
content_object = GenericForeignKey()
rating = models.DecimalField(decimal_places=1, max_digits=3, null=True)
+ category = models.IntegerField(null=True, choices=RATING_CATEGORY_CHOICES)
+
objects = OverallRatingManager()
class Meta:
unique_together = [
- ("object_id", "content_type"),
+ ("object_id", "content_type", "category"),
]
def update(self):
@@ -42,9 +45,11 @@ class Rating(models.Model):
rating = models.IntegerField()
timestamp = models.DateTimeField(default=datetime.datetime.now)
+ category = models.IntegerField(null=True, choices=RATING_CATEGORY_CHOICES)
+
class Meta:
unique_together = [
- ("object_id", "content_type", "user"),
+ ("object_id", "content_type", "user", "category"),
]
def __unicode__(self):
View
11 agon_ratings/templates/agon_ratings/_script.html
@@ -17,15 +17,20 @@
url: "{{ post_url }}",
type: "POST",
data: {
- "rating": current_rating
+ "rating": current_rating,
+ "category": "{{ category }}"
},
statusCode: {
403: function(jqXHR, textStatus, errorThrown) {
- // Invalid rating was posted
+ // Invalid rating was posted or invalid category was sent
console.log(errorThrown);
},
200: function(data, textStatus, jqXHR) {
- $(".overall_rating").text(data["overall_rating"]);
+ {% if category %}
+ $(".overall_rating.category-{{ category }}").text(data["overall_rating"]);
+ {% else %}
+ $(".overall_rating").text(data["overall_rating"]);
+ {% endif %}
}
}
});
View
67 agon_ratings/templatetags/agon_ratings_tags.py
@@ -4,19 +4,21 @@
from django.contrib.contenttypes.models import ContentType
+from agon_ratings.categories import category_value
from agon_ratings.models import Rating, OverallRating
register = template.Library()
-def get_user_rating(user, obj):
+def user_rating_value(user, obj, category=None):
try:
ct = ContentType.objects.get_for_model(obj)
rating = Rating.objects.get(
object_id = obj.pk,
content_type = ct,
- user = user
+ user = user,
+ category = category_value(obj, category)
).rating
except Rating.DoesNotExist:
rating = 0
@@ -28,31 +30,43 @@ class UserRatingNode(template.Node):
@classmethod
def handle_token(cls, parser, token):
bits = token.split_contents()
- if len(bits) != 7:
+
+ if len(bits) == 5:
+ category = None
+ elif len(bits) == 6:
+ category = parser.compile_filter(bits[3])
+ else:
raise template.TemplateSyntaxError()
+
return cls(
- user = parser.compile_filter(bits[2]),
- obj = parser.compile_filter(bits[4]),
- as_var = bits[6]
+ user = parser.compile_filter(bits[1]),
+ obj = parser.compile_filter(bits[2]),
+ as_var = bits[len(bits) - 1],
+ category = category
)
- def __init__(self, user, obj, as_var):
+ def __init__(self, user, obj, as_var, category=None):
self.user = user
self.obj = obj
self.as_var = as_var
+ self.category = category
def render(self, context):
user = self.user.resolve(context)
obj = self.obj.resolve(context)
- context[self.as_var] = get_user_rating(user, obj)
+ if self.category:
+ category = self.category.resolve(context)
+ else:
+ category = None
+ context[self.as_var] = user_rating_value(user, obj, category)
return ""
@register.tag
def user_rating(parser, token):
"""
Usage:
- {% user_rating for user and obj as var %}
+ {% user_rating user obj [category] as var %}
"""
return UserRatingNode.handle_token(parser, token)
@@ -62,24 +76,38 @@ class OverallRatingNode(template.Node):
@classmethod
def handle_token(cls, parser, token):
bits = token.split_contents()
- if len(bits) != 5:
+
+ if len(bits) == 4:
+ category = None
+ elif len(bits) == 5:
+ category = parser.compile_filter(bits[2])
+ else:
raise template.TemplateSyntaxError()
+
return cls(
- obj = parser.compile_filter(bits[2]),
- as_var = bits[4]
+ obj = parser.compile_filter(bits[1]),
+ as_var = bits[len(bits) - 1],
+ category = category
)
- def __init__(self, obj, as_var):
+ def __init__(self, obj, as_var, category=None):
self.obj = obj
self.as_var = as_var
+ self.category = category
def render(self, context):
obj = self.obj.resolve(context)
+ if self.category:
+ category = self.category.resolve(context)
+ else:
+ category = None
+
try:
ct = ContentType.objects.get_for_model(obj)
rating = OverallRating.objects.get(
- object_id=obj.pk,
- content_type=ct
+ object_id = obj.pk,
+ content_type = ct,
+ category = category_value(obj, category)
).rating or 0
except OverallRating.DoesNotExist:
rating = 0
@@ -91,7 +119,7 @@ def render(self, context):
def overall_rating(parser, token):
"""
Usage:
- {% overall_rating for obj as var %}
+ {% overall_rating obj [category] as var %}
"""
return OverallRatingNode.handle_token(parser, token)
@@ -109,15 +137,16 @@ def rating_post_url(user, obj):
@register.inclusion_tag("agon_ratings/_script.html")
-def user_rating_js(user, obj):
+def user_rating_js(user, obj, category=None):
post_url = rating_post_url(user, obj)
- rating = get_user_rating(user, obj)
+ rating = user_rating_value(user, obj, category)
return {
"obj": obj,
"post_url": post_url,
+ "category": category,
"the_user_rating": rating,
- "STATIC_URL": settings.STATIC_URL
+ "STATIC_URL": settings.STATIC_URL,
}
View
32 agon_ratings/views.py
@@ -7,6 +7,7 @@
from django.contrib.auth.decorators import login_required
from django.contrib.contenttypes.models import ContentType
+from agon_ratings.categories import category_value
from agon_ratings.models import Rating, OverallRating
@@ -19,10 +20,26 @@ def rate(request, content_type_id, object_id):
ct = get_object_or_404(ContentType, pk=content_type_id)
obj = get_object_or_404(ct.model_class(), pk=object_id)
rating_input = int(request.POST.get("rating"))
+ category = request.POST.get("category")
+ if category:
+ cat_choice = category_value(obj, category)
+ else:
+ cat_choice = None
+
+ # Check for errors and bail early
+ if category is not None and cat_choice is None:
+ return HttpResponseForbidden(
+ "Invalid category. It must match a preconfigured setting"
+ )
+ if rating_input not in range(NUM_OF_RATINGS + 1):
+ return HttpResponseForbidden(
+ "Invalid rating. It must be a value between 0 and %s" % NUM_OF_RATINGS
+ )
data = {
"user_rating": rating_input,
- "overall_rating": 0
+ "overall_rating": 0,
+ "category": category
}
# @@@ Seems like this could be much more DRY with a model method or something
@@ -31,35 +48,34 @@ def rate(request, content_type_id, object_id):
rating = Rating.objects.get(
object_id = object_id,
content_type = ct,
- user = request.user
+ user = request.user,
+ category = cat_choice
)
overall = rating.overall_rating
rating.delete()
overall.update()
data["overall_rating"] = str(overall.rating)
except Rating.DoesNotExist:
pass
- elif 1 <= rating_input <= NUM_OF_RATINGS: # set the rating
+ else: # set the rating
rating, created = Rating.objects.get_or_create(
object_id = obj.pk,
content_type = ct,
user = request.user,
+ category = cat_choice,
defaults = {
"rating": rating_input
}
)
overall, created = OverallRating.objects.get_or_create(
object_id = obj.pk,
- content_type = ct
+ content_type = ct,
+ category = cat_choice
)
rating.overall_rating = overall
rating.rating = rating_input
rating.save()
overall.update()
data["overall_rating"] = str(overall.rating)
- else: # whoops
- return HttpResponseForbidden(
- "Invalid rating. It must be a value between 0 and %s" % NUM_OF_RATINGS
- )
return HttpResponse(json.dumps(data), mimetype="application/json")
View
24 docs/changelog.rst
@@ -3,6 +3,30 @@
ChangeLog
=========
+0.2
+---
+
+- added support for ratings to have categories instead of just a single
+ rating for an object
+- dropped natural language of template tags
+
+Migrations
+^^^^^^^^^^
+
+Added a category model and updated the unique index on both models::
+
+ ALTER TABLE "agon_ratings_overallrating" ADD COLUMN "category" int;
+ ALTER TABLE "agon_ratings_rating" ADD COLUMN "category" int;
+ CREATE UNIQUE INDEX "agon_ratings_overallrating_unq_object_id_content_type_id_category_idx"
+ ON "agon_ratings_overallrating" (object_id, content_type_id, category);
+ CREATE UNIQUE INDEX "agon_ratings_rating_unq_object_id_content_type_id_user_id_category_idx"
+ ON "agon_ratings_rating" (object_id, content_type_id, user_id, category);
+ ALTER TABLE "agon_ratings_rating" DROP CONSTRAINT
+ IF EXISTS "agon_ratings_rating_object_id_content_type_id_user_id_key";
+ ALTER TABLE "agon_ratings_overallrating" DROP CONSTRAINT
+ IF EXISTS "agon_ratings_overallrating_object_id_content_type_id_key";
+
+
0.1.2
-----
View
4 docs/conf.py
@@ -4,8 +4,8 @@
master_doc = 'index'
project = u'agon_ratings'
copyright = u'2011, Eldarion'
-version = '0.1.2'
-release = '0.1.2'
+version = '0.2'
+release = '0.2'
exclude_patterns = ['_build']
pygments_style = 'sphinx'
html_theme = 'default'
View
17 docs/installation.rst
@@ -22,3 +22,20 @@ Installation
...
url(r"^ratings/", include("agon_ratings.urls")),
...
+
+* Optionally, if want to use the ratings category feature of `agon-ratings`
+ then you will need to add the `AGON_RATINGS_CATEGORY_CHOICES` setting
+ in your `settings.py`::
+
+ AGON_RATINGS_CATEGORY_CHOICES = {
+ "app.Model": {
+ "exposure": "How good is the exposure?",
+ "framing": "How well was the photo framed?",
+ "saturation": "How would you rate the saturation?"
+ },
+ "app.Model2": {
+ "grammar": "Good grammar?",
+ "complete": "Is the story complete?",
+ "compelling": "Is the article compelling?"
+ }
+ }
View
27 docs/settings.rst
@@ -11,3 +11,30 @@ AGON_NUM_OF_RATINGS
:Default: 5
Defines the number of different rating choices there will be.
+
+
+AGON_RATINGS_CATEGORY_CHOICES
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:Default: `None`
+
+Defines a dictionary of choices for different models for the application of
+ratings along different dimensions rather than just a single rating for an
+object.
+
+It should follow the format of a dictionary of dictionaries. For example, think of
+the context of a website that allowed ratings of photographs and articles
+published by other users::
+
+ AGON_RATINGS_CATEGORY_CHOICES = {
+ "app.Model": {
+ "exposure": "How good is the exposure?",
+ "framing": "How well was the photo framed?",
+ "saturation": "How would you rate the saturation?"
+ },
+ "app.Model2": {
+ "grammar": "Good grammar?",
+ "complete": "Is the story complete?",
+ "compelling": "Is the article compelling?"
+ }
+ }
View
53 docs/usage.rst
@@ -7,15 +7,58 @@ Integrating `agon_ratings` into your project is just a matter of using a couple
template tags and wiring up a bit of javascript. The rating form is intended
to function via AJAX and as such returns JSON.
-Firstly, you will want to add the following blocks in your templates where
-you want to expose the rating form::
+Firstly, add load the template tags for `agon_ratings`::
{% load agon_ratings_tags %}
+
+
+Then, if you want to display an overall rating average for an object you can set
+a context variable and display it::
+
+ {% overall_rating obj as the_overall_rating %}
- {% overall_rating for some_object as the_overall_rating %}
+ <div class="overall_rating">{{ the_overall_rating }}</div>
+
+
+Likewise for displaying a user's rating::
+
+ {% user_rating request.user obj as the_user_rating %}
+ <div class="user_rating">{{ the_user_rating }}</div>
+
+
+If you want to add an AJAX form for allowing a user to set a rating, add the
+following in the appropriate location on your page::
+
<div id="user_rating"></div>
+
+
+And then add this near the end of your HTML `<body>` to emit some Javascript
+libraries and hook up the ratings UI::
+
+ {% user_rating_js request.user obj %}
+
+
+If you want to do any rating based on categories of ratings for an object or
+objects then you do the same as above but just use an optional argument on
+the tags::
+
+ {% overall_rating obj "accuracy" as category_rating %}
- <div class="overall_rating">{{ the_overall_rating }}</div>
+ <div class="overall_rating category-accuracy">
+ {{ category_rating }}
+ </div>
+
+and::
+
+ {% user_rating request.user obj "accuracy" as category_rating %}
+
+ <div class="user_rating category-accuracy">
+ {{ category_rating }}
+ </div>
+
+and::
+
+ <div id="user_rating" class="category-accuracy"></div>
- {% user_rating_js request.user some_object %}
+ {% user_rating_js request.user obj "accuracy" %}

0 comments on commit bd68c0f

Please sign in to comment.