• R/O
  • HTTP
  • SSH
  • HTTPS

Commit

Tags
No Tags

Frequently used words (click to add to your profile)

javac++androidlinuxc#windowsobjective-ccocoa誰得qtpythonphprubygameguibathyscaphec計画中(planning stage)翻訳omegatframeworktwitterdomtestvb.netdirectxゲームエンジンbtronarduinopreviewer

allura


Commit MetaInfo

Revisión840d748a54b1464890e283a04546d17fa13bc4e5 (tree)
Tiempo2012-07-18 04:25:21
AutorIgor Bondarenko <jetmind2@gmai...>
CommiterCory Johns

Log Message

[#4181] ticket:100 Implemented ability to vote for a ticket.

Cambiar Resumen

Diferencia incremental

--- a/Allura/allura/lib/widgets/__init__.py
+++ b/Allura/allura/lib/widgets/__init__.py
@@ -2,3 +2,4 @@ from .discuss import Post, Thread, Discussion
22 from .subscriptions import SubscriptionForm
33 from .oauth_widgets import OAuthApplicationForm, OAuthRevocationForm
44 from .auth_widgets import LoginForm
5+from .vote import VoteForm
--- /dev/null
+++ b/Allura/allura/lib/widgets/resources/css/vote.css
@@ -0,0 +1,22 @@
1+#vote {
2+ display: block;
3+ float: right;
4+ margin-right: .5em
5+ padding: .5em;
6+ text-align: center;
7+ width: 12em;
8+}
9+
10+#vote .vote-uparrow {
11+ color: red;
12+ cursor: pointer;
13+}
14+
15+#vote .vote-downarrow {
16+ color: blue;
17+ cursor: pointer;
18+}
19+
20+#vote .voted {
21+ color: red;
22+}
--- /dev/null
+++ b/Allura/allura/lib/widgets/resources/js/vote.js
@@ -0,0 +1,41 @@
1+$(document).ready(function() {
2+ function vote(vote) {
3+ var $form = $('#vote form');
4+ var url = $form.attr('action');
5+ var method = $form.attr('method');
6+ var _session_id = $form.find('input[name="_session_id"]').val();
7+ $.ajax({
8+ url: url,
9+ type: method,
10+ data: {
11+ vote: vote,
12+ _session_id: _session_id
13+ },
14+ success: function(data) {
15+ if (data.status == 'ok') {
16+ $('#vote .votes-up').text(data.votes_up);
17+ $('#vote .votes-down').text(data.votes_down);
18+ }
19+ }
20+ });
21+ }
22+
23+ function set_voted(vote) {
24+ if (vote == 'u') {
25+ $('#vote .votes-down').removeClass('voted');
26+ $('#vote .votes-up').addClass('voted');
27+ } else if (vote == 'd') {
28+ $('#vote .votes-up').removeClass('voted');
29+ $('#vote .votes-down').addClass('voted');
30+ }
31+ }
32+
33+ $('#vote .vote-up').click(function() {
34+ vote('u');
35+ set_voted('u')
36+ });
37+ $('#vote .vote-down').click(function() {
38+ vote('d');
39+ set_voted('d');
40+ });
41+});
--- /dev/null
+++ b/Allura/allura/lib/widgets/vote.py
@@ -0,0 +1,15 @@
1+import ew as ew_core
2+import ew.jinja2_ew as ew
3+
4+
5+class VoteForm(ew_core.Widget):
6+ template = 'jinja:allura:templates/widgets/vote.html'
7+ defaults = dict(
8+ ew_core.Widget.defaults,
9+ action='vote',
10+ artifact=None
11+ )
12+
13+ def resources(self):
14+ yield ew.CSSLink('css/vote.css')
15+ yield ew.JSLink('js/vote.js')
--- a/Allura/allura/model/artifact.py
+++ b/Allura/allura/model/artifact.py
@@ -693,3 +693,17 @@ class VotableArtifact(MappedClass):
693693 self.votes_up = self.votes_up - 1
694694 self.votes_down_users.append(user.username)
695695 self.votes_down += 1
696+
697+ def user_voted(self, user):
698+ """Check that user voted for this artifact.
699+
700+ Return:
701+ 1 if user voted up
702+ -1 if user voted down
703+ 0 if user doesn't vote
704+ """
705+ if user.username in self.votes_up_users:
706+ return 1
707+ if user.username in self.votes_down_users:
708+ return -1
709+ return 0
--- /dev/null
+++ b/Allura/allura/templates/widgets/vote.html
@@ -0,0 +1,17 @@
1+{% set can_vote = c.user and c.user != c.user.anonymous()
2+ and h.has_access(artifact, 'post')() %}
3+{% set voted = artifact.user_voted(c.user) %}
4+
5+<div id="vote" class="info message">
6+ Vote for this ticket:
7+ <br />
8+ <span class='vote-uparrow {% if can_vote %}vote-up{% endif %}'>&uArr;</span>
9+ <span class='votes-up {% if voted == 1 %}voted{% endif %}'>{{artifact.votes_up}}</span> up&nbsp;
10+ <span class='vote-downarrow {% if can_vote %}vote-down{% endif %}'>&dArr;</span>
11+ <span class='votes-down {% if voted == -1 %}voted{% endif %}'>{{artifact.votes_down}}</span> down
12+ {% if can_vote %}
13+ <form action="{{ action }}" method="POST">
14+ {# csrf protection will be automatically inserted here (_session_id field) #}
15+ </form>
16+ {% endif %}
17+</div>
--- a/ForgeTracker/forgetracker/templates/tracker/ticket.html
+++ b/ForgeTracker/forgetracker/templates/tracker/ticket.html
@@ -110,6 +110,7 @@
110110 {% endblock %}
111111
112112 {% block content %}
113+{{ c.vote_form.display(artifact=ticket) }}
113114 <div id="ticket_content">
114115 {{g.markdown.convert(ticket.description)|safe}}
115116 {% if ticket.attachments %}
--- a/ForgeTracker/forgetracker/tests/functional/test_root.py
+++ b/ForgeTracker/forgetracker/tests/functional/test_root.py
@@ -1,6 +1,7 @@
11 # -*- coding: utf-8 -*-
22 import os
33 import time
4+import json
45 import Image, StringIO
56 import allura
67
@@ -830,6 +831,39 @@ class TestFunctionalController(TrackerTestController):
830831 assert_in('test second ticket', str(ticket_rows))
831832 assert_false('test third ticket' in str(ticket_rows))
832833
834+ def test_vote(self):
835+ r = self.new_ticket(summary='test vote').follow()
836+ vote = r.html.find('div', {'id': 'vote'})
837+ assert_in('0 up', str(vote))
838+ assert_in('0 down', str(vote))
839+
840+ # invalid vote
841+ r = self.app.post('/bugs/1/vote', dict(vote='invalid'))
842+ expected_resp = json.dumps(
843+ dict(status='error', votes_up=0, votes_down=0))
844+ assert r.response.content == expected_resp
845+
846+ # vote up
847+ r = self.app.post('/bugs/1/vote', dict(vote='u'))
848+ expected_resp = json.dumps(
849+ dict(status='ok', votes_up=1, votes_down=0))
850+ assert r.response.content == expected_resp
851+
852+ # vote down by another user
853+ r = self.app.post('/bugs/1/vote', dict(vote='d'),
854+ extra_environ=dict(username='test-user-0'))
855+
856+ expected_resp = json.dumps(
857+ dict(status='ok', votes_up=1, votes_down=1))
858+ assert r.response.content == expected_resp
859+
860+ # make sure that on the page we see the same result
861+ r = self.app.get('/bugs/1/')
862+ vote = r.html.find('div', {'id': 'vote'})
863+ assert_in('1 up', str(vote))
864+ assert_in('1 down', str(vote))
865+
866+
833867 @td.with_tool('test', 'Tickets', 'tracker',
834868 post_install_hook=post_install_create_ticket_permission)
835869 def test_create_permission(self):
--- a/ForgeTracker/forgetracker/tracker_main.py
+++ b/ForgeTracker/forgetracker/tracker_main.py
@@ -27,7 +27,8 @@ from allura.lib import helpers as h
2727 from allura.app import Application, SitemapEntry, DefaultAdminController, ConfigOption
2828 from allura.lib.search import search_artifact
2929 from allura.lib.decorators import require_post
30-from allura.lib.security import require_access, has_access, require
30+from allura.lib.security import (require_access, has_access, require,
31+ require_authenticated)
3132 from allura.lib import widgets as w
3233 from allura.lib import validators as V
3334 from allura.lib.widgets import form_fields as ffw
@@ -98,6 +99,7 @@ class W:
9899 ticket_custom_field = TicketCustomField
99100 options_admin = OptionsAdmin()
100101 search_help_modal = SearchHelp()
102+ vote_form = w.VoteForm()
101103
102104 class ForgeTrackerApp(Application):
103105 __version__ = version.__version__
@@ -978,6 +980,7 @@ class TicketController(BaseController):
978980 c.attachment_list = W.attachment_list
979981 c.subscribe_form = W.ticket_subscribe_form
980982 c.ticket_custom_field = W.ticket_custom_field
983+ c.vote_form = W.vote_form
981984 tool_subscribed = M.Mailbox.subscribed()
982985 if tool_subscribed:
983986 subscribed = False
@@ -1152,6 +1155,24 @@ class TicketController(BaseController):
11521155 self.ticket.unsubscribe()
11531156 redirect(request.referer)
11541157
1158+ @expose('json')
1159+ @require_post()
1160+ def vote(self, vote):
1161+ require_authenticated()
1162+ require_access(self.ticket, 'post')
1163+ status = 'ok'
1164+ if vote == 'u':
1165+ self.ticket.vote_up(c.user)
1166+ elif vote == 'd':
1167+ self.ticket.vote_down(c.user)
1168+ else:
1169+ status = 'error'
1170+ return dict(
1171+ status=status,
1172+ votes_up=self.ticket.votes_up,
1173+ votes_down=self.ticket.votes_down)
1174+
1175+
11551176 class AttachmentController(ac.AttachmentController):
11561177 AttachmentClass = TM.TicketAttachment
11571178 edit_perm = 'update'