[extensions-web/filter-sort-ui: 10/20] Add a binary rating system to replace star ratings



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]