[extensions-web] Make ratings optional



commit a4a9d8beaf6f5b5d182e2d8137f3a5b2d8c19892
Author: Jasper St. Pierre <jstpierre mecheye net>
Date:   Tue Jan 31 13:00:13 2012 -0500

    Make ratings optional
    
    "Ratings" were meant to be in-depth reviews of an extension, explaining what a
    user liked or didn't like about an extension, which should hopefully give the
    extension author insightful feedback about the quality of what they created and
    the path forward for developing and improving their extension.
    
    That never happened. The "ratings" section is used like a discussions board
    for the extension. The section isn't really designed as a discussion board,
    without proper replies or pagination or anything modern (go Django!).
    Part of this may be that there wasn't a separate discussion board for users
    to get their gripes about GNOME Shell and "I LOVE IT"s out.
    
    Rather than introduce a new system, let's try and jiggle the UI around a bit.
    The comment form is now divided into three choices: you can "Leave a comment",
    a "rating", or a "bug report" (which links to the error report system as
    before).
    
    Comments are just specialized versions of ratings with the "rating" field
    fixed to -1. The UI to pick a star rating doesn't appear, and the list of
    comments won't show the stars at all.

 .../extensions/templates/extensions/comments.html  |   43 +-
 sweettooth/ratings/forms.py                        |   50 +--
 sweettooth/ratings/migrations/0001_initial.py      |   91 ++++
 .../0002_auto__chg_field_ratingcomment_rating.py   |   87 ++++
 sweettooth/ratings/models.py                       |    2 +-
 sweettooth/ratings/templates/comments/form.html    |    4 +-
 sweettooth/ratings/templatetags/rating.py          |   33 --
 sweettooth/settings.py                             |    2 +-
 sweettooth/static/css/sweettooth.css               |   87 ++--
 sweettooth/static/js/jquery.rating.js              |  383 ----------------
 sweettooth/static/js/jquery.raty.js                |  471 ++++++++++++++++++++
 sweettooth/static/js/main.js                       |   33 ++-
 12 files changed, 752 insertions(+), 534 deletions(-)
---
diff --git a/sweettooth/extensions/templates/extensions/comments.html b/sweettooth/extensions/templates/extensions/comments.html
index af82994..cea9902 100644
--- a/sweettooth/extensions/templates/extensions/comments.html
+++ b/sweettooth/extensions/templates/extensions/comments.html
@@ -13,9 +13,9 @@
   {% endif %}
     <img src="{% gravatar_url comment.email %}" class="gravatar">
     <div class="rating-author">
-      <div class="rating">
-        {% rating comment.rating 5 %}
-      </div> by
+      {% if comment.rating != -1 %}
+      <div class="rating" data-rating-value="{{ comment.rating }}"></div> by
+      {% endif %}
       <a class="comment-author" href="{% url auth-profile user=comment.user.username %}">{{ comment.user }}</a>
     </div>
     <p>{{ comment.comment }}</p>
@@ -26,26 +26,21 @@
   There are no comments.
   {% endfor %}
 </div>
-<div id="new-comment-form">
-  <div>
-    <h4>Your opinion</h4>
-    {% if request.user.is_authenticated %}
-    <div class="report rating">
-      <span class="txt">It worked, and...</span>
-      {% render_comment_form for extension %}
-    </div>
-
-    <div class="report error">
-      <a class="txt" href="{% url errorreports-report pk=extension.pk %}" class="report error">Help! It didn't work!</a>
-      <p>
-        Errors should be reported through the error reporting tool &mdash; it
-        helps extension authors be known about bugs in their extension!
-      </p>
-    </div>
-    {% else %}
-    <p class="unauthenticated">
-      Unfortunately, to help prevent spam, we require that you <a href="{% url auth-login %}">log in to GNOME Shell Extensions</a> in order to post a comment or report an error. You understand, right?
-    </p>
-    {% endif %}
+<div id="opinion_form">
+  <h4>Your opinion</h4>
+  {% if request.user.is_authenticated %}
+  <div class="comment_choice">
+    Leave a...
+    <a href="javascript:void 0" id="leave_comment">Comment</a>
+    <a href="javascript:void 0" id="leave_rating">Rating</a>
+    <a href="{% url errorreports-report pk=extension.pk %}">Bug report</a>
+  </div>
+  <div id="rating_form">
+    {% render_comment_form for extension %}
   </div>
