changeset 0:2aa25c11dc8a

FiveStarVote plugin from http://trac-hacks.org/svn/fivestarvoteplugin/0.11/ at r13350
author IBBoard <dev@ibboard.co.uk>
date Sat, 10 Aug 2013 04:54:07 -0500
parents
children d118d75bc1d2
files fivestarvote/__init__.py fivestarvote/htdocs/css/fivestarvote.css fivestarvote/htdocs/css/rating.png fivestarvote/htdocs/js/fivestarvote.js setup.py
diffstat 5 files changed, 322 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/fivestarvote/__init__.py	Sat Aug 10 04:54:07 2013 -0500
@@ -0,0 +1,204 @@
+# Based on code from : http://trac-hacks.org/wiki/VotePlugin
+# Ported to a 5 star style voting system
+
+import re
+from trac.core import *
+from trac.config import ListOption
+from trac.env import IEnvironmentSetupParticipant
+from trac.web.api import IRequestFilter, IRequestHandler, Href
+from trac.web.chrome import ITemplateProvider, add_ctxtnav, add_stylesheet, \
+                            add_script, add_notice
+from trac.resource import get_resource_url
+from trac.db import DatabaseManager, Table, Column
+from trac.perm import IPermissionRequestor
+from trac.util import get_reporter_id
+from genshi import Markup, Stream
+from genshi.builder import tag
+from pkg_resources import resource_filename
+
+
+class FiveStarVoteSystem(Component):
+    """Allow up and down-voting on Trac resources."""
+
+    implements(ITemplateProvider, IRequestFilter, IRequestHandler,
+               IEnvironmentSetupParticipant, IPermissionRequestor)
+
+    voteable_paths = ListOption('fivestarvote', 'paths', '^/$,^/wiki*,^/ticket*',
+        doc='List of URL paths to allow voting on. Globs are supported.')
+
+    schema = [
+        Table('fivestarvote', key=('resource', 'username', 'vote'))[
+            Column('resource'),
+            Column('username'),
+            Column('vote', 'int'),
+            ]
+        ]
+
+    path_match = re.compile(r'/fivestarvote/([1-5])/(.*)')
+
+
+    # Public methods
+    def get_vote_counts(self, resource):
+        """Get total, count and tally vote counts and return them in a tuple."""
+        resource = self.normalise_resource(resource)
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        sql = "select sum(vote), count(*) from fivestarvote where (resource = '%s')" % resource
+        self.env.log.debug("sql:: %s" % sql)
+        cursor.execute(sql)
+        row = cursor.fetchone()
+        sum = row[0] or 0
+        total = row[1] or 0
+        tally = 0
+        if (total > 0):
+            tally = (sum / total)
+        return (sum, total, tally)
+
+    def get_vote(self, req, resource):
+        """Return the current users vote for a resource."""
+        resource = self.normalise_resource(resource)
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute('SELECT vote FROM fivestarvote WHERE username=%s '
+                       'AND resource = %s', (get_reporter_id(req), resource))
+        row = cursor.fetchone()
+        vote = row and row[0] or 0
+        return vote
+
+    def set_vote(self, req, resource, vote):
+        """Vote for a resource."""
+        resource = self.normalise_resource(resource)
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute('DELETE FROM fivestarvote WHERE username=%s '
+                       'AND resource = %s', (get_reporter_id(req), resource))
+        if vote:
+            cursor.execute('INSERT INTO fivestarvote (resource, username, vote) '
+                           'VALUES (%s, %s, %s)',
+                           (resource, get_reporter_id(req), vote))
+        db.commit()
+
+    # IPermissionRequestor methods
+    def get_permission_actions(self):
+        return ['VOTE_VIEW', 'VOTE_MODIFY']
+
+    # ITemplateProvider methods
+    def get_templates_dirs(self):
+        return []
+
+    def get_htdocs_dirs(self):
+        return [('fivestarvote', resource_filename(__name__, 'htdocs'))]
+
+    # IRequestHandler methods
+    def match_request(self, req):
+        return 'VOTE_VIEW' in req.perm and self.path_match.match(req.path_info)
+
+    def process_request(self, req):
+        req.perm.require('VOTE_MODIFY')
+        match = self.path_match.match(req.path_info)
+        vote, resource = match.groups()
+        resource = self.normalise_resource(resource)
+
+        self.set_vote(req, resource, vote)
+        self.env.log.debug("DAV::")
+
+        if req.args.get('js'):
+            percent, str, title = self.format_votes(resource)
+            req.send(','.join(("%s" % percent, str, title)))
+        
+        req.redirect(req.get_header('Referer'))
+
+    # IRequestFilter methods
+    def pre_process_request(self, req, handler):
+        if 'VOTE_VIEW' not in req.perm:
+            return handler
+
+        for path in self.voteable_paths:
+            if re.match(path, req.path_info):
+                self.render_voter(req)
+                break
+
+        return handler
+
+    def post_process_request(self, req, template, data, content_type):
+        return (template, data, content_type)
+
+    # IEnvironmentSetupParticipant methods
+    def environment_created(self):
+        self.upgrade_environment(self.env.get_db_cnx())
+
+    def environment_needs_upgrade(self, db):
+        cursor = db.cursor()
+        try:
+            cursor.execute("select count(*) from fivestarvote")
+            cursor.fetchone()
+            return False
+        except:
+            cursor.connection.rollback() 
+            return True
+
+    def upgrade_environment(self, db):
+        db_backend, _ = DatabaseManager(self.env)._get_connector()
+        cursor = db.cursor()
+        for table in self.schema:
+            for stmt in db_backend.to_sql(table):
+                self.env.log.debug(stmt)
+                cursor.execute(stmt)
+        db.commit()
+
+    # Internal methods
+    def render_voter(self, req):
+        resource = self.normalise_resource(req.path_info)
+
+        count = self.get_vote_counts(resource)
+
+        add_stylesheet(req, 'fivestarvote/css/fivestarvote.css')
+
+        names = ['', 'one', 'two', 'three', 'four', 'five']
+        els = []
+        percent = 0
+        if count[2] > 0:
+            percent = count[2] * 20
+
+        str = "Currently %s/5 stars." % count[2]
+        sign = '%'
+        style = "width: %s%s" % (percent, sign)
+        li = tag.li(str, class_='current-rating', style=style)
+        els.append(li)
+        for i in range(1, 6):
+            className = "item %s-star" % names[i]
+            href = "#"
+            if 'VOTE_MODIFY' in req.perm and get_reporter_id(req) != 'anonymous':
+                href = req.href.fivestarvote(i, resource)
+                add_script(req, 'fivestarvote/js/fivestarvote.js', mimetype='text/javascript')
+            a = tag.a(i, href=href, class_=className)
+            li = tag.li(a)
+            els.append(li)
+        
+        ul = tag.ul(els, class_='star-rating')
+        className = ''
+        if 'VOTE_MODIFY' in req.perm and get_reporter_id(req) != 'anonymous':
+            className = 'active'
+        title = "Current Vote: %s users voted for a total of %s" % (count[1], count[0]) 
+        add_ctxtnav(req, tag.span(tag.object(ul), id='fivestarvotes', title=title, class_=className))
+
+
+    def normalise_resource(self, resource):
+        if isinstance(resource, basestring):
+            resource = resource.strip('/')
+            # Special-case start page
+            if not resource or resource == 'wiki':
+                resource = 'wiki/WikiStart'
+            return resource
+        return get_resource_url(self.env, resource, Href('')).strip('/')
+
+    def format_votes(self, resource):
+        count = self.get_vote_counts(resource)
+
+        percent = 0
+        if count[2] > 0:
+            percent = count[2] * 20
+
+        str = "Currently %s/5 stars." % count[2]
+        title = "Current Vote: %s users voted for a total of %s" % (count[1], count[0]) 
+        return (percent, str, title)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/fivestarvote/htdocs/css/fivestarvote.css	Sat Aug 10 04:54:07 2013 -0500
@@ -0,0 +1,88 @@
+/**
+* CSS technique used from this article:
+* http://www.komodomedia.com/blog/2007/01/css-star-rating-redux/
+*/
+
+#fivestarvotes .star-rating,
+#fivestarvotes .star-rating li a.item:hover,
+#fivestarvotes .star-rating li a.item:active,
+#fivestarvotes .star-rating li a.item:focus,
+#fivestarvotes .star-rating li.current-rating {
+    background: url(rating.png) left -1000px repeat-x;
+}
+#fivestarvotes .star-rating li a.item,
+#fivestarvotes .star-rating li.current-rating {
+    padding: 0;
+    margin: 0;
+    border-right: none;
+    background-color: transparent;
+}
+#fivestarvotes .star-rating {
+    position:relative;
+    width: 80px;
+    height: 18px;
+    overflow:hidden;
+    list-style:none;
+    margin:0;
+    padding:0;
+    display: block;
+    background-position: 0 -40px;
+    border: none;
+    text-align: left;
+}
+#fivestarvotes .star-rating li {
+    display: inline;
+    border: none;
+}
+#fivestarvotes .star-rating li a,
+#fivestarvotes .star-rating li.current-rating {
+    position:absolute;
+    top:0;
+    left:0;
+    text-indent:-1000em;
+    height:25px;
+    line-height:25px;
+    outline:none;
+    overflow:hidden;
+    border: none;
+    background-color: transparent;
+}
+#fivestarvotes.active .star-rating li a.item:hover {
+    background-position: 0 -19px;
+}
+#fivestarvotes .star-rating li a.item:active,
+#fivestarvotes .star-rating li a.item:focus {
+    /*background-position: 0 1px;*/
+}
+#fivestarvotes .star-rating li a.one-star {
+    width:20%;
+    z-index:6;
+}
+#fivestarvotes .star-rating li a.two-star{
+    width:40%;
+    z-index:5;
+}
+#fivestarvotes .star-rating li a.three-star{
+    width:60%;
+    z-index:4;
+}
+#fivestarvotes .star-rating li a.four-star{
+    width:80%;
+    z-index:3;
+}
+#fivestarvotes .star-rating li a.five-star{
+    width:100%;
+    z-index:2;
+ }
+#fivestarvotes .star-rating li.current-rating{
+    z-index:1;
+    background-position: 0 1px;
+}
+
+#fivestarvotes {
+    display: -moz-inline-block;
+    display: -moz-inline-box;
+    display: inline-block;
+    vertical-align: middle;
+}
+
Binary file fivestarvote/htdocs/css/rating.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/fivestarvote/htdocs/js/fivestarvote.js	Sat Aug 10 04:54:07 2013 -0500
@@ -0,0 +1,13 @@
+$(document).ready(function() {
+    $('#fivestarvotes').click(function(e) {
+        var button = this;
+        e.preventDefault();
+        $.get(e.target.href + '?js=1', function(result) {
+            result = result.split(',');
+            $('#fivestarvotes .current-rating').css('width', result[0] + '%').html(result[1]);
+            $('#fivestarvotes').attr('title', result[2]);
+        });
+        return false;
+    });
+});
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup.py	Sat Aug 10 04:54:07 2013 -0500
@@ -0,0 +1,17 @@
+# Based on code from : http://trac-hacks.org/wiki/VotePlugin
+from setuptools import setup
+
+setup(
+    	name='FiveStarVote',
+    	version='0.1.1',
+    	packages=['fivestarvote'],
+        package_data={'fivestarvote' : ['htdocs/js/*.js', 'htdocs/css/*.css', 'htdocs/css/*.png']},
+    	author='Dav Glass',
+      	author_email='dav.glass@yahoo.com',
+        maintainer = 'Ryan J Ollos',
+        maintainer_email = 'ryano@physiosonics.com',
+    	license='BSD',
+    	url='http://trac-hacks.org/wiki/FiveStarVotePlugin',
+    	description='A plugin for 5-star voting on Trac resources.',
+    	entry_points = {'trac.plugins': ['fivestarvote = fivestarvote']},
+    )