Commit 4e00e7c56a48d36f0ecd450524f11b016ade6fa4

Authored by Sergio Oliveira
1 parent a4d52056

Updating message thread layout

src/api/handlers.py
1 1  
2 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 4 from piston.utils import rc
8 5 from piston.handler import BaseHandler
9 6  
10 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 11 class CountHandler(BaseHandler):
... ...
src/api/urls.py
... ... @@ -2,15 +2,15 @@ from django.conf.urls import patterns, include, url
2 2  
3 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 9 count_handler = Resource(CountHandler)
10 10 search_handler = Resource(SearchHandler)
11 11  
12 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 14 url(r'hit/$', count_handler),
15 15 url(r'search/$', search_handler),
16 16 )
... ...
src/api/views.py 0 → 100644
... ... @@ -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 2 {% load i18n browserid conversejs gravatar %}
3 3 <html>
4 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 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 14  
15 15 <script type="text/javascript" src="{{ STATIC_URL }}third-party/jquery-2.0.3.min.js"></script>
16 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 18 <script src="{{ STATIC_URL }}third-party/bootstrap/js/bootstrap.js"></script>
18 19  
19 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 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 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 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 1 function pagehit(path_info) {
62 2 jQuery.ajax({
63 3 url: '/api/hit/',
... ...
src/colab/static/third-party/jquery.cookie.js 0 → 100755
... ... @@ -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 26  
27 27  
28 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 30 address = models.EmailField(unique=True)
31 31 real_name = models.CharField(max_length=64, blank=True, db_index=True)
32 32 md5 = models.CharField(max_length=32, null=True)
33   -
  33 +
34 34 def save(self, *args, **kwargs):
35 35 self.md5 = md5(self.address).hexdigest()
36 36 super(EmailAddress, self).save(*args, **kwargs)
37   -
  37 +
38 38 def get_full_name(self):
39 39 if self.user and self.user.get_full_name():
40 40 return self.user.get_full_name()
41 41 elif self.real_name:
42 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 47 def __unicode__(self):
45 48 return '"%s" <%s>' % (self.get_full_name(), self.address)
46 49  
... ... @@ -65,21 +68,21 @@ class MailingListMembership(models.Model):
65 68  
66 69  
67 70 class Thread(models.Model):
68   -
  71 +
69 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 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 79 help_text=_(u"Latest message posted"))
77 80 score = models.IntegerField(default=0, verbose_name=_(u"Score"), help_text=_(u"Thread score"))
78 81 spam = models.BooleanField(default=False)
79   -
  82 +
80 83 all_objects = models.Manager()
81 84 objects = NotSpamManager()
82   -
  85 +
83 86 class Meta:
84 87 verbose_name = _(u"Thread")
85 88 verbose_name_plural = _(u"Threads")
... ... @@ -87,17 +90,17 @@ class Thread(models.Model):
87 90  
88 91 def __unicode__(self):
89 92 return '%s - %s (%s)' % (self.id,
90   - self.subject_token,
  93 + self.subject_token,
91 94 self.message_set.count())
92 95  
93 96 def update_score(self):
94 97 """Update the relevance score for this thread.
95   -
  98 +
96 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 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 104 replied with minimum of 5.
102 105 * page_view_weight: 10.
103 106  
... ... @@ -111,8 +114,8 @@ class Thread(models.Model):
111 114 """
112 115  
113 116 if not self.subject_token:
114   - return
115   -
  117 + return
  118 +
116 119 # Save this pseudo now to avoid calling the
117 120 # function N times in the loops below
118 121 now = timezone.now()
... ... @@ -130,8 +133,8 @@ class Thread(models.Model):
130 133 for vote in msg.vote_set.all():
131 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 138 url = reverse('thread_view', args=[self.mailinglist.name,
136 139 self.subject_token])
137 140 pagehit = PageHit.objects.get(url_path=url)
... ... @@ -157,18 +160,18 @@ class Vote(models.Model):
157 160  
158 161  
159 162 class Message(models.Model):
160   -
  163 +
161 164 from_address = models.ForeignKey(EmailAddress, db_index=True)
162 165 thread = models.ForeignKey(Thread, null=True, db_index=True)
163 166 # RFC 2822 recommends to use 78 chars + CRLF (so 80 chars) for
164 167 # the max_length of a subject but most of implementations
165 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 171 help_text=_(u"Please enter a message subject"))
169 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 175 help_text=_(u"Please enter a message body"))
173 176 received_time = models.DateTimeField()
174 177 message_id = models.CharField(max_length=512)
... ... @@ -176,39 +179,37 @@ class Message(models.Model):
176 179  
177 180 all_objects = models.Manager()
178 181 objects = NotSpamManager()
179   -
  182 +
180 183 class Meta:
181 184 verbose_name = _(u"Message")
182 185 verbose_name_plural = _(u"Messages")
183 186 unique_together = ('thread', 'message_id')
184   -
  187 +
185 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 191 self.subject_clean)
189   -
  192 +
190 193 @property
191 194 def mailinglist(self):
192 195 if not self.thread or not self.thread.mailinglist:
193 196 return None
194   -
  197 +
195 198 return self.thread.mailinglist
196 199  
197   -
198 200 def vote_list(self):
199 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 204 def votes_count(self):
204 205 return len(self.vote_list())
205   -
  206 +
206 207 def vote(self, user):
207 208 Vote.objects.create(
208 209 message=self,
209 210 user=user
210 211 )
211   -
  212 +
212 213 def unvote(self, user):
213 214 Vote.objects.get(
214 215 message=self,
... ... @@ -220,7 +221,7 @@ class Message(models.Model):
220 221 """Shortcut to get thread url"""
221 222 return reverse('thread_view', args=[self.mailinglist.name,
222 223 self.thread.subject_token])
223   -
  224 +
224 225 @property
225 226 def Description(self):
226 227 """Alias to self.body"""
... ... @@ -247,4 +248,4 @@ class MessageMetadata(models.Model):
247 248 def __unicode__(self):
248 249 return 'Email Message Id: %s - %s: %s' % (self.Message.id,
249 250 self.name, self.value)
250   -
  251 +
... ...
src/super_archives/templates/message-thread.html
1 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 100 {% block main-content %}
6 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 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 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 200 <a href="{% append_to_get order='date' %}">{% trans "Date" %}</a>
19   - </span></li>
  201 + </li>
20 202 </ul>
21 203  
22   - <div>&nbsp;</div>
23   -
24 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 209 {% trans "started at" %}
29   - </span></li>
30 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 215 {% trans "viewed" %}
33   - </span></li>
34 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 220 {% trans "answered" %}
37   - </span></li>
38 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 225 {% trans "voted" %}
41   - </span></li>
42 226 <h5>{{ total_votes }} {% trans "times" %}</h5>
  227 + </li>
43 228 </ul>
44 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>&nbsp;</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>&nbsp;</div>
74   - {% endif %}
75   - </li>
76   - {% endfor %}
77   - </ul>
78   -
79 231 <script type="text/javascript" charset="utf-8">
80 232 pagehit("{{ request.path_info }}");
81 233 </script>
... ...