[extensions-web/filter-sort-ui: 10/20] Add a binary rating system to replace star ratings
- From: Jasper St. Pierre <jstpierre src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [extensions-web/filter-sort-ui: 10/20] Add a binary rating system to replace star ratings
- Date: Tue, 3 Jan 2012 04:01:31 +0000 (UTC)
commit 17026747d15a00a9d6b2042d1e06973477cb4c93
Author: Jasper St. Pierre <jstpierre mecheye net>
Date: Wed Dec 28 01:42:01 2011 -0500
Add a binary rating system to replace star ratings
The rationale is discussed in a previous commit.
We've stolen a bit of sorting code from Reddit, Inc, which allows us to sort
extensions based on their ratings in a smart way:
http://www.evanmiller.org/how-not-to-sort-by-average-rating.html
Conflicts:
sweettooth/extensions/views.py
...nsionliketracker__add_field_extension_rating.py | 121 ++++++++++++++++
.../extensions/migrations/0015_ratings_to_likes.py | 151 ++++++++++++++++++++
sweettooth/extensions/models.py | 46 ++++++
.../extensions/templates/extensions/comments.html | 17 +++
sweettooth/extensions/urls.py | 1 +
sweettooth/extensions/views.py | 65 +++++++++
sweettooth/static/css/sweettooth.css | 74 ++++++++++
sweettooth/static/js/main.js | 27 ++++
8 files changed, 502 insertions(+), 0 deletions(-)
---
diff --git a/sweettooth/extensions/migrations/0014_auto__add_extensionliketracker__add_field_extension_rating.py b/sweettooth/extensions/migrations/0014_auto__add_extensionliketracker__add_field_extension_rating.py
new file mode 100644
index 0000000..f8e4bac
--- /dev/null
+++ b/sweettooth/extensions/migrations/0014_auto__add_extensionliketracker__add_field_extension_rating.py
@@ -0,0 +1,121 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding model 'ExtensionLikeTracker'
+ db.create_table('extensions_extensionliketracker', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('extension', self.gf('django.db.models.fields.related.ForeignKey')(related_name='like_trackers', to=orm['extensions.Extension'])),
+ ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
+ ('vote', self.gf('django.db.models.fields.BooleanField')(default=False, db_index=True)),
+ ))
+ db.send_create_signal('extensions', ['ExtensionLikeTracker'])
+
+ # Adding field 'Extension.rating'
+ db.add_column('extensions_extension', 'rating', self.gf('django.db.models.fields.FloatField')(default=0), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting model 'ExtensionLikeTracker'
+ db.delete_table('extensions_extensionliketracker')
+
+ # Deleting field 'Extension.rating'
+ db.delete_column('extensions_extension', 'rating')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'extensions.extension': {
+ 'Meta': {'object_name': 'Extension'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'disables': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'downloads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'enables': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'icon': ('django.db.models.fields.files.ImageField', [], {'default': "'/static/images/plugin.png'", 'max_length': '100', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
+ 'popularity': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'rating': ('django.db.models.fields.FloatField', [], {'default': '0'}),
+ 'screenshot': ('sorl.thumbnail.fields.ImageField', [], {'max_length': '100', 'blank': 'True'}),
+ 'slug': ('autoslug.fields.AutoSlugField', [], {'unique_with': '()', 'max_length': '50', 'populate_from': 'None', 'db_index': 'True'}),
+ 'url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200', 'db_index': 'True'})
+ },
+ 'extensions.extensionliketracker': {
+ 'Meta': {'object_name': 'ExtensionLikeTracker'},
+ 'extension': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'like_trackers'", 'to': "orm['extensions.Extension']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'vote': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'})
+ },
+ 'extensions.extensionpopularityitem': {
+ 'Meta': {'object_name': 'ExtensionPopularityItem'},
+ 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'extension': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'popularity_items'", 'to': "orm['extensions.Extension']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'offset': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'extensions.extensionversion': {
+ 'Meta': {'unique_together': "(('extension', 'version'),)", 'object_name': 'ExtensionVersion'},
+ 'extension': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'versions'", 'to': "orm['extensions.Extension']"}),
+ 'extra_json_fields': ('django.db.models.fields.TextField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'shell_versions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['extensions.ShellVersion']", 'symmetrical': 'False'}),
+ 'source': ('django.db.models.fields.files.FileField', [], {'max_length': '223'}),
+ 'status': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'version': ('django.db.models.fields.IntegerField', [], {'default': '0'})
+ },
+ 'extensions.shellversion': {
+ 'Meta': {'object_name': 'ShellVersion'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'major': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'minor': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'point': ('django.db.models.fields.IntegerField', [], {})
+ }
+ }
+
+ complete_apps = ['extensions']
diff --git a/sweettooth/extensions/migrations/0015_ratings_to_likes.py b/sweettooth/extensions/migrations/0015_ratings_to_likes.py
new file mode 100644
index 0000000..716d845
--- /dev/null
+++ b/sweettooth/extensions/migrations/0015_ratings_to_likes.py
@@ -0,0 +1,151 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+from django.contrib.contenttypes.models import ContentType
+from extensions.models import Extension
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ "Write your forwards methods here."
+ for ext in orm.Extension.objects.all():
+ user_ratings = {}
+ rc = orm['ratings.RatingComment']
+ ct = ContentType.objects.get_for_model(ext)
+ for rating_comment in rc.objects.filter(content_type=ct, object_pk=ext.pk):
+ user_ratings.setdefault(rating_comment.user, []).append(rating_comment.rating)
+
+ for user, ratings in user_ratings.iteritems():
+ avg = sum(ratings) / float(len(ratings))
+ tracker, created = orm.ExtensionLikeTracker.objects.get_or_create(user=user,
+ extension=ext)
+ tracker.vote = (avg >= 3)
+ tracker.save()
+
+ for ext in Extension.objects.all():
+ ext.recalculate_rating()
+ ext.save(replace_metadata_json=False)
+
+ def backwards(self, orm):
+ "Write your backwards methods here."
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'comments.comment': {
+ 'Meta': {'ordering': "('submit_date',)", 'object_name': 'Comment', 'db_table': "'django_comments'"},
+ 'comment': ('django.db.models.fields.TextField', [], {'max_length': '3000'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_comment'", 'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_address': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}),
+ 'is_public': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_removed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'object_pk': ('django.db.models.fields.TextField', [], {}),
+ 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['sites.Site']"}),
+ 'submit_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comment_comments'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'user_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'user_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
+ 'user_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'extensions.extension': {
+ 'Meta': {'object_name': 'Extension'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'disables': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'downloads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'enables': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'icon': ('django.db.models.fields.files.ImageField', [], {'default': "'/static/images/plugin.png'", 'max_length': '100', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
+ 'popularity': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'rating': ('django.db.models.fields.FloatField', [], {'default': '0'}),
+ 'screenshot': ('sorl.thumbnail.fields.ImageField', [], {'max_length': '100', 'blank': 'True'}),
+ 'slug': ('autoslug.fields.AutoSlugField', [], {'unique_with': '()', 'max_length': '50', 'populate_from': 'None', 'db_index': 'True'}),
+ 'url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
+ 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200', 'db_index': 'True'})
+ },
+ 'extensions.extensionliketracker': {
+ 'Meta': {'object_name': 'ExtensionLikeTracker'},
+ 'extension': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'like_trackers'", 'to': "orm['extensions.Extension']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'vote': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'})
+ },
+ 'extensions.extensionpopularityitem': {
+ 'Meta': {'object_name': 'ExtensionPopularityItem'},
+ 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'extension': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'popularity_items'", 'to': "orm['extensions.Extension']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'offset': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'extensions.extensionversion': {
+ 'Meta': {'unique_together': "(('extension', 'version'),)", 'object_name': 'ExtensionVersion'},
+ 'extension': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'versions'", 'to': "orm['extensions.Extension']"}),
+ 'extra_json_fields': ('django.db.models.fields.TextField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'shell_versions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['extensions.ShellVersion']", 'symmetrical': 'False'}),
+ 'source': ('django.db.models.fields.files.FileField', [], {'max_length': '223'}),
+ 'status': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'version': ('django.db.models.fields.IntegerField', [], {'default': '0'})
+ },
+ 'extensions.shellversion': {
+ 'Meta': {'object_name': 'ShellVersion'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'major': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'minor': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'point': ('django.db.models.fields.IntegerField', [], {})
+ },
+ 'ratings.ratingcomment': {
+ 'Meta': {'ordering': "('submit_date',)", 'object_name': 'RatingComment', '_ormbases': ['comments.Comment']},
+ 'comment_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['comments.Comment']", 'unique': 'True', 'primary_key': 'True'}),
+ 'rating': ('django.db.models.fields.PositiveIntegerField', [], {})
+ },
+ 'sites.site': {
+ 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"},
+ 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ }
+ }
+
+ complete_apps = ['ratings', 'extensions']
diff --git a/sweettooth/extensions/models.py b/sweettooth/extensions/models.py
index 0b13fdf..4530b73 100644
--- a/sweettooth/extensions/models.py
+++ b/sweettooth/extensions/models.py
@@ -4,6 +4,7 @@ try:
except ImportError:
import simplejson as json
+import math
import uuid
from zipfile import ZipFile, BadZipfile
@@ -63,6 +64,28 @@ def build_shell_version_map(versions):
return shell_version_map
+
+# Ported to Python, from https://github.com/reddit/reddit/blob/master/r2/r2/lib/db/_sorts.pyx
+# Licensed under the CPAL 1.0, written by Reddit, Inc.
+
+def confidence(ups, downs):
+ """The confidence sort.
+http://www.evanmiller.org/how-not-to-sort-by-average-rating.html"""
+ n = ups + downs
+
+ if n == 0:
+ return 0
+
+ z = 1.281551565545 # 80% confidence
+ p = float(ups) / n
+
+ left = p + 1/(2*n)*z*z
+ right = z*math.sqrt(p*(1-p)/n + z*z/(4*n*n))
+ under = 1+1/n*z*z
+
+ return (left - right) / under
+
+
class Extension(models.Model):
name = models.CharField(max_length=200)
uuid = models.CharField(max_length=200, unique=True, db_index=True)
@@ -73,6 +96,8 @@ class Extension(models.Model):
created = models.DateTimeField(auto_now_add=True)
downloads = models.PositiveIntegerField(default=0)
+ rating = models.FloatField(default=0)
+
class Meta:
permissions = (
("can-modify-data", "Can modify extension data"),
@@ -141,7 +166,28 @@ class Extension(models.Model):
return reverse('extensions-detail', kwargs=dict(pk=self.pk,
slug=self.slug))
+ @property
+ def likes(self):
+ return self.like_trackers.filter(vote=True).count()
+
+ @property
+ def dislikes(self):
+ return self.like_trackers.filter(vote=False).count()
+
+ def recalculate_rating(self):
+ self.rating = confidence(self.likes, self.dislikes)
+
+class ExtensionLikeTracker(models.Model):
+ extension = models.ForeignKey(Extension, db_index=True,
+ related_name='like_trackers')
+ user = models.ForeignKey(User, db_index=True)
+ vote = models.BooleanField(db_index = True)
+
+ def is_like(self):
+ return self.vote
+ def is_dislike(self):
+ return not self.vote
class ExtensionPopularityItem(models.Model):
extension = models.ForeignKey(Extension, db_index=True,
diff --git a/sweettooth/extensions/templates/extensions/comments.html b/sweettooth/extensions/templates/extensions/comments.html
index 96a9d70..823285d 100644
--- a/sweettooth/extensions/templates/extensions/comments.html
+++ b/sweettooth/extensions/templates/extensions/comments.html
@@ -27,6 +27,23 @@
<div>
<h4>Your opinion</h4>
{% if request.user.is_authenticated %}
+ <div class="binary-rating">
+ <div class="binary-rating-like {% if like_tracker.is_like %} depressed {% endif %}">
+ <span>I like it!</span>
+ </div>
+
+ <div class="binary-rating-dislike {% if like_tracker.is_dislike %} depressed {% endif %}">
+ <span>I hate it!</span>
+ </div>
+ </div>
+ {% endif %}
+
+ <div class="binary-rating-stats">
+ <div class="binary-rating-stats-likes" style="width: {{ like_percent }}%;"></div>
+ <div class="binary-rating-stats-dislikes" style="width: {{ dislike_percent }}%;"></div>
+ </div>
+
+ {% if request.user.is_authenticated %}
<div class="report rating">
<span class="txt">It worked, and...</span>
{% render_comment_form for extension %}
diff --git a/sweettooth/extensions/urls.py b/sweettooth/extensions/urls.py
index 6dc7214..4a0e73f 100644
--- a/sweettooth/extensions/urls.py
+++ b/sweettooth/extensions/urls.py
@@ -22,6 +22,7 @@ ajax_patterns = patterns('',
dict(newstatus=models.STATUS_INACTIVE), name='extensions-ajax-set-status-inactive'),
url(r'^extensions-list/', views.ajax_extensions_list),
url(r'^adjust-popularity/', views.ajax_adjust_popularity_view),
+ url(r'^adjust-rating/', views.ajax_adjust_rating_view),
)
shell_patterns = patterns('',
diff --git a/sweettooth/extensions/views.py b/sweettooth/extensions/views.py
index 92a7a85..90f054f 100644
--- a/sweettooth/extensions/views.py
+++ b/sweettooth/extensions/views.py
@@ -139,6 +139,43 @@ def ajax_extensions_list(request):
return dict(html=render_to_string('extensions/list_bare.html', context),
numpages = paginator.num_pages)
+
+def standard_extension_context(extension, request, wants_tracker=True):
+ tracker = None
+
+ total_votes = extension.like_trackers.count()
+ likes = extension.likes
+ dislikes = extension.dislikes
+
+ if total_votes == 0:
+ like_percent = 0
+ dislike_percent = 0
+ else:
+ like_percent = likes / float(total_votes) * 100
+ dislike_percent = dislikes / float(total_votes) * 100
+
+ context = dict(like_percent = like_percent,
+ dislike_percent = dislike_percent,
+ likes = likes,
+ dislikes = dislikes)
+
+ if wants_tracker:
+ if request.user.is_authenticated():
+ try:
+ tracker = models.ExtensionLikeTracker.objects.get(user=request.user,
+ extension=extension)
+ except models.ExtensionLikeTracker.DoesNotExist:
+ pass
+
+ if tracker is None:
+ # Rely on the fact that in Django's template language, foo.is_like
+ # can do a dict lookup after an attribute lookup.
+ tracker = dict(is_like=False, is_dislike=False)
+
+ context['like_tracker'] = tracker
+
+ return context
+
@model_view(models.Extension)
def extension_view(request, obj, **kwargs):
extension, versions = obj, obj.visible_versions
@@ -165,6 +202,8 @@ def extension_view(request, obj, **kwargs):
all_versions = extension.versions.order_by('-version'),
is_visible = True,
is_multiversion = True)
+ context.update(standard_extension_context(extension, request))
+
return render(request, template_name, context)
@model_view(models.ExtensionVersion)
@@ -213,12 +252,38 @@ def extension_version_view(request, obj, **kwargs):
is_rejected = status in models.REJECTED_STATUSES,
is_new_extension = (extension.versions.count() == 1),
status = status)
+ context.update(standard_extension_context(extension, request))
if extension.latest_version is not None:
context['old_version'] = version.version < extension.latest_version.version
return render(request, template_name, context)
@require_POST
+ login_required
+ ajax_view
+def ajax_adjust_rating_view(request):
+ uuid = request.POST['uuid']
+ action = request.POST['action']
+
+ extension = models.Extension.objects.get(uuid=uuid)
+
+ tracker, created = models.ExtensionLikeTracker.objects.get_or_create(user=request.user,
+ extension=extension)
+
+ if action == 'like':
+ tracker.vote = True
+ elif action == 'dislike':
+ tracker.vote = False
+ else:
+ return HttpResponseServerError()
+
+ tracker.save()
+ extension.recalculate_rating()
+ extension.save()
+
+ return standard_extension_context(extension, None, wants_tracker=False)
+
+ require_POST
@ajax_view
def ajax_adjust_popularity_view(request):
uuid = request.POST['uuid']
diff --git a/sweettooth/static/css/sweettooth.css b/sweettooth/static/css/sweettooth.css
index 58c425c..da9a064 100644
--- a/sweettooth/static/css/sweettooth.css
+++ b/sweettooth/static/css/sweettooth.css
@@ -600,6 +600,80 @@ input[type=submit], button {
float: right;
}
+/* Ratings */
+/* ==================================================================== */
+
+.binary-rating {
+ overflow: auto;
+ margin-bottom: 0.2em;
+}
+
+.binary-rating div {
+ float: left;
+ width: 49%;
+ font-size: 1.4em;
+ line-height: 2.5em;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ text-align: center;
+ background-image: linear-gradient(270deg, rgba(255,255,255,0.4), rgba(255,255,255,0.1));
+ background-image: -moz-linear-gradient(270deg, rgba(255,255,255,0.4), rgba(255,255,255,0.1));
+ background-image: -webkit-linear-gradient(270deg, rgba(255,255,255,0.4), rgba(255,255,255,0.1));
+ cursor: pointer;
+}
+
+.binary-rating div:hover {
+ background-image: linear-gradient(270deg, rgba(255,255,255,0.6), rgba(255,255,255,0.1));
+ background-image: -moz-linear-gradient(270deg, rgba(255,255,255,0.6), rgba(255,255,255,0.1));
+ background-image: -webkit-linear-gradient(270deg, rgba(255,255,255,0.6), rgba(255,255,255,0.1));
+}
+
+.binary-rating div.depressed {
+ background-image: none;
+ box-shadow: inset 0 3px 10px 2px rgba(0,0,0,0.2);
+ cursor: auto;
+}
+
+.binary-rating-like {
+ background-color: #aaeeaa;
+}
+
+.binary-rating-dislike {
+ background-color: #eeaaaa;
+}
+
+.binary-rating div:first-child {
+ border-radius: 8px 0 0 8px;
+}
+
+.binary-rating div:last-child {
+ border-radius: 0 8px 8px 0;
+}
+
+.binary-rating-stats {
+ width: 99%;
+ border: 1px solid #ccc;
+ margin-bottom: 1em;
+}
+
+.binary-rating-stats,
+.binary-rating-stats-likes,
+.binary-rating-stats-dislikes {
+ height: 6px;
+}
+
+.binary-rating-stats-likes,
+.binary-rating-stats-dislikes {
+ float: left;
+}
+
+.binary-rating-stats-likes {
+ background-color: #44bb44;
+}
+
+.binary-rating-stats-dislikes {
+ background-color: #bb4444;
+}
+
/* Review List */
/* ==================================================================== */
diff --git a/sweettooth/static/js/main.js b/sweettooth/static/js/main.js
index 4c3596f..580fdd4 100644
--- a/sweettooth/static/js/main.js
+++ b/sweettooth/static/js/main.js
@@ -145,5 +145,32 @@ require(['jquery', 'messages', 'extensions', 'uploader',
$('.screenshot.upload').uploadify('/ajax/upload/screenshot/'+pk+'?geometry=300x200');
$('.icon.upload').uploadify('/ajax/upload/icon/'+pk);
}
+
+ var uuid = $('.extension').data('uuid');
+ if (uuid) {
+ var $brd = $('.binary-rating div');
+ var ratingClick = function(action) {
+ return function() {
+ if ($(this).hasClass('depressed'))
+ return;
+
+ var d = $.ajax({ type: 'POST',
+ url: '/ajax/adjust-rating/',
+ data: { uuid: uuid,
+ action: action }});
+
+ d.done(function(result) {
+ $('.binary-rating-stats-likes').css('width', result.like_percent + '%');
+ $('.binary-rating-stats-dislikes').css('width', result.dislike_percent + '%');
+ });
+
+ $brd.removeClass('depressed');
+ $(this).addClass('depressed');
+ };
+ };
+
+ $('.binary-rating-like').click(ratingClick('like'));
+ $('.binary-rating-dislike').click(ratingClick('dislike'));
+ }
});
});
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]