Commit 4e00e7c56a48d36f0ecd450524f11b016ade6fa4
1 parent
a4d52056
Exists in
master
and in
39 other branches
Updating message thread layout
Showing
9 changed files
with
398 additions
and
208 deletions
Show diff stats
src/api/handlers.py
1 | 1 | ||
2 | from django.core.cache import cache | 2 | from django.core.cache import cache |
3 | -from django.db import IntegrityError | ||
4 | -from django.core.exceptions import ObjectDoesNotExist | ||
5 | -from django.contrib.auth.decorators import login_required | ||
6 | 3 | ||
7 | from piston.utils import rc | 4 | from piston.utils import rc |
8 | from piston.handler import BaseHandler | 5 | from piston.handler import BaseHandler |
9 | 6 | ||
10 | from colab.deprecated import solrutils | 7 | from colab.deprecated import solrutils |
11 | -from super_archives.models import Message, PageHit | ||
12 | - | ||
13 | - | ||
14 | -class VoteHandler(BaseHandler): | ||
15 | - allowed_methods = ('GET', 'POST', 'DELETE') | ||
16 | - | ||
17 | - def create(self, request, message_id): | ||
18 | - if not request.user.is_authenticated(): | ||
19 | - return rc.FORBIDDEN | ||
20 | - | ||
21 | - try: | ||
22 | - Message.objects.get(id=message_id).vote(request.user) | ||
23 | - except IntegrityError: | ||
24 | - return rc.DUPLICATE_ENTRY | ||
25 | - | ||
26 | - return rc.CREATED | ||
27 | - | ||
28 | - def read(self, request, message_id): | ||
29 | - return Message.objects.get(id=message_id).votes_count() | ||
30 | - | ||
31 | - def delete(self, request, message_id): | ||
32 | - if not request.user.is_authenticated(): | ||
33 | - return rc.FORBIDDEN | ||
34 | - | ||
35 | - try: | ||
36 | - Message.objects.get(id=message_id).unvote(request.user) | ||
37 | - except ObjectDoesNotExist: | ||
38 | - return rc.NOT_HERE | ||
39 | - | ||
40 | - return rc.DELETED | 8 | +from super_archives.models import PageHit |
41 | 9 | ||
42 | 10 | ||
43 | class CountHandler(BaseHandler): | 11 | class CountHandler(BaseHandler): |
src/api/urls.py
@@ -2,15 +2,15 @@ from django.conf.urls import patterns, include, url | @@ -2,15 +2,15 @@ from django.conf.urls import patterns, include, url | ||
2 | 2 | ||
3 | from piston.resource import Resource | 3 | from piston.resource import Resource |
4 | 4 | ||
5 | -from .handlers import VoteHandler, CountHandler, SearchHandler | 5 | +from .handlers import CountHandler, SearchHandler |
6 | +from .views import VoteView | ||
6 | 7 | ||
7 | 8 | ||
8 | -vote_handler = Resource(VoteHandler) | ||
9 | count_handler = Resource(CountHandler) | 9 | count_handler = Resource(CountHandler) |
10 | search_handler = Resource(SearchHandler) | 10 | search_handler = Resource(SearchHandler) |
11 | 11 | ||
12 | urlpatterns = patterns('', | 12 | urlpatterns = patterns('', |
13 | - url(r'message/(?P<message_id>\d+)/vote$', vote_handler), | 13 | + url(r'message/(?P<msg_id>\d+)/vote$', VoteView.as_view()), |
14 | url(r'hit/$', count_handler), | 14 | url(r'hit/$', count_handler), |
15 | url(r'search/$', search_handler), | 15 | url(r'search/$', search_handler), |
16 | ) | 16 | ) |
@@ -0,0 +1,45 @@ | @@ -0,0 +1,45 @@ | ||
1 | + | ||
2 | +from django import http | ||
3 | +from django.db import IntegrityError | ||
4 | +from django.views.generic import View | ||
5 | +from django.core.exceptions import ObjectDoesNotExist | ||
6 | + | ||
7 | + | ||
8 | +from super_archives.models import Message | ||
9 | + | ||
10 | + | ||
11 | +class VoteView(View): | ||
12 | + | ||
13 | + http_method_names = [u'get', u'put', u'delete', u'head'] | ||
14 | + | ||
15 | + def put(self, request, msg_id): | ||
16 | + if not request.user.is_authenticated(): | ||
17 | + return http.HttpResponseForbidden() | ||
18 | + | ||
19 | + try: | ||
20 | + Message.objects.get(id=msg_id).vote(request.user) | ||
21 | + except IntegrityError: | ||
22 | + # 409 Conflict | ||
23 | + # used for duplicated entries | ||
24 | + return http.HttpResponse(status=409) | ||
25 | + | ||
26 | + # 201 Created | ||
27 | + return http.HttpResponse(status=201) | ||
28 | + | ||
29 | + def get(self, request, msg_id): | ||
30 | + votes = Message.objects.get(id=msg_id).votes_count() | ||
31 | + return http.HttpResponse(votes, content_type='application/json') | ||
32 | + | ||
33 | + def delete(self, request, msg_id): | ||
34 | + if not request.user.is_authenticated(): | ||
35 | + return http.HttpResponseForbidden() | ||
36 | + | ||
37 | + try: | ||
38 | + Message.objects.get(id=msg_id).unvote(request.user) | ||
39 | + except ObjectDoesNotExist: | ||
40 | + return http.HttpResponseGone() | ||
41 | + | ||
42 | + # 204 No Content | ||
43 | + # empty body, as per RFC2616. | ||
44 | + # object deleted | ||
45 | + return http.HttpResponse(status=204) |
src/colab/deprecated/templates/base.html
@@ -2,7 +2,7 @@ | @@ -2,7 +2,7 @@ | ||
2 | {% load i18n browserid conversejs gravatar %} | 2 | {% load i18n browserid conversejs gravatar %} |
3 | <html> | 3 | <html> |
4 | <head> | 4 | <head> |
5 | - <meta name="viewport" content="width=device-width, initial-scale=1.0"> | 5 | + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
6 | 6 | ||
7 | <link rel="stylesheet" href="{{ STATIC_URL }}third-party/bootstrap/css/bootstrap.css" type="text/css" media="screen, projection" /> | 7 | <link rel="stylesheet" href="{{ STATIC_URL }}third-party/bootstrap/css/bootstrap.css" type="text/css" media="screen, projection" /> |
8 | 8 | ||
@@ -14,6 +14,7 @@ | @@ -14,6 +14,7 @@ | ||
14 | 14 | ||
15 | <script type="text/javascript" src="{{ STATIC_URL }}third-party/jquery-2.0.3.min.js"></script> | 15 | <script type="text/javascript" src="{{ STATIC_URL }}third-party/jquery-2.0.3.min.js"></script> |
16 | <script type="text/javascript" src="{{ STATIC_URL }}third-party/jquery.debouncedresize.js"></script> | 16 | <script type="text/javascript" src="{{ STATIC_URL }}third-party/jquery.debouncedresize.js"></script> |
17 | + <script type="text/javascript" src="{{ STATIC_URL }}third-party/jquery.cookie.js"></script> | ||
17 | <script src="{{ STATIC_URL }}third-party/bootstrap/js/bootstrap.js"></script> | 18 | <script src="{{ STATIC_URL }}third-party/bootstrap/js/bootstrap.js"></script> |
18 | 19 | ||
19 | <script type="text/javascript" src="{{ STATIC_URL }}js/base.js"></script> | 20 | <script type="text/javascript" src="{{ STATIC_URL }}js/base.js"></script> |
src/colab/static/css/screen.css
@@ -250,33 +250,26 @@ ul.unstyled-list .glyphicon-chevron-right { | @@ -250,33 +250,26 @@ ul.unstyled-list .glyphicon-chevron-right { | ||
250 | margin-top: 1em; | 250 | margin-top: 1em; |
251 | } | 251 | } |
252 | 252 | ||
253 | -.plus { | ||
254 | - border: 1px dotted #ddd; | ||
255 | - margin: 0; | 253 | +.email-message pre { |
254 | + border: 0; | ||
255 | + background-color: #fff; | ||
256 | + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; | ||
256 | } | 257 | } |
257 | 258 | ||
258 | -.plus span { | 259 | +.email-message .user-fullname { |
259 | vertical-align: middle; | 260 | vertical-align: middle; |
260 | - height: 40px; | ||
261 | - line-height: 40px; | ||
262 | - font-size: 150%; | ||
263 | - font-weight: bold; | ||
264 | - color: #0a0; | ||
265 | } | 261 | } |
266 | 262 | ||
267 | -.plus img { | ||
268 | - vertical-align: middle; | ||
269 | - cursor: pointer; | ||
270 | - margin-top: 8px; | ||
271 | - margin-right: 20px; | 263 | +.email-message .panel-heading img { |
264 | + margin-right: 10px; | ||
272 | } | 265 | } |
273 | 266 | ||
274 | -.plus img:hover { | ||
275 | - opacity: 0.8; | 267 | +.email-message .panel-heading .date { |
268 | + margin-right: 10px; | ||
276 | } | 269 | } |
277 | 270 | ||
278 | -.no-margin { | ||
279 | - margin: 0; | 271 | +.email-message .panel-heading { |
272 | + padding: 10px 0; | ||
280 | } | 273 | } |
281 | 274 | ||
282 | .selected .glyphicon-remove { | 275 | .selected .glyphicon-remove { |
src/colab/static/js/base.js
1 | - | ||
2 | -function vote_callback(msg_id, step) { | ||
3 | - return function() { | ||
4 | - jQuery('#msg-' + msg_id + ' .plus span').text(function(self, count) { | ||
5 | - return parseInt(count) + step; | ||
6 | - }); | ||
7 | - jQuery('#msg-' + msg_id + ' .minus').toggleClass('hide'); | ||
8 | - jQuery('#vote-notification').addClass('hide'); | ||
9 | - } | ||
10 | -} | ||
11 | - | ||
12 | -function get_vote_ajax_dict(msg_id, type_) { | ||
13 | - if (type_ === 'DELETE') { | ||
14 | - step = -1; | ||
15 | - } else if (type_ === 'POST') { | ||
16 | - step = 1; | ||
17 | - } else { | ||
18 | - return {}; | ||
19 | - } | ||
20 | - | ||
21 | - return { | ||
22 | - url: "/api/message/" + msg_id + "/vote", | ||
23 | - type: type_, | ||
24 | - success: vote_callback(msg_id, step), | ||
25 | - error: function (jqXHR, textStatus, errorThrown) { | ||
26 | - | ||
27 | - error_msg = '<b>Seu voto não foi computado.</b>' | ||
28 | - if (jqXHR.status === 401) { | ||
29 | - error_msg += ' Você deve estar autenticado para votar.'; | ||
30 | - } else { | ||
31 | - error_msg += ' Erro desconhecido ao tentando votar.'; | ||
32 | - } | ||
33 | - | ||
34 | - jQuery('#vote-notification').html(error_msg).removeClass('hide'); | ||
35 | - scroll(0, 0); | ||
36 | - } | ||
37 | - } | ||
38 | -} | ||
39 | - | ||
40 | -function vote(msg_id) { | ||
41 | - jQuery.ajax(get_vote_ajax_dict(msg_id, 'POST')); | ||
42 | -} | ||
43 | - | ||
44 | -function unvote(msg_id) { | ||
45 | - jQuery.ajax(get_vote_ajax_dict(msg_id, 'DELETE')); | ||
46 | -} | ||
47 | - | ||
48 | -jQuery(document).ready(function() { | ||
49 | - jQuery('.email_message').each(function() { | ||
50 | - var msg_id = this.getAttribute('id').split('-')[1]; | ||
51 | - jQuery('.plus img', this).bind('click', function() { | ||
52 | - vote(msg_id); | ||
53 | - }); | ||
54 | - jQuery('.minus a', this).bind('click', function() { | ||
55 | - unvote(msg_id); | ||
56 | - return false; | ||
57 | - }); | ||
58 | - }); | ||
59 | -}); | ||
60 | - | ||
61 | function pagehit(path_info) { | 1 | function pagehit(path_info) { |
62 | jQuery.ajax({ | 2 | jQuery.ajax({ |
63 | url: '/api/hit/', | 3 | url: '/api/hit/', |
@@ -0,0 +1,90 @@ | @@ -0,0 +1,90 @@ | ||
1 | +/*! | ||
2 | + * jQuery Cookie Plugin v1.3.1 | ||
3 | + * https://github.com/carhartl/jquery-cookie | ||
4 | + * | ||
5 | + * Copyright 2013 Klaus Hartl | ||
6 | + * Released under the MIT license | ||
7 | + */ | ||
8 | +(function ($, document, undefined) { | ||
9 | + | ||
10 | + var pluses = /\+/g; | ||
11 | + | ||
12 | + function raw(s) { | ||
13 | + return s; | ||
14 | + } | ||
15 | + | ||
16 | + function decoded(s) { | ||
17 | + return unRfc2068(decodeURIComponent(s.replace(pluses, ' '))); | ||
18 | + } | ||
19 | + | ||
20 | + function unRfc2068(value) { | ||
21 | + if (value.indexOf('"') === 0) { | ||
22 | + // This is a quoted cookie as according to RFC2068, unescape | ||
23 | + value = value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); | ||
24 | + } | ||
25 | + return value; | ||
26 | + } | ||
27 | + | ||
28 | + function fromJSON(value) { | ||
29 | + return config.json ? JSON.parse(value) : value; | ||
30 | + } | ||
31 | + | ||
32 | + var config = $.cookie = function (key, value, options) { | ||
33 | + | ||
34 | + // write | ||
35 | + if (value !== undefined) { | ||
36 | + options = $.extend({}, config.defaults, options); | ||
37 | + | ||
38 | + if (value === null) { | ||
39 | + options.expires = -1; | ||
40 | + } | ||
41 | + | ||
42 | + if (typeof options.expires === 'number') { | ||
43 | + var days = options.expires, t = options.expires = new Date(); | ||
44 | + t.setDate(t.getDate() + days); | ||
45 | + } | ||
46 | + | ||
47 | + value = config.json ? JSON.stringify(value) : String(value); | ||
48 | + | ||
49 | + return (document.cookie = [ | ||
50 | + encodeURIComponent(key), '=', config.raw ? value : encodeURIComponent(value), | ||
51 | + options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE | ||
52 | + options.path ? '; path=' + options.path : '', | ||
53 | + options.domain ? '; domain=' + options.domain : '', | ||
54 | + options.secure ? '; secure' : '' | ||
55 | + ].join('')); | ||
56 | + } | ||
57 | + | ||
58 | + // read | ||
59 | + var decode = config.raw ? raw : decoded; | ||
60 | + var cookies = document.cookie.split('; '); | ||
61 | + var result = key ? null : {}; | ||
62 | + for (var i = 0, l = cookies.length; i < l; i++) { | ||
63 | + var parts = cookies[i].split('='); | ||
64 | + var name = decode(parts.shift()); | ||
65 | + var cookie = decode(parts.join('=')); | ||
66 | + | ||
67 | + if (key && key === name) { | ||
68 | + result = fromJSON(cookie); | ||
69 | + break; | ||
70 | + } | ||
71 | + | ||
72 | + if (!key) { | ||
73 | + result[name] = fromJSON(cookie); | ||
74 | + } | ||
75 | + } | ||
76 | + | ||
77 | + return result; | ||
78 | + }; | ||
79 | + | ||
80 | + config.defaults = {}; | ||
81 | + | ||
82 | + $.removeCookie = function (key, options) { | ||
83 | + if ($.cookie(key) !== null) { | ||
84 | + $.cookie(key, null, options); | ||
85 | + return true; | ||
86 | + } | ||
87 | + return false; | ||
88 | + }; | ||
89 | + | ||
90 | +})(jQuery, document); |
src/super_archives/models.py
@@ -26,21 +26,24 @@ class PageHit(models.Model): | @@ -26,21 +26,24 @@ class PageHit(models.Model): | ||
26 | 26 | ||
27 | 27 | ||
28 | class EmailAddress(models.Model): | 28 | class EmailAddress(models.Model): |
29 | - user = models.ForeignKey(User, null=True, related_name='emails') | 29 | + user = models.ForeignKey(User, null=True, related_name='emails') |
30 | address = models.EmailField(unique=True) | 30 | address = models.EmailField(unique=True) |
31 | real_name = models.CharField(max_length=64, blank=True, db_index=True) | 31 | real_name = models.CharField(max_length=64, blank=True, db_index=True) |
32 | md5 = models.CharField(max_length=32, null=True) | 32 | md5 = models.CharField(max_length=32, null=True) |
33 | - | 33 | + |
34 | def save(self, *args, **kwargs): | 34 | def save(self, *args, **kwargs): |
35 | self.md5 = md5(self.address).hexdigest() | 35 | self.md5 = md5(self.address).hexdigest() |
36 | super(EmailAddress, self).save(*args, **kwargs) | 36 | super(EmailAddress, self).save(*args, **kwargs) |
37 | - | 37 | + |
38 | def get_full_name(self): | 38 | def get_full_name(self): |
39 | if self.user and self.user.get_full_name(): | 39 | if self.user and self.user.get_full_name(): |
40 | return self.user.get_full_name() | 40 | return self.user.get_full_name() |
41 | elif self.real_name: | 41 | elif self.real_name: |
42 | return self.real_name | 42 | return self.real_name |
43 | 43 | ||
44 | + def get_full_name_or_anonymous(self): | ||
45 | + return self.get_full_name() or _('Anonymous') | ||
46 | + | ||
44 | def __unicode__(self): | 47 | def __unicode__(self): |
45 | return '"%s" <%s>' % (self.get_full_name(), self.address) | 48 | return '"%s" <%s>' % (self.get_full_name(), self.address) |
46 | 49 | ||
@@ -65,21 +68,21 @@ class MailingListMembership(models.Model): | @@ -65,21 +68,21 @@ class MailingListMembership(models.Model): | ||
65 | 68 | ||
66 | 69 | ||
67 | class Thread(models.Model): | 70 | class Thread(models.Model): |
68 | - | 71 | + |
69 | subject_token = models.CharField(max_length=512) | 72 | subject_token = models.CharField(max_length=512) |
70 | - mailinglist = models.ForeignKey(MailingList, | ||
71 | - verbose_name=_(u"Mailing List"), | 73 | + mailinglist = models.ForeignKey(MailingList, |
74 | + verbose_name=_(u"Mailing List"), | ||
72 | help_text=_(u"The Mailing List where is the thread")) | 75 | help_text=_(u"The Mailing List where is the thread")) |
73 | - latest_message = models.OneToOneField('Message', null=True, | ||
74 | - related_name='+', | ||
75 | - verbose_name=_(u"Latest message"), | 76 | + latest_message = models.OneToOneField('Message', null=True, |
77 | + related_name='+', | ||
78 | + verbose_name=_(u"Latest message"), | ||
76 | help_text=_(u"Latest message posted")) | 79 | help_text=_(u"Latest message posted")) |
77 | score = models.IntegerField(default=0, verbose_name=_(u"Score"), help_text=_(u"Thread score")) | 80 | score = models.IntegerField(default=0, verbose_name=_(u"Score"), help_text=_(u"Thread score")) |
78 | spam = models.BooleanField(default=False) | 81 | spam = models.BooleanField(default=False) |
79 | - | 82 | + |
80 | all_objects = models.Manager() | 83 | all_objects = models.Manager() |
81 | objects = NotSpamManager() | 84 | objects = NotSpamManager() |
82 | - | 85 | + |
83 | class Meta: | 86 | class Meta: |
84 | verbose_name = _(u"Thread") | 87 | verbose_name = _(u"Thread") |
85 | verbose_name_plural = _(u"Threads") | 88 | verbose_name_plural = _(u"Threads") |
@@ -87,17 +90,17 @@ class Thread(models.Model): | @@ -87,17 +90,17 @@ class Thread(models.Model): | ||
87 | 90 | ||
88 | def __unicode__(self): | 91 | def __unicode__(self): |
89 | return '%s - %s (%s)' % (self.id, | 92 | return '%s - %s (%s)' % (self.id, |
90 | - self.subject_token, | 93 | + self.subject_token, |
91 | self.message_set.count()) | 94 | self.message_set.count()) |
92 | 95 | ||
93 | def update_score(self): | 96 | def update_score(self): |
94 | """Update the relevance score for this thread. | 97 | """Update the relevance score for this thread. |
95 | - | 98 | + |
96 | The score is calculated with the following variables: | 99 | The score is calculated with the following variables: |
97 | 100 | ||
98 | - * vote_weight: 100 - (minus) 1 for each 3 days since | 101 | + * vote_weight: 100 - (minus) 1 for each 3 days since |
99 | voted with minimum of 5. | 102 | voted with minimum of 5. |
100 | - * replies_weight: 300 - (minus) 1 for each 3 days since | 103 | + * replies_weight: 300 - (minus) 1 for each 3 days since |
101 | replied with minimum of 5. | 104 | replied with minimum of 5. |
102 | * page_view_weight: 10. | 105 | * page_view_weight: 10. |
103 | 106 | ||
@@ -111,8 +114,8 @@ class Thread(models.Model): | @@ -111,8 +114,8 @@ class Thread(models.Model): | ||
111 | """ | 114 | """ |
112 | 115 | ||
113 | if not self.subject_token: | 116 | if not self.subject_token: |
114 | - return | ||
115 | - | 117 | + return |
118 | + | ||
116 | # Save this pseudo now to avoid calling the | 119 | # Save this pseudo now to avoid calling the |
117 | # function N times in the loops below | 120 | # function N times in the loops below |
118 | now = timezone.now() | 121 | now = timezone.now() |
@@ -130,8 +133,8 @@ class Thread(models.Model): | @@ -130,8 +133,8 @@ class Thread(models.Model): | ||
130 | for vote in msg.vote_set.all(): | 133 | for vote in msg.vote_set.all(): |
131 | vote_score += get_score(100, vote.created) | 134 | vote_score += get_score(100, vote.created) |
132 | 135 | ||
133 | - # Calculate page_view_score | ||
134 | - try: | 136 | + # Calculate page_view_score |
137 | + try: | ||
135 | url = reverse('thread_view', args=[self.mailinglist.name, | 138 | url = reverse('thread_view', args=[self.mailinglist.name, |
136 | self.subject_token]) | 139 | self.subject_token]) |
137 | pagehit = PageHit.objects.get(url_path=url) | 140 | pagehit = PageHit.objects.get(url_path=url) |
@@ -157,18 +160,18 @@ class Vote(models.Model): | @@ -157,18 +160,18 @@ class Vote(models.Model): | ||
157 | 160 | ||
158 | 161 | ||
159 | class Message(models.Model): | 162 | class Message(models.Model): |
160 | - | 163 | + |
161 | from_address = models.ForeignKey(EmailAddress, db_index=True) | 164 | from_address = models.ForeignKey(EmailAddress, db_index=True) |
162 | thread = models.ForeignKey(Thread, null=True, db_index=True) | 165 | thread = models.ForeignKey(Thread, null=True, db_index=True) |
163 | # RFC 2822 recommends to use 78 chars + CRLF (so 80 chars) for | 166 | # RFC 2822 recommends to use 78 chars + CRLF (so 80 chars) for |
164 | # the max_length of a subject but most of implementations | 167 | # the max_length of a subject but most of implementations |
165 | # goes for 256. We use 512 just in case. | 168 | # goes for 256. We use 512 just in case. |
166 | - subject = models.CharField(max_length=512, db_index=True, | ||
167 | - verbose_name=_(u"Subject"), | 169 | + subject = models.CharField(max_length=512, db_index=True, |
170 | + verbose_name=_(u"Subject"), | ||
168 | help_text=_(u"Please enter a message subject")) | 171 | help_text=_(u"Please enter a message subject")) |
169 | subject_clean = models.CharField(max_length=512, db_index=True) | 172 | subject_clean = models.CharField(max_length=512, db_index=True) |
170 | - body = models.TextField(default='', | ||
171 | - verbose_name=_(u"Message body"), | 173 | + body = models.TextField(default='', |
174 | + verbose_name=_(u"Message body"), | ||
172 | help_text=_(u"Please enter a message body")) | 175 | help_text=_(u"Please enter a message body")) |
173 | received_time = models.DateTimeField() | 176 | received_time = models.DateTimeField() |
174 | message_id = models.CharField(max_length=512) | 177 | message_id = models.CharField(max_length=512) |
@@ -176,39 +179,37 @@ class Message(models.Model): | @@ -176,39 +179,37 @@ class Message(models.Model): | ||
176 | 179 | ||
177 | all_objects = models.Manager() | 180 | all_objects = models.Manager() |
178 | objects = NotSpamManager() | 181 | objects = NotSpamManager() |
179 | - | 182 | + |
180 | class Meta: | 183 | class Meta: |
181 | verbose_name = _(u"Message") | 184 | verbose_name = _(u"Message") |
182 | verbose_name_plural = _(u"Messages") | 185 | verbose_name_plural = _(u"Messages") |
183 | unique_together = ('thread', 'message_id') | 186 | unique_together = ('thread', 'message_id') |
184 | - | 187 | + |
185 | def __unicode__(self): | 188 | def __unicode__(self): |
186 | - return '(%s) %s: %s' % (self.id, | ||
187 | - self.from_address.get_full_name(), | 189 | + return '(%s) %s: %s' % (self.id, |
190 | + self.from_address.get_full_name(), | ||
188 | self.subject_clean) | 191 | self.subject_clean) |
189 | - | 192 | + |
190 | @property | 193 | @property |
191 | def mailinglist(self): | 194 | def mailinglist(self): |
192 | if not self.thread or not self.thread.mailinglist: | 195 | if not self.thread or not self.thread.mailinglist: |
193 | return None | 196 | return None |
194 | - | 197 | + |
195 | return self.thread.mailinglist | 198 | return self.thread.mailinglist |
196 | 199 | ||
197 | - | ||
198 | def vote_list(self): | 200 | def vote_list(self): |
199 | """Return a list of user that voted in this message.""" | 201 | """Return a list of user that voted in this message.""" |
200 | - | ||
201 | - return [vote.user for vote in self.vote_set.all()] | ||
202 | - | 202 | + return [vote.user for vote in self.vote_set.iterator()] |
203 | + | ||
203 | def votes_count(self): | 204 | def votes_count(self): |
204 | return len(self.vote_list()) | 205 | return len(self.vote_list()) |
205 | - | 206 | + |
206 | def vote(self, user): | 207 | def vote(self, user): |
207 | Vote.objects.create( | 208 | Vote.objects.create( |
208 | message=self, | 209 | message=self, |
209 | user=user | 210 | user=user |
210 | ) | 211 | ) |
211 | - | 212 | + |
212 | def unvote(self, user): | 213 | def unvote(self, user): |
213 | Vote.objects.get( | 214 | Vote.objects.get( |
214 | message=self, | 215 | message=self, |
@@ -220,7 +221,7 @@ class Message(models.Model): | @@ -220,7 +221,7 @@ class Message(models.Model): | ||
220 | """Shortcut to get thread url""" | 221 | """Shortcut to get thread url""" |
221 | return reverse('thread_view', args=[self.mailinglist.name, | 222 | return reverse('thread_view', args=[self.mailinglist.name, |
222 | self.thread.subject_token]) | 223 | self.thread.subject_token]) |
223 | - | 224 | + |
224 | @property | 225 | @property |
225 | def Description(self): | 226 | def Description(self): |
226 | """Alias to self.body""" | 227 | """Alias to self.body""" |
@@ -247,4 +248,4 @@ class MessageMetadata(models.Model): | @@ -247,4 +248,4 @@ class MessageMetadata(models.Model): | ||
247 | def __unicode__(self): | 248 | def __unicode__(self): |
248 | return 'Email Message Id: %s - %s: %s' % (self.Message.id, | 249 | return 'Email Message Id: %s - %s: %s' % (self.Message.id, |
249 | self.name, self.value) | 250 | self.name, self.value) |
250 | - | 251 | + |
src/super_archives/templates/message-thread.html
1 | {% extends "base.html" %} | 1 | {% extends "base.html" %} |
2 | -{% load i18n %} | ||
3 | -{% load append_to_get %} | ||
4 | -{% load gravatar %} | 2 | +{% load i18n append_to_get gravatar %} |
3 | + | ||
4 | +{% trans "Anonymous" as anonymous %} | ||
5 | + | ||
6 | +{% block head_js %} | ||
7 | + | ||
8 | +<script> | ||
9 | + function vote_done_callback(msg_id, step) { | ||
10 | + console.debug('(un)vote successfuly (step ' + step + ')'); | ||
11 | + var $msg = $('#msg-' + msg_id) | ||
12 | + | ||
13 | + $('.vote-count', $msg).text(function(self, count) { | ||
14 | + return parseInt(count) + step; | ||
15 | + }); | ||
16 | + | ||
17 | + if (step == -1) { | ||
18 | + var $btn = $('.vote.btn-success', $msg); | ||
19 | + $btn.unbind('click'); | ||
20 | + $btn.bind('click', function() { | ||
21 | + vote(msg_id); | ||
22 | + }); | ||
23 | + $('.text', $btn).text("{% trans 'Vote' %}"); | ||
24 | + } else { | ||
25 | + var $btn = $('.vote.btn-default', $msg); | ||
26 | + $btn.unbind('click'); | ||
27 | + $btn.bind('click', function() { | ||
28 | + unvote(msg_id); | ||
29 | + }); | ||
30 | + $('.text', $btn).text("{% trans 'Voted' %}"); | ||
31 | + } | ||
32 | + $btn.toggleClass('btn-success'); | ||
33 | + $btn.toggleClass('btn-default'); | ||
34 | + } | ||
35 | + | ||
36 | + function vote_fail_callback(jqXHR, textStatus, errorThrown) { | ||
37 | + alert('error'); | ||
38 | + //error_msg = '<b>Seu voto não foi computado.</b>' | ||
39 | + //if (jqXHR.status === 401) { | ||
40 | + // error_msg += ' Você deve estar autenticado para votar.'; | ||
41 | + //} else { | ||
42 | + // error_msg += ' Erro desconhecido ao tentando votar.'; | ||
43 | + //} | ||
44 | + | ||
45 | + //jQuery('#vote-notification').html(error_msg).removeClass('hide'); | ||
46 | + //scroll(0, 0); | ||
47 | + } | ||
48 | + | ||
49 | + function get_vote_ajax_dict(msg_id, method) { | ||
50 | + var csrftoken = $.cookie('csrftoken'); | ||
51 | + | ||
52 | + return { | ||
53 | + url: "/api/message/" + msg_id + "/vote", | ||
54 | + type: method, | ||
55 | + beforeSend: function(xhr, settings) { | ||
56 | + xhr.setRequestHeader("X-CSRFToken", csrftoken); | ||
57 | + } | ||
58 | + } | ||
59 | + } | ||
60 | + | ||
61 | + function vote(msg_id) { | ||
62 | + console.debug('trying to vote'); | ||
63 | + $.ajax(get_vote_ajax_dict(msg_id, 'PUT')) | ||
64 | + .done(function(){ | ||
65 | + vote_done_callback(msg_id, 1); | ||
66 | + }) | ||
67 | + .fail(vote_fail_callback); | ||
68 | + } | ||
69 | + | ||
70 | + function unvote(msg_id) { | ||
71 | + console.debug('trying to remove vote'); | ||
72 | + $.ajax(get_vote_ajax_dict(msg_id, 'DELETE')) | ||
73 | + .done(function(){ | ||
74 | + vote_done_callback(msg_id, -1); | ||
75 | + }) | ||
76 | + .fail(vote_fail_callback); | ||
77 | + } | ||
78 | + | ||
79 | + // Binding functions | ||
80 | + $(function() { | ||
81 | + $('.email-message').each(function() { | ||
82 | + var msg_id = this.getAttribute('id').split('-')[1]; | ||
83 | + console.debug('binding vote calls to ' + msg_id); | ||
84 | + | ||
85 | + $('.vote.btn-default', this).bind('click', function() { | ||
86 | + vote(msg_id); | ||
87 | + }); | ||
88 | + | ||
89 | + $('.vote.btn-success', this).bind('click', function() { | ||
90 | + unvote(msg_id); | ||
91 | + }); | ||
92 | + | ||
93 | + }); | ||
94 | + }); | ||
95 | + | ||
96 | +</script> | ||
97 | + | ||
98 | +{% endblock %} | ||
99 | + | ||
5 | {% block main-content %} | 100 | {% block main-content %} |
6 | <div class="row"> | 101 | <div class="row"> |
7 | 102 | ||
8 | - <h2>{{ first_msg.subject_clean }}</h2> | ||
9 | - <hr /> | 103 | + <div class="col-lg-12"> |
104 | + <h2>{{ first_msg.subject_clean }}</h2> | ||
105 | + <hr /> | ||
106 | + </div> | ||
10 | 107 | ||
11 | - <div class="col-lg-3 pull-right"> | 108 | + <div class="col-lg-10 col-md-10 col-sm-12"> |
109 | + <ul class="unstyled-list"> | ||
110 | + {% for email in emails %} | ||
111 | + {% with email.from_address.get_absolute_url as profile_link %} | ||
112 | + <li> | ||
113 | + <!-- | ||
114 | + <div class="col-lg-2 col-md-3 hidden-sm hidden-xm text-center"> | ||
115 | + {% if profile_link %} | ||
116 | + <a href="{{ profile_link }}"> | ||
117 | + {% endif %} | ||
118 | + <div>{% gravatar email.from_address 80 %}</div> | ||
119 | + <span>{{ email.from_address.get_full_name_or_anonymous }}</span> | ||
120 | + {% if profile_link %} | ||
121 | + </a> | ||
122 | + {% endif %} | ||
123 | + | ||
124 | + <p>{{ email.received_time|date:"SHORT_DATETIME_FORMAT" }}</p> | ||
125 | + | ||
126 | + <div class="plus"> | ||
127 | + <span>{{ email.votes_count }}</span> | ||
128 | + <img title="{% trans 'Vote' %}" class="pull-right" src="{{ STATIC_URL }}img/plus.png"> | ||
129 | + </div> | ||
130 | + | ||
131 | + <p class="{% if not user in email.vote_list %}hide{% endif %}"> | ||
132 | + <a href="#">{% trans "Remove votes" %}</a> | ||
133 | + </p> | ||
134 | + </div> | ||
135 | + --> | ||
136 | + | ||
137 | + <div class="email-message" id="msg-{{ email.id }}"> | ||
138 | + <div class="panel panel-default"> | ||
139 | + <div class="panel-heading clearfix"> | ||
140 | + <div class="col-lg-6 col-md-6 col-sm-6"> | ||
141 | + {% if profile_link %} | ||
142 | + <a href="{{ profile_link }}"> | ||
143 | + {% endif %} | ||
144 | + {% gravatar email.from_address 34 %} | ||
145 | + <strong class="user-fullname">{{ email.from_address.get_full_name_or_anonymous }}</strong> | ||
146 | + {% if profile_link %} | ||
147 | + </a> | ||
148 | + {% endif %} | ||
149 | + </div> | ||
150 | + | ||
151 | + <div class="col-lg-6 col-md-6 col-sm-6"> | ||
152 | + <div class="pull-right text-right"> | ||
153 | + <span class="date"> | ||
154 | + {{ email.received_time|date:'DATETIME_FORMAT' }} | ||
155 | + </span> | ||
156 | + | ||
157 | + <div class="btn-group"> | ||
158 | + <button class="btn btn-default vote-count disabled"> | ||
159 | + {{ email.votes_count }} | ||
160 | + </button> | ||
161 | + {% if user in email.vote_list %} | ||
162 | + <button class="btn btn-success vote"> | ||
163 | + {% else %} | ||
164 | + <button class="btn btn-default vote"> | ||
165 | + {% endif %} | ||
166 | + <span class="glyphicon glyphicon-thumbs-up"></span> | ||
167 | + <span class="text"> | ||
168 | + {% if user in email.vote_list %} | ||
169 | + {% trans "Voted" %} | ||
170 | + {% else %} | ||
171 | + {% trans "Vote" %} | ||
172 | + {% endif %} | ||
173 | + </span> | ||
174 | + </button> | ||
175 | + </div> | ||
176 | + </div> | ||
177 | + </div> | ||
178 | + | ||
179 | + </div> | ||
180 | + <div class="panel-body"> | ||
181 | + <pre>{{ email.body }}</pre> | ||
182 | + </div> | ||
183 | + </div> | ||
184 | + </div> | ||
185 | + </li> | ||
186 | + {% endwith %} | ||
187 | + {% endfor %} | ||
188 | + </ul> | ||
189 | + </div> | ||
190 | + | ||
191 | + <div class="col-lg-2 col-md-2 hidden-sm hidden-xs"> | ||
12 | <h4><strong>{% trans "Order by" %}:</strong></h4> | 192 | <h4><strong>{% trans "Order by" %}:</strong></h4> |
13 | - <ul class="none"> | ||
14 | - <li><span class="glyphicon glyphicon-chevron-right"> | 193 | + <ul class="unstyled-list"> |
194 | + <li> | ||
195 | + <span class="glyphicon glyphicon-chevron-right"></span> | ||
15 | <a href="{% append_to_get order='voted' %}">{% trans "Votes" %}</a> | 196 | <a href="{% append_to_get order='voted' %}">{% trans "Votes" %}</a> |
16 | - </span></li> | ||
17 | - <li><span class="glyphicon glyphicon-chevron-right"> | 197 | + </li> |
198 | + <li> | ||
199 | + <span class="glyphicon glyphicon-chevron-right"></span> | ||
18 | <a href="{% append_to_get order='date' %}">{% trans "Date" %}</a> | 200 | <a href="{% append_to_get order='date' %}">{% trans "Date" %}</a> |
19 | - </span></li> | 201 | + </li> |
20 | </ul> | 202 | </ul> |
21 | 203 | ||
22 | - <div> </div> | ||
23 | - | ||
24 | <h4><strong>{% trans "Statistics:" %}</strong></h4> | 204 | <h4><strong>{% trans "Statistics:" %}</strong></h4> |
25 | 205 | ||
26 | - <ul class="none"> | ||
27 | - <li class="quiet"><span class="glyphicon glyphicon-chevron-right"> | 206 | + <ul class="unstyled-list"> |
207 | + <li> | ||
208 | + <span class="glyphicon glyphicon-chevron-right"></span> | ||
28 | {% trans "started at" %} | 209 | {% trans "started at" %} |
29 | - </span></li> | ||
30 | <h5>{{ first_msg.received_time|timesince }} {% trans "ago" %}</h5> | 210 | <h5>{{ first_msg.received_time|timesince }} {% trans "ago" %}</h5> |
31 | - <li class="quiet"><span class="glyphicon glyphicon-chevron-right"> | 211 | + </li> |
212 | + | ||
213 | + <li> | ||
214 | + <span class="glyphicon glyphicon-chevron-right"></span> | ||
32 | {% trans "viewed" %} | 215 | {% trans "viewed" %} |
33 | - </span></li> | ||
34 | <h5>{{ pagehits }} {% trans "times" %}</h5> | 216 | <h5>{{ pagehits }} {% trans "times" %}</h5> |
35 | - <li class="quiet"><span class="glyphicon glyphicon-chevron-right"> | 217 | + </li> |
218 | + <li> | ||
219 | + <span class="glyphicon glyphicon-chevron-right"></span> | ||
36 | {% trans "answered" %} | 220 | {% trans "answered" %} |
37 | - </span></li> | ||
38 | <h5>{{ emails|length }} {% trans "times" %}</h5> | 221 | <h5>{{ emails|length }} {% trans "times" %}</h5> |
39 | - <li class="quiet"><span class="glyphicon glyphicon-chevron-right"> | 222 | + </li> |
223 | + <li> | ||
224 | + <span class="glyphicon glyphicon-chevron-right"></span> | ||
40 | {% trans "voted" %} | 225 | {% trans "voted" %} |
41 | - </span></li> | ||
42 | <h5>{{ total_votes }} {% trans "times" %}</h5> | 226 | <h5>{{ total_votes }} {% trans "times" %}</h5> |
227 | + </li> | ||
43 | </ul> | 228 | </ul> |
44 | </div> | 229 | </div> |
45 | 230 | ||
46 | - <ul class="none"> | ||
47 | - {% for email in emails %} | ||
48 | - <li> | ||
49 | - <div class="col-lg-2 text-center"> | ||
50 | - <a href="{{ email.from_address.get_profile_link }}"> | ||
51 | - {% gravatar email.from_address 80 %} | ||
52 | - <div> </div> | ||
53 | - {% trans "Anonymous" as anonymous %} | ||
54 | - <span>{% firstof email.from_address.get_full_name anonymous %}</span> | ||
55 | - </a> | ||
56 | - | ||
57 | - <p>{{ email.received_time|date:"SHORT_DATETIME_FORMAT" }}</p> | ||
58 | - | ||
59 | - <div class="plus"> | ||
60 | - <span>{{ email.votes_count }}</span> | ||
61 | - <img title="{% trans 'Vote' %}" class="pull-right" src="{{ STATIC_URL }}img/plus.png"> | ||
62 | - </div> | ||
63 | - | ||
64 | - <p class="{% if not user in email.vote_list %}hide{% endif %}"> | ||
65 | - <a href="#">{% trans "Remove votes" %}</a> | ||
66 | - </p> | ||
67 | - </div> | ||
68 | - | ||
69 | - <div class="col-lg-7"> | ||
70 | - <pre>{{ email.body }}</pre> | ||
71 | - </div> | ||
72 | - {% if not forloop.last %} | ||
73 | - <div> </div> | ||
74 | - {% endif %} | ||
75 | - </li> | ||
76 | - {% endfor %} | ||
77 | - </ul> | ||
78 | - | ||
79 | <script type="text/javascript" charset="utf-8"> | 231 | <script type="text/javascript" charset="utf-8"> |
80 | pagehit("{{ request.path_info }}"); | 232 | pagehit("{{ request.path_info }}"); |
81 | </script> | 233 | </script> |