• R/O
  • HTTP
  • SSH
  • HTTPS

提交

標籤
無標籤

Frequently used words (click to add to your profile)

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

allura


Commit MetaInfo

修訂65713e7a130cc78ad9ea88bbbcc99392a9738113 (tree)
時間2012-07-07 00:47:08
作者Igor Bondarenko <jetmind2@gmai...>
CommiterYaroslav Luzin

Log Message

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

Change Summary

差異

--- 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
@@ -108,6 +108,7 @@
108108 {% endblock %}
109109
110110 {% block content %}
111+{{ c.vote_form.display(artifact=ticket) }}
111112 <div id="ticket_content">
112113 {{g.markdown.convert(ticket.description)|safe}}
113114 {% 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
@@ -777,6 +778,39 @@ class TestFunctionalController(TrackerTestController):
777778 assert_in('test second ticket', str(ticket_rows))
778779 assert_false('test third ticket' in str(ticket_rows))
779780
781+ def test_vote(self):
782+ r = self.new_ticket(summary='test vote').follow()
783+ vote = r.html.find('div', {'id': 'vote'})
784+ assert_in('0 up', str(vote))
785+ assert_in('0 down', str(vote))
786+
787+ # invalid vote
788+ r = self.app.post('/bugs/1/vote', dict(vote='invalid'))
789+ expected_resp = json.dumps(
790+ dict(status='error', votes_up=0, votes_down=0))
791+ assert r.response.content == expected_resp
792+
793+ # vote up
794+ r = self.app.post('/bugs/1/vote', dict(vote='u'))
795+ expected_resp = json.dumps(
796+ dict(status='ok', votes_up=1, votes_down=0))
797+ assert r.response.content == expected_resp
798+
799+ # vote down by another user
800+ r = self.app.post('/bugs/1/vote', dict(vote='d'),
801+ extra_environ=dict(username='test-user-0'))
802+
803+ expected_resp = json.dumps(
804+ dict(status='ok', votes_up=1, votes_down=1))
805+ assert r.response.content == expected_resp
806+
807+ # make sure that on the page we see the same result
808+ r = self.app.get('/bugs/1/')
809+ vote = r.html.find('div', {'id': 'vote'})
810+ assert_in('1 up', str(vote))
811+ assert_in('1 down', str(vote))
812+
813+
780814 class TestMilestoneAdmin(TrackerTestController):
781815 def _post(self, params, **kw):
782816 params['open_status_names'] = 'aa bb'
--- 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__
@@ -1011,6 +1013,7 @@ class TicketController(BaseController):
10111013 c.attachment_list = W.attachment_list
10121014 c.subscribe_form = W.ticket_subscribe_form
10131015 c.ticket_custom_field = W.ticket_custom_field
1016+ c.vote_form = W.vote_form
10141017 tool_subscribed = M.Mailbox.subscribed()
10151018 if tool_subscribed:
10161019 subscribed = False
@@ -1185,6 +1188,24 @@ class TicketController(BaseController):
11851188 self.ticket.unsubscribe()
11861189 redirect(request.referer)
11871190
1191+ @expose('json')
1192+ @require_post()
1193+ def vote(self, vote):
1194+ require_authenticated()
1195+ require_access(self.ticket, 'post')
1196+ status = 'ok'
1197+ if vote == 'u':
1198+ self.ticket.vote_up(c.user)
1199+ elif vote == 'd':
1200+ self.ticket.vote_down(c.user)
1201+ else:
1202+ status = 'error'
1203+ return dict(
1204+ status=status,
1205+ votes_up=self.ticket.votes_up,
1206+ votes_down=self.ticket.votes_down)
1207+
1208+
11881209 class AttachmentController(ac.AttachmentController):
11891210 AttachmentClass = TM.TicketAttachment
11901211 edit_perm = 'write'