+  {% else %}
+  <p class="unauthenticated">
+    Unfortunately, to help prevent spam, we require that you <a href="{% url auth-login %}">log in to GNOME Shell Extensions</a> in order to post a comment or report an error. You understand, right?
+  </p>
+  {% endif %}
 </div>
diff --git a/sweettooth/ratings/forms.py b/sweettooth/ratings/forms.py
index 000884d..9325515 100644
--- a/sweettooth/ratings/forms.py
+++ b/sweettooth/ratings/forms.py
@@ -10,45 +10,21 @@ from django.utils.safestring import mark_safe
 
 from ratings.models import RatingComment
 
-CHOICES = [(i, str(i+1)) for i in range(5)]
-
-class NoLabelRadioInput(widgets.RadioInput):
-    """
-    Like RadioInput, but with no label.
-    """
-
-    def __unicode__(self):
-        return self.tag()
-
-class NoSoapRadio(StrAndUnicode):
-    """
-    Like RadioSelectRenderer, but without lists.
-    """
-
-    def __init__(self, name, value, attrs, choices):
-        self.name, self.value, self.attrs = name, value, attrs
-        self.choices = choices
-
-    def __iter__(self):
-        for i, choice in enumerate(self.choices):
-            yield NoLabelRadioInput(self.name, self.value, self.attrs.copy(), choice, i)
-
-    def __getitem__(self, idx):
-        choice = self.choices[idx] # Let the IndexError propogate
-        return NoLabelRadioInput(self.name, self.value, self.attrs.copy(), choice, idx)
-
-    def __unicode__(self):
-        return self.render()
-
-    def render(self):
-        """Outputs a <ul> for this set of radio fields."""
-        return mark_safe(u'\n'.join(force_unicode(w) for w in self))
+# Raty inserts its own <input> element, so we don't want to provide
+# a widget here. We'll insert a <div> for raty to fill in the template.
+class NoOpWidget(widgets.Widget):
+    def render(self, *a, **kw):
+        return u''
 
 class RatingCommentForm(CommentForm):
-    rating = fields.IntegerField(min_value=0, max_value=4,
-                                 widget=widgets.RadioSelect(choices=CHOICES,
-                                                            renderer=NoSoapRadio,
-                                                            attrs={"class": "rating"}))
+    rating = fields.IntegerField(min_value=-1, max_value=4,
+                                 required=False, widget=NoOpWidget())
+
+    def clean_rating(self):
+        rating = self.cleaned_data["rating"]
+        if rating is None:
+            rating = -1
+        return rating
 
     def get_comment_model(self):
         return RatingComment
diff --git a/sweettooth/ratings/migrations/0001_initial.py b/sweettooth/ratings/migrations/0001_initial.py
new file mode 100644
index 0000000..6c0a17e
--- /dev/null
+++ b/sweettooth/ratings/migrations/0001_initial.py
@@ -0,0 +1,91 @@
+# 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 'RatingComment'
+        db.create_table('ratings_ratingcomment', (
+            ('comment_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['comments.Comment'], unique=True, primary_key=True)),
+            ('rating', self.gf('django.db.models.fields.PositiveIntegerField')()),
+        ))
+        db.send_create_signal('ratings', ['RatingComment'])
+
+
+    def backwards(self, orm):
+        
+        # Deleting model 'RatingComment'
+        db.delete_table('ratings_ratingcomment')
+
+
+    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'})
+        },
+        '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']
diff --git a/sweettooth/ratings/migrations/0002_auto__chg_field_ratingcomment_rating.py b/sweettooth/ratings/migrations/0002_auto__chg_field_ratingcomment_rating.py
new file mode 100644
index 0000000..abee6e6
--- /dev/null
+++ b/sweettooth/ratings/migrations/0002_auto__chg_field_ratingcomment_rating.py
@@ -0,0 +1,87 @@
+# 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):
+        
+        # Changing field 'RatingComment.rating'
+        db.alter_column('ratings_ratingcomment', 'rating', self.gf('django.db.models.fields.IntegerField')())
+
+
+    def backwards(self, orm):
+        
+        # Changing field 'RatingComment.rating'
+        db.alter_column('ratings_ratingcomment', 'rating', self.gf('django.db.models.fields.PositiveIntegerField')())
+
+
+    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'})
+        },
+        '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.IntegerField', [], {'default': '-1', 'blank': 'True'})
+        },
+        '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']
diff --git a/sweettooth/ratings/migrations/__init__.py b/sweettooth/ratings/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sweettooth/ratings/models.py b/sweettooth/ratings/models.py
index 964cf36..af31923 100644
--- a/sweettooth/ratings/models.py
+++ b/sweettooth/ratings/models.py
@@ -4,7 +4,7 @@ from django.contrib.comments.models import Comment
 from django.contrib.comments.signals import comment_will_be_posted
 
 class RatingComment(Comment):
