allura
Revisión | 840d748a54b1464890e283a04546d17fa13bc4e5 (tree) |
---|---|
Tiempo | 2012-07-18 04:25:21 |
Autor | Igor Bondarenko <jetmind2@gmai...> |
Commiter | Cory Johns |
[#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> |
@@ -110,6 +110,7 @@ | ||
110 | 110 | {% endblock %} |
111 | 111 | |
112 | 112 | {% block content %} |
113 | +{{ c.vote_form.display(artifact=ticket) }} | |
113 | 114 | <div id="ticket_content"> |
114 | 115 | {{g.markdown.convert(ticket.description)|safe}} |
115 | 116 | {% 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 |
@@ -830,6 +831,39 @@ class TestFunctionalController(TrackerTestController): | ||
830 | 831 | assert_in('test second ticket', str(ticket_rows)) |
831 | 832 | assert_false('test third ticket' in str(ticket_rows)) |
832 | 833 | |
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 | + | |
833 | 867 | @td.with_tool('test', 'Tickets', 'tracker', |
834 | 868 | post_install_hook=post_install_create_ticket_permission) |
835 | 869 | def test_create_permission(self): |
@@ -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__ |
@@ -978,6 +980,7 @@ class TicketController(BaseController): | ||
978 | 980 | c.attachment_list = W.attachment_list |
979 | 981 | c.subscribe_form = W.ticket_subscribe_form |
980 | 982 | c.ticket_custom_field = W.ticket_custom_field |
983 | + c.vote_form = W.vote_form | |
981 | 984 | tool_subscribed = M.Mailbox.subscribed() |
982 | 985 | if tool_subscribed: |
983 | 986 | subscribed = False |
@@ -1152,6 +1155,24 @@ class TicketController(BaseController): | ||
1152 | 1155 | self.ticket.unsubscribe() |
1153 | 1156 | redirect(request.referer) |
1154 | 1157 | |
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 | + | |
1155 | 1176 | class AttachmentController(ac.AttachmentController): |
1156 | 1177 | AttachmentClass = TM.TicketAttachment |
1157 | 1178 | edit_perm = 'update' |