allura
修訂 | 65713e7a130cc78ad9ea88bbbcc99392a9738113 (tree) |
---|---|
時間 | 2012-07-07 00:47:08 |
作者 | Igor Bondarenko <jetmind2@gmai...> |
Commiter | Yaroslav Luzin |
[#4181] ticket:100 Implemented ability to vote for a ticket.
@@ -2,3 +2,4 @@ from .discuss import Post, Thread, Discussion | ||
2 | 2 | from .subscriptions import SubscriptionForm |
3 | 3 | from .oauth_widgets import OAuthApplicationForm, OAuthRevocationForm |
4 | 4 | from .auth_widgets import LoginForm |
5 | +from .vote import VoteForm |
@@ -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 | +} |
@@ -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 | +}); |
@@ -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') |
@@ -693,3 +693,17 @@ class VotableArtifact(MappedClass): | ||
693 | 693 | self.votes_up = self.votes_up - 1 |
694 | 694 | self.votes_down_users.append(user.username) |
695 | 695 | 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 |
@@ -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 %}'>⇑</span> | |
9 | + <span class='votes-up {% if voted == 1 %}voted{% endif %}'>{{artifact.votes_up}}</span> up | |
10 | + <span class='vote-downarrow {% if can_vote %}vote-down{% endif %}'>⇓</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> |
@@ -108,6 +108,7 @@ | ||
108 | 108 | {% endblock %} |
109 | 109 | |
110 | 110 | {% block content %} |
111 | +{{ c.vote_form.display(artifact=ticket) }} | |
111 | 112 | <div id="ticket_content"> |
112 | 113 | {{g.markdown.convert(ticket.description)|safe}} |
113 | 114 | {% if ticket.attachments %} |
@@ -1,6 +1,7 @@ | ||
1 | 1 | # -*- coding: utf-8 -*- |
2 | 2 | import os |
3 | 3 | import time |
4 | +import json | |
4 | 5 | import Image, StringIO |
5 | 6 | import allura |
6 | 7 |
@@ -777,6 +778,39 @@ class TestFunctionalController(TrackerTestController): | ||
777 | 778 | assert_in('test second ticket', str(ticket_rows)) |
778 | 779 | assert_false('test third ticket' in str(ticket_rows)) |
779 | 780 | |
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 | + | |
780 | 814 | class TestMilestoneAdmin(TrackerTestController): |
781 | 815 | def _post(self, params, **kw): |
782 | 816 | params['open_status_names'] = 'aa bb' |
@@ -27,7 +27,8 @@ from allura.lib import helpers as h | ||
27 | 27 | from allura.app import Application, SitemapEntry, DefaultAdminController, ConfigOption |
28 | 28 | from allura.lib.search import search_artifact |
29 | 29 | 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) | |
31 | 32 | from allura.lib import widgets as w |
32 | 33 | from allura.lib import validators as V |
33 | 34 | from allura.lib.widgets import form_fields as ffw |
@@ -98,6 +99,7 @@ class W: | ||
98 | 99 | ticket_custom_field = TicketCustomField |
99 | 100 | options_admin = OptionsAdmin() |
100 | 101 | search_help_modal = SearchHelp() |
102 | + vote_form = w.VoteForm() | |
101 | 103 | |
102 | 104 | class ForgeTrackerApp(Application): |
103 | 105 | __version__ = version.__version__ |
@@ -1011,6 +1013,7 @@ class TicketController(BaseController): | ||
1011 | 1013 | c.attachment_list = W.attachment_list |
1012 | 1014 | c.subscribe_form = W.ticket_subscribe_form |
1013 | 1015 | c.ticket_custom_field = W.ticket_custom_field |
1016 | + c.vote_form = W.vote_form | |
1014 | 1017 | tool_subscribed = M.Mailbox.subscribed() |
1015 | 1018 | if tool_subscribed: |
1016 | 1019 | subscribed = False |
@@ -1185,6 +1188,24 @@ class TicketController(BaseController): | ||
1185 | 1188 | self.ticket.unsubscribe() |
1186 | 1189 | redirect(request.referer) |
1187 | 1190 | |
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 | + | |
1188 | 1209 | class AttachmentController(ac.AttachmentController): |
1189 | 1210 | AttachmentClass = TM.TicketAttachment |
1190 | 1211 | edit_perm = 'write' |