-    rating = models.PositiveIntegerField()
+    rating = models.IntegerField(blank=True, default=-1)
 
 def make_sure_user_was_authenticated(sender, comment, request, **kwargs):
     return request.user.is_authenticated()
diff --git a/sweettooth/ratings/templates/comments/form.html b/sweettooth/ratings/templates/comments/form.html
index b0e108a..98b9542 100644
--- a/sweettooth/ratings/templates/comments/form.html
+++ b/sweettooth/ratings/templates/comments/form.html
@@ -11,11 +11,11 @@
       <p
         {% if field.errors %} class="error"{% endif %}
         {% ifequal field.name "honeypot" %} style="display:none;"{% endifequal %}>
-        {% ifnotequal field.name "rating" %} {{ field.label_tag }} {% endifnotequal %}
         {{ field }}
       </p>
     {% endif %}
     {% endifnotequal %}
   {% endfor %}
-  <input type="submit" name="post" class="submit-post" value="{% trans "Post" %}" />
+  <div class="rating"></div>
+  <input type="submit" name="post" value="{% trans "Post" %}" />
 </form>
diff --git a/sweettooth/settings.py b/sweettooth/settings.py
index 084e164..d49c8e8 100644
--- a/sweettooth/settings.py
+++ b/sweettooth/settings.py
@@ -2,7 +2,7 @@
 
 import os
 
-DEBUG = False
+DEBUG = True
 TEMPLATE_DEBUG = DEBUG
 
 ADMINS = (
diff --git a/sweettooth/static/css/sweettooth.css b/sweettooth/static/css/sweettooth.css
index 3d4de1b..4902650 100644
--- a/sweettooth/static/css/sweettooth.css
+++ b/sweettooth/static/css/sweettooth.css
@@ -412,49 +412,6 @@ li.extension:last-child {
     padding: 0 1em 0 0;
 }
 
-.extension .report {
-    overflow: auto;
-}
-
-.extension .report .txt {
-    display: block;
-    height: 32px;
-    padding-left: 36px;
-    font-size: 16pt;
-    background-repeat: no-repeat;
-}
-
-.extension .report.error .txt {
-    background-image: url(../images/face-angry.png);
-}
-
-.extension .report.rating .txt {
-    background-image: url(../images/face-smile.png);
-    margin-bottom: 10px;
-}
-
-.extension .report p {
-    line-height: 1.7em;
-}
-
-.extension .report.error, .extension .unauthenticated {
-    background: #EEEEEC;
-    border: 1px solid #BABDB6;
-    border-radius: 8px;
-    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
-    clear: both;
-    padding: 20px;
-
-    /* Extra margin on the bottom, left and right is for
-     * the shadow -- we use overflow: hidden on the parent
-     * to establish a BFC, so it will clip the shadow. */
-    margin: 20px 4px 4px 4px;
-}
-
-.extension .report.error p {
-    padding-left: 16px;
-}
-
 .extension table {
     width: 100%;
 }
@@ -632,19 +589,23 @@ input[type=submit], button {
 }
 
 
-/* New comment form */
+/* Opinion form */
 /* ==================================================================== */
 
-#new-comment-form {
+#opinion_form {
     overflow: hidden;
 }
 
-#new-comment-form label {
+#opinion_form label {
     display: block;
 }
 
-#new-comment-form p input,
-#new-comment-form p textarea {
+#opinion_form .rating {
+    float: left;
+}
+
+#opinion_form p input,
+#opinion_form p textarea {
     width: 100%;
     -moz-box-sizing: border-box;
     -webkit-box-sizing: border-box;
@@ -652,15 +613,41 @@ input[type=submit], button {
     padding: 3px;
 }
 
-#new-comment-form p textarea {
+#opinion_form p textarea {
     resize: vertical;
     min-height: 150px;
 }
 
-#new-comment-form .submit-post {
+#opinion_form input[type=submit] {
+    height: 32px;
     float: right;
 }
 
+#opinion_form .comment_choice {
+    display: block;
+    height: 32px;
+    font-size: 16pt;
+    background-repeat: no-repeat;
+}
+
+#opinion_form .comment_choice a.selected {
+    font-weight: bold;
+}
+
+#opinion_form .unauthenticated {
+    background: #EEEEEC;
+    border: 1px solid #BABDB6;
+    border-radius: 8px;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+    clear: both;
+    padding: 20px;
+
+    /* Extra margin on the bottom, left and right is for
+     * the shadow -- we use overflow: hidden on the parent
+     * to establish a BFC, so it will clip the shadow. */
+    margin: 20px 4px 4px 4px;
+}
+
 /* Review List */
 /* ==================================================================== */
 
diff --git a/sweettooth/static/js/jquery.raty.js b/sweettooth/static/js/jquery.raty.js
new file mode 100755
index 0000000..c6b4255
--- /dev/null
+++ b/sweettooth/static/js/jquery.raty.js
@@ -0,0 +1,471 @@
+/*!
+ * jQuery Raty - A Star Rating Plugin - http://wbotelhos.com/raty
+ * ---------------------------------------------------------------------
+ *
+ * jQuery Raty is a plugin that generates a customizable star rating.
+ *
+ * Licensed under The MIT License
+ *
+ * @version        2.1.0
+ * @since          2010.06.11
+ * @author         Washington Botelho
+ * @documentation  wbotelhos.com/raty
+ * @twitter        twitter.com/wbotelhos
+ *
+ * Usage with default values:
+ * ---------------------------------------------------------------------
+ * $('#star').raty();
+ *
+ * <div id="star"></div>
+ *
+ * $('.star').raty();
+ *
+ * <div class="star"></div>
+ * <div class="star"></div>
+ * <div class="star"></div>
+ *
+ */
+
+;(function($) {
+
+	var methods = {
+		init: function(options) {
+			return this.each(function() {
+
+				var opt		= $.extend({}, $.fn.raty.defaults, options),
+					$this	= $(this).data('options', opt);
+
+				if (opt.number > 20) {
+					opt.number = 20;
+				} else if (opt.number < 0) {
+					opt.number = 0;
+				}
+
+				if (opt.round.down === undefined) {
+					opt.round.down = $.fn.raty.defaults.round.down;
+				}
+
+				if (opt.round.full === undefined) {
+					opt.round.full = $.fn.raty.defaults.round.full;
+				}
+
+				if (opt.round.up === undefined) {
+					opt.round.up = $.fn.raty.defaults.round.up;
+				}
+
+				if (opt.path.substring(opt.path.length - 1, opt.path.length) != '/') {
+					opt.path += '/';
+				}
+
+				if (typeof opt.start == 'function') {
+					opt.start = opt.start.call(this);
+				}
+
+				var isValidStart	= !isNaN(parseInt(opt.start, 10)),
+					start			= '';
+
+				if (isValidStart) {
+					start = (opt.start > opt.number) ? opt.number : opt.start;
+				} 
+
+				var starFile	= opt.starOn,
+					space		= (opt.space) ? 4 : 0,
+					hint		= '';
+
+				for (var i = 1; i <= opt.number; i++) {
+					starFile = (start < i) ? opt.starOff : opt.starOn;
+
+					hint = (i <= opt.hintList.length && opt.hintList[i - 1] !== null) ? opt.hintList[i - 1] : i;
+
+					$this.append('<img src="' + opt.path + starFile + '" alt="' + i + '" title="' + hint + '" />');
+
+					if (opt.space) {
+						$this.append((i < opt.number) ? '&nbsp;' : '');
+					}
+				}
+
+				var $score = $('<input/>', { type: 'hidden', name: opt.scoreName}).appendTo($this);
+
+				if (isValidStart) {
+					if (opt.start > 0) {
+						$score.val(start);
+					}
+
+					methods.roundStar.call($this, start);
+				}
+
+				if (opt.iconRange) {
+					methods.fillStar.call($this, start);	
+				}
+
+				methods.setTarget.call($this, start, opt.targetKeep);
+
+				var width = opt.width || (opt.number * opt.size + opt.number * space);
+
+				if (opt.cancel) {
+					var $cancel = $('<img src="' + opt.path + opt.cancelOff + '" alt="x" title="' + opt.cancelHint + '" class="raty-cancel"/>');
+
+					if (opt.cancelPlace == 'left') {
+						$this.prepend('&nbsp;').prepend($cancel);
+					} else {
+						$this.append('&nbsp;').append($cancel);
+					}
+
+					width += opt.size + space;
+				}
+
+				if (opt.readOnly) {
+					methods.fixHint.call($this);
+
+					$this.children('.raty-cancel').hide();
+				} else {
+					$this.css('cursor', 'pointer');
+
+					methods.bindAction.call($this);
+				}
+
+				$this.css('width', width);
+			});
+		}, bindAction: function() {
+			var self	= this,
+				opt		= this.data('options'),
+				$score	= this.children('input');
+
+			self.mouseleave(function() {
+				methods.initialize.call(self, $score.val());
+
+				methods.setTarget.call(self, $score.val(), opt.targetKeep);
+			});
+
+			var $stars	= this.children('img').not('.raty-cancel'),
+				action	= (opt.half) ? 'mousemove' : 'mouseover';
+
+			if (opt.cancel) {
+				self.children('.raty-cancel').mouseenter(function() {
+					$(this).attr('src', opt.path + opt.cancelOn);
+
+					$stars.attr('src', opt.path + opt.starOff);
+
+					methods.setTarget.call(self, null, true);
+				}).mouseleave(function() {
+					$(this).attr('src', opt.path + opt.cancelOff);
+
+					self.mouseout();
+				}).click(function(evt) {
+					$score.removeAttr('value');
+
+					if (opt.click) {
+			          opt.click.call(self[0], null, evt);
+			        }
+				});
+			}
+
+			$stars.bind(action, function(evt) {
+				var value = parseInt(this.alt, 10);
+
+				if (opt.half) {
+					var position	= parseFloat((evt.pageX - $(this).offset().left) / opt.size),
+						diff		= (position > .5) ? 1 : .5;
+
+					value = parseFloat(this.alt) - 1 + diff;
+
+					methods.fillStar.call(self, value);
+
+					if (opt.precision) {
+						value = value - diff + position;
+					}
+
+					methods.showHalf.call(self, value);
+				} else {
+					methods.fillStar.call(self, value);
+				}
+
+				self.data('score', value);
+
+				methods.setTarget.call(self, value, true);
+			}).click(function(evt) {
+				$score.val((opt.half || opt.precision) ? self.data('score') : this.alt);
+
+				if (opt.click) {
+					opt.click.call(self[0], $score.val(), evt);
+				}
+			});
+		}, cancel: function(isClick) {
+			return this.each(function() {
+				var $this = $(this);
+
+				if ($this.data('readonly') == 'readonly') {
+					return false;
+				}
+
+				if (isClick) {
+					methods.click.call($this, null);
+				} else {
+					methods.start.call($this, null);
+				}
+
+				$this.mouseleave().children('input').removeAttr('value');
+			});
+		}, click: function(score) {
+			return this.each(function() {
+				var $this = $(this);
+
+				if ($this.data('readonly') == 'readonly') {
+					return false;
+				}
+
+				methods.initialize.call($this, score);
+
+				var opt = $this.data('options');
+
+				if (opt.click) {
+					opt.click.call($this[0], score);
+				} else {
+					$.error('you must add the "click: function(score, evt) { }" callback.');
+				}
+
+				methods.setTarget.call($this, score, true);
+			});
+		}, fillStar: function(score) {
+			var opt		= this.data('options'),
+				$stars	= this.children('img').not('.raty-cancel'),
+				qtyStar	= $stars.length,
+				count	= 0,
+				$star	,
+				star	,
+				icon	;
+
+			for (var i = 1; i <= qtyStar; i++) {
+				$star = $stars.eq(i - 1);
+
+				if (opt.iconRange && opt.iconRange.length > count) {
+					star = opt.iconRange[count];
+
+					if (opt.single) {
+						icon = (i == score) ? (star.on || opt.starOn) : (star.off || opt.starOff);
+					} else {
+						icon = (i <= score) ? (star.on || opt.starOn) : (star.off || opt.starOff);
+					}
+
+					if (i <= star.range) {
+						$star.attr('src', opt.path + icon);
+					}
+
+					if (i == star.range) {
+						count++;
+					}
+				} else {
+					if (opt.single) {
+						icon = (i == score) ? opt.starOn : opt.starOff;
+					} else {
+						icon = (i <= score) ? opt.starOn : opt.starOff;
+					}
+
+					$star.attr('src', opt.path + icon);
+				}
+			}
+		}, fixHint: function() {
+			var opt		= this.data('options'),
+				$score	= this.children('input'),
+				score	= parseInt($score.val(), 10),
+				hint	= opt.noRatedMsg;
+
+			if (!isNaN(score) && score > 0) {
+				hint = (score <= opt.hintList.length && opt.hintList[score - 1] !== null) ? opt.hintList[score - 1] : score;
+			}
+
+			$score.attr('readonly', 'readonly');
+			this.css('cursor', 'default').data('readonly', 'readonly').attr('title', hint).children('img').attr('title', hint);
+		}, readOnly: function(isReadOnly) {
+			return this.each(function() {
+				var $this	= $(this),
+					$cancel	= $this.children('.raty-cancel');
+
+				if ($cancel.length) {
+					if (isReadOnly) {
+						$cancel.hide();
+					} else {
+						$cancel.show();
+					}
+				}
+
+				if (isReadOnly) {
+					$this.unbind();
+
+					$this.children('img').unbind();
+
+					methods.fixHint.call($this);
+				} else {
+					methods.bindAction.call($this);
+
+					methods.unfixHint.call($this);
+				}
+			});
+		}, roundStar: function(score) {
+			var opt		= this.data('options'),
+				diff	= (score - Math.floor(score)).toFixed(2);
+
+			if (diff > opt.round.down) {
+				var icon = opt.starOn;						// Full up: [x.76 .. x.99]
+
+				if (diff < opt.round.up && opt.halfShow) {	// Half: [x.26 .. x.75]
+					icon = opt.starHalf;
+				} else if (diff < opt.round.full) {			// Full down: [x.00 .. x.5]
+					icon = opt.starOff;
+				}
+
+				this.children('img').not('.raty-cancel').eq(Math.ceil(score) - 1).attr('src', opt.path + icon);
+			}												// Full down: [x.00 .. x.25]
+		}, score: function() {
+			var score	= [],
+				value	;
+
+			this.each(function() {
+				value = $(this).children('input').val();
+				value = (value == '') ? null : parseFloat(value);
+
+				score.push(value);
+			});
+
+			return (score.length > 1) ? score : score[0];
+		}, setTarget: function(value, isKeep) {
+			var opt = this.data('options');
+
+			if (opt.target) {
+				var $target = $(opt.target);
+
+				if ($target.length == 0) {
+					$.error('target selector invalid or missing!');
+				} else {
+					var score = value;
+
+					if (score == null && !opt.cancel) {
+						$.error('you must enable the "cancel" option to set hint on target.');
+					} else {
+						if (!isKeep || score == '') {
+							score = opt.targetText;
+						} else {
+							if (opt.targetType == 'hint') {
+								if (score === null && opt.cancel) {
+									score = opt.cancelHint;
+								} else {
+									score = opt.hintList[Math.ceil(score - 1)];
+								}
+							} else {
+								if (score != '' && !opt.precision) {
+									score = parseInt(score, 10);
+								} else {
+									score = parseFloat(score).toFixed(1);
+								}
+							}
+						}
+
+						if (opt.targetFormat.indexOf('{score}') < 0) {
+							$.error('template "{score}" missing!');
+						} else if (value !== null) {
+							score = opt.targetFormat.toString().replace('{score}', score);
+						}
+
+						if ($target.is(':input')) {
+							$target.val(score);
+						} else {
+							$target.html(score);
+						}
+					}
+				}
+			}
+		}, showHalf: function(score) {
+			var opt		= this.data('options'),
+				diff	= (score - Math.floor(score)).toFixed(1);
+
+			if (diff > 0 && diff < .6) {
+				this.children('img').not('.raty-cancel').eq(Math.ceil(score) - 1).attr('src', opt.path + opt.starHalf);
+			}
+		}, start: function(score) {
+			return this.each(function() {
+				var $this = $(this);
+
+				if ($this.data('readonly') == 'readonly') {
+					return false;
+				}
+
+				methods.initialize.call($this, score);
+
+				var opt = $this.data('options');
+
+				methods.setTarget.call($this, score, true);
+			});
+		}, initialize: function(score) {
+			var opt	= this.data('options');
+
+			if (score < 0) {
+				score = 0;
+			} else if (score > opt.number) {
+				score = opt.number;
+			}
+
+			methods.fillStar.call(this, score);
+
+			if (score != '') {
+				if (opt.halfShow) {
+					methods.roundStar.call(this, score);
+				}
+
+				this.children('input').val(score);
+			}
+		}, unfixHint: function() {
+			var opt		= this.data('options'),
+				$imgs	= this.children('img').filter(':not(.raty-cancel)');
+
+			for (var i = 0; i < opt.number; i++) {
+				$imgs.eq(i).attr('title', (i < opt.hintList.length && opt.hintList[i] !== null) ? opt.hintList[i] : i);
+			}
+
+			this.css('cursor', 'pointer').removeData('readonly').removeAttr('title').children('input').attr('readonly', 'readonly');
+		}
+	};
+
+	$.fn.raty = function(method) {
+		if (methods[method]) {
+			return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
+		} else if (typeof method === 'object' || !method) {
+			return methods.init.apply(this, arguments);
+		} else {
+			$.error('Method ' + method + ' does not exist!');
+		} 
+	};
+
+	$.fn.raty.defaults = {
+		cancel:			false,
+		cancelHint:		'cancel this rating!',
+		cancelOff:		'cancel-off.png',
+		cancelOn:		'cancel-on.png',
+		cancelPlace:	'left',
+		click:			undefined,
+		half:			false,
+		halfShow:		true,
+		hintList:		['bad', 'poor', 'regular', 'good', 'gorgeous'],
+		iconRange:		undefined,
+		noRatedMsg:		'not rated yet',
+		number:			5,
+		path:			'img/',
+		precision:		false,
+		round:			{ down: .25, full: .6, up: .76 },
+		readOnly:		false,
+		scoreName:		'score',
+		single:			false,
+		size:			16,
+		space:			true,
+		starHalf:		'star-half.png',
+		starOff:		'star-off.png',
+		starOn:			'star-on.png',
+		start:			0,
+		target:			undefined,
+		targetFormat:	'{score}',
+		targetKeep:		false,
+		targetText:		'',
+		targetType:		'hint',
+		width:			undefined
+	};
+
+})(jQuery);
diff --git a/sweettooth/static/js/main.js b/sweettooth/static/js/main.js
index 737c2ed..82cf4f4 100644
--- a/sweettooth/static/js/main.js
+++ b/sweettooth/static/js/main.js
@@ -3,7 +3,7 @@
 require(['jquery', 'messages', 'modal',
          'extensions', 'uploader', 'fsui',
          'jquery.cookie', 'jquery.jeditable',
-         'jquery.timeago', 'jquery.rating'], function($, messages, modal) {
+         'jquery.timeago', 'jquery.raty'], function($, messages, modal) {
     if (!$.ajaxSettings.headers)
         $.ajaxSettings.headers = {};
 
@@ -79,10 +79,37 @@ require(['jquery', 'messages', 'modal',
 
         $('#local_extensions').addLocalExtensions();
         $('.extension.single-page').addExtensionSwitch();
+
+        $.extend($.fn.raty.defaults, {
+            path: '/static/images/',
+            starOff: 'star-empty.png',
+            starOn: 'star-full.png',
+            size: 25
+        });
+
         $('.comment .rating').each(function() {
-            $(this).find('input').rating();
+            $(this).raty({
+                start: $(this).data('rating-value'),
+                readOnly: true
+            });
         });
-        $('form .rating').rating();
+        $('#rating_form').hide();
+        $('#rating_form .rating').raty({ scoreName: 'rating' });
+
+        function makeShowForm(isRating) {
+            return function() {
+                $('#leave_comment, #leave_rating').removeClass('selected');
+                $(this).addClass('selected');
+                var $rating = $('#rating_form').slideDown().find('.rating');
+                if (isRating)
+                    $rating.show();
+                else
+                    $rating.hide();
+            };
+        }
+
+        $('#leave_comment').click(makeShowForm(false));
+        $('#leave_rating').click(makeShowForm(true));
 
         $('.expandy_header').click(function() {
             $(this).toggleClass('expanded').next().slideToggle();



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]