Просмотр исходного кода

- Watched threads and E-mail reply notifications
- Small fix in threads list template

Ralfp 12 лет назад
Родитель
Сommit
e8b0097289

+ 1 - 0
misago/context_processors.py

@@ -4,6 +4,7 @@ from misago import get_version
 # Register context processors
 def core(request):
     return {
+        'request_path': request.get_full_path(),
         'board_address': settings.BOARD_ADDRESS,
         'version': get_version(),
     }

+ 23 - 0
misago/threads/models.py

@@ -6,6 +6,7 @@ from misago.forums.signals import move_forum_content
 from misago.threads.signals import move_thread, merge_thread, move_post, merge_post
 from misago.users.signals import delete_user_content, rename_user
 from misago.utils import slugify, ugettext_lazy
+from misago.watcher.models import ThreadWatch
 
 class ThreadManager(models.Manager):
     def filter_stats(self, start, end):
@@ -89,6 +90,28 @@ class Thread(models.Model):
         self.moderated = start_post.moderated
         self.deleted = start_post.deleted
         self.merges = last_post.merge
+        
+    def email_watchers(self, request, post):
+        from misago.acl.builder import get_acl
+        from misago.acl.utils import ACLError403, ACLError404
+        
+        for watch in ThreadWatch.objects.filter(thread=self).filter(email=True).filter(last_read__gte=self.previous_last):
+            user = watch.user
+            if user.pk != request.user.pk:
+                try:
+                    acl = get_acl(request, user)
+                    acl.forums.allow_forum_view(self.forum)
+                    acl.threads.allow_thread_view(user, self)
+                    acl.threads.allow_post_view(user, self, post)
+                    if not user.is_ignoring(request.user):
+                        user.email_user(
+                            request,
+                            'post_notification',
+                            _('New reply in thread "%(thread)s"') % {'thread': self.name},
+                            {'author': request.user, 'post': post, 'thread': self}
+                            )
+                except (ACLError403, ACLError404):
+                    pass
 
 
 class PostManager(models.Manager):

+ 4 - 0
misago/threads/urls.py

@@ -11,6 +11,10 @@ urlpatterns = patterns('misago.threads.views',
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/moderated/$', 'FirstModeratedView', name="thread_moderated"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/reported/$', 'FirstReportedView', name="thread_reported"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/show-hidden/$', 'ShowHiddenRepliesView', name="thread_show_hidden"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/watch/$', 'WatchThreadView', name="thread_watch"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/watch/email/$', 'WatchEmailThreadView', name="thread_watch_email"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/$', 'UnwatchThreadView', name="thread_unwatch"),
+    url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/unwatch/email/$', 'UnwatchEmailThreadView', name="thread_unwatch_email"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<page>\d+)/$', 'ThreadView', name="thread"),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/reply/$', 'PostingView', name="thread_reply", kwargs={'mode': 'new_post'}),
     url(r'^thread/(?P<slug>(\w|-)+)-(?P<thread>\d+)/(?P<quote>\d+)/reply/$', 'PostingView', name="thread_reply", kwargs={'mode': 'new_post'}),

+ 68 - 13
misago/threads/views/jumps.py

@@ -1,6 +1,7 @@
 from django.core.urlresolvers import reverse
 from django.shortcuts import redirect
 from django.template import RequestContext
+from django.utils import timezone
 from django.utils.translation import ugettext as _
 from misago.acl.utils import ACLError403, ACLError404
 from misago.authn.decorators import block_guest
@@ -12,6 +13,7 @@ from misago.threads.models import Thread, Post
 from misago.threads.views.base import BaseView
 from misago.views import error403, error404
 from misago.utils import make_pagination
+from misago.watcher.models import ThreadWatch
 
 class JumpView(BaseView):
     def fetch_thread(self, thread):
@@ -48,19 +50,6 @@ class JumpView(BaseView):
             return error404(request, e.message)
 
 
-class ShowHiddenRepliesView(JumpView):
-    def make_jump(self):
-        @block_guest
-        @check_csrf
-        def view(request):
-            ignored_exclusions = request.session.get('unignore_threads', [])
-            ignored_exclusions.append(self.thread.pk)
-            request.session['unignore_threads'] = ignored_exclusions
-            request.messages.set_flash(Message(_('Replies made to this thread by members on your ignore list have been revealed.')), 'success', 'threads')
-            return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}))
-        return view(self.request)
-
-
 class LastReplyView(JumpView):
     def make_jump(self):
         return self.redirect(self.thread.post_set.order_by('-id')[:1][0])
@@ -103,3 +92,69 @@ class FirstReportedView(JumpView):
                 self.thread.post_set.get(reported=True))
         except Post.DoesNotExist:
             return error404(self.request)
+
+
+class ShowHiddenRepliesView(JumpView):
+    def make_jump(self):
+        @block_guest
+        @check_csrf
+        def view(request):
+            ignored_exclusions = request.session.get('unignore_threads', [])
+            ignored_exclusions.append(self.thread.pk)
+            request.session['unignore_threads'] = ignored_exclusions
+            request.messages.set_flash(Message(_('Replies made to this thread by members on your ignore list have been revealed.')), 'success', 'threads')
+            return redirect(reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug}))
+        return view(self.request)
+
+
+class WatchThreadView(JumpView):
+    def get_retreat(self):
+        return redirect(self.request.POST.get('retreat', reverse('thread', kwargs={'thread': self.thread.pk, 'slug': self.thread.slug})))
+    
+    def update_watcher(self, request, watcher):
+        request.messages.set_flash(Message(_('This thread has been added to your watched threads list.')), 'success', 'threads')
+    
+    def make_jump(self):
+        @block_guest
+        @check_csrf
+        def view(request):
+            try:
+                watcher = ThreadWatch.objects.get(user=request.user, thread=self.thread)
+            except ThreadWatch.DoesNotExist:
+                watcher = ThreadWatch()
+                watcher.user = request.user
+                watcher.forum = self.forum
+                watcher.thread = self.thread
+                watcher.last_read = timezone.now()
+            self.update_watcher(request, watcher)
+            if watcher.pk:
+                watcher.save(force_update=True)
+            else:
+                watcher.save(force_insert=True)
+            return self.get_retreat()
+        return view(self.request)
+
+
+class WatchEmailThreadView(WatchThreadView):
+    def update_watcher(self, request, watcher):
+        watcher.email = True
+        if watcher.pk:
+            request.messages.set_flash(Message(_('You will now receive e-mail with notification when somebody replies to this thread.')), 'success', 'threads')
+        else:
+            request.messages.set_flash(Message(_('This thread has been added to your watched threads list. You will also receive e-mail with notification when somebody replies to it.')), 'success', 'threads')
+
+
+class UnwatchThreadView(WatchThreadView):
+    def update_watcher(self, request, watcher):
+        watcher.deleted = True
+        watcher.delete()
+        if watcher.email:
+            request.messages.set_flash(Message(_('This thread has been removed from your watched threads list. You will no longer receive e-mails with notifications when somebody replies to it.')), 'success', 'threads')
+        else:
+            request.messages.set_flash(Message(_('This thread has been removed from your watched threads list.')), 'success', 'threads')
+
+
+class UnwatchEmailThreadView(WatchThreadView):
+    def update_watcher(self, request, watcher):
+        watcher.email = False
+        request.messages.set_flash(Message(_('You will no longer receive e-mails with notifications when somebody replies to this thread.')), 'success', 'threads')

+ 4 - 0
misago/threads/views/posting.py

@@ -98,6 +98,7 @@ class PostingView(BaseView):
         message = request.messages.get_message('threads')
         if request.method == 'POST':
             form = self.get_form(True)
+            # Show message preview
             if 'preview' in request.POST:
                 if form['post'].value():
                     md, preparsed = post_markdown(request, form['post'].value())
@@ -117,6 +118,7 @@ class PostingView(BaseView):
                                                          'form': FormLayout(form),
                                                          },
                                                         context_instance=RequestContext(request));
+            # Commit form to database
             if form.is_valid():                
                 # Record original vars if user is editing 
                 if self.mode in ['edit_thread', 'edit_post']:
@@ -155,6 +157,7 @@ class PostingView(BaseView):
                     if self.mode == 'edit_thread':
                         thread.name = form.cleaned_data['thread_name']
                         thread.slug = slugify(form.cleaned_data['thread_name'])
+                thread.previous_last = thread.last 
 
                 # Create new message
                 if self.mode in ['new_thread', 'new_post', 'new_post_quick']:
@@ -315,6 +318,7 @@ class PostingView(BaseView):
                     return redirect(reverse('thread', kwargs={'thread': thread.pk, 'slug': thread.slug}) + ('#post-%s' % post.pk))
 
                 if self.mode in ['new_post', 'new_post_quick']:
+                    thread.email_watchers(request, post)
                     if moderation:
                         request.messages.set_flash(Message(_("Your reply has been posted. It will be hidden from other members until moderator reviews it.")), 'success', 'threads_%s' % post.pk)
                     else:

+ 11 - 0
misago/threads/views/thread.py

@@ -17,6 +17,7 @@ from misago.threads.models import Thread, Post, Change, Checkpoint
 from misago.threads.views.base import BaseView
 from misago.views import error403, error404
 from misago.utils import make_pagination, slugify
+from misago.watcher.models import ThreadWatch
 
 class ThreadView(BaseView):
     def fetch_thread(self, thread):
@@ -27,6 +28,11 @@ class ThreadView(BaseView):
         self.request.acl.threads.allow_thread_view(self.request.user, self.thread)
         self.parents = Forum.objects.forum_parents(self.forum.pk, True)
         self.tracker = ThreadsTracker(self.request, self.forum)
+        if self.request.user.is_authenticated():
+            try:
+                self.watcher = ThreadWatch.objects.get(user=self.request.user, thread=self.thread)
+            except ThreadWatch.DoesNotExist:
+                pass
 
     def fetch_posts(self, page):
         self.count = self.request.acl.threads.filter_posts(self.request, self.thread, Post.objects.filter(thread=self.thread)).count()
@@ -52,6 +58,9 @@ class ThreadView(BaseView):
         if not self.tracker.is_read(self.thread):
             self.tracker.set_read(self.thread, last_post)
             self.tracker.sync()
+        if self.watcher and last_post.date > self.watcher.last_read:
+            self.watcher.last_read = timezone.now()
+            self.watcher.save(force_update=True)
 
     def get_post_actions(self):
         acl = self.request.acl.threads.get_role(self.thread.forum_id)
@@ -513,6 +522,7 @@ class ThreadView(BaseView):
         self.pagination = None
         self.parents = None
         self.ignored = False
+        self.watcher = None
         try:
             self.fetch_thread(thread)
             self.fetch_posts(page)
@@ -545,6 +555,7 @@ class ThreadView(BaseView):
                                                  'count': self.count,
                                                  'posts': self.posts,
                                                  'ignored_posts': self.ignored,
+                                                 'watcher': self.watcher,
                                                  'pagination': self.pagination,
                                                  'quick_reply': FormFields(QuickReplyForm(request=request)).fields,
                                                  'thread_form': FormFields(self.thread_form).fields if self.thread_form else None,

+ 1 - 0
misago/urls.py

@@ -12,6 +12,7 @@ urlpatterns = patterns('',
     (r'^activate/', include('misago.activation.urls')),
     (r'^reset-password/', include('misago.resetpswd.urls')),
     (r'^', include('misago.threads.urls')),
+    (r'^', include('misago.watcher.urls')),
     url(r'^category/(?P<slug>(\w|-)+)-(?P<forum>\d+)/$', 'misago.views.category', name="category"),
     url(r'^redirect/(?P<slug>(\w|-)+)-(?P<forum>\d+)/$', 'misago.views.redirection', name="redirect"),
     url(r'^$', 'misago.views.home', name="index"),

+ 0 - 0
misago/watcher/__init__.py


+ 33 - 0
misago/watcher/models.py

@@ -0,0 +1,33 @@
+from django.db import models
+from misago.forums.signals import move_forum_content
+from misago.threads.signals import move_thread, merge_thread
+
+class ThreadWatch(models.Model):
+    user = models.ForeignKey('users.User')
+    forum = models.ForeignKey('forums.Forum')
+    thread = models.ForeignKey('threads.Thread')
+    last_read = models.DateTimeField()
+    email = models.BooleanField(default=False)
+    deleted = False
+    
+    def save(self, *args, **kwargs):
+        if not self.deleted:
+            super(ThreadWatch, self).save(*args, **kwargs)
+            
+
+def move_forum_content_handler(sender, **kwargs):
+    ThreadWatch.objects.filter(forum=sender).update(forum=kwargs['move_to'])
+
+move_forum_content.connect(move_forum_content_handler, dispatch_uid="move_forum_threads_watchers")
+
+
+def move_thread_handler(sender, **kwargs):
+    ThreadWatch.objects.filter(forum=sender.forum_pk).update(forum=kwargs['move_to'])
+
+move_thread.connect(move_thread_handler, dispatch_uid="move_thread_watchers")
+
+
+def merge_thread_handler(sender, **kwargs):
+    ThreadWatch.objects.filter(thread=sender).delete()
+
+merge_thread.connect(merge_thread_handler, dispatch_uid="merge_threads_watchers")

+ 8 - 0
misago/watcher/urls.py

@@ -0,0 +1,8 @@
+from django.conf.urls import patterns, url
+
+urlpatterns = patterns('misago.watcher.views',
+    url(r'^watched/$', 'watched_threads', name="watched_threads"),
+    url(r'^watched/(?P<page>\d+)/$', 'watched_threads', name="watched_threads"),
+    url(r'^watched/new/$', 'watched_threads', name="watched_threads_new", kwargs={'new': True}),
+    url(r'^watched/new/(?P<page>\d+)/$', 'watched_threads', name="watched_threads_new", kwargs={'new': True}),
+)

+ 85 - 0
misago/watcher/views.py

@@ -0,0 +1,85 @@
+from django import forms
+from django.core.urlresolvers import reverse
+from django.db.models import F
+from django.shortcuts import redirect
+from django.template import RequestContext
+from django.utils.translation import ugettext as _
+from misago.authn.decorators import block_guest
+from misago.forms import Form, FormLayout, FormFields
+from misago.messages import Message
+from misago.watcher.models import ThreadWatch
+from misago.utils import make_pagination
+
+@block_guest
+def watched_threads(request, page=0, new=False):
+    # Find mode and fetch threads
+    queryset = ThreadWatch.objects.filter(user=request.user).filter(forum_id__in=request.acl.threads.get_readable_forums(request.acl)).select_related('thread').filter(thread__moderated=False).filter(thread__deleted=False)
+    if new:
+        queryset = queryset.filter(last_read__lt=F('thread__last'))
+    count = queryset.count()
+    pagination = make_pagination(page, count, request.settings.threads_per_page)
+    queryset = queryset.order_by('-thread__last')
+    if request.settings.threads_per_page < count:
+        queryset = queryset[pagination['start']:pagination['stop']]
+    queryset.prefetch_related('thread__forum', 'thread__last_poster')
+    threads = []
+    for thread in queryset:
+        thread.thread.send_email = thread.email
+        thread.thread.is_read = thread.thread.last <= thread.last_read             
+        threads.append(thread.thread)
+    
+    # Build form and handle submit
+    form = None
+    message = request.messages.get_message('watcher')
+    if threads:
+        form_fields = {}
+        form_fields['list_action'] = forms.ChoiceField(choices=(
+                                                                ('mails', _("Send me e-mails")),
+                                                                ('nomails', _("Don't send me e-mails")),
+                                                                ('delete', _("Remove from watched threads")),
+                                                                ))
+        list_choices = []
+        for item in threads:
+            list_choices.append((item.pk, None))
+        form_fields['list_items'] = forms.MultipleChoiceField(choices=list_choices, widget=forms.CheckboxSelectMultiple)
+        form = type('ThreadsWatchForm', (Form,), form_fields)
+        if request.method == 'POST':
+            form = form(request.POST, request=request)
+            if form.is_valid():
+                checked_items = []
+                for thread in threads:
+                    if str(thread.pk) in form.cleaned_data['list_items']:
+                        checked_items.append(thread.pk)
+                if checked_items:
+                    queryset = ThreadWatch.objects.filter(user=request.user).filter(thread_id__in = checked_items)
+                    if form.cleaned_data['list_action'] == 'mails':
+                        queryset.update(email=True)
+                        request.messages.set_flash(Message(_('Selected threads will now send e-mails with notifications when somebody replies to them.')), 'success', 'watcher')
+                    if form.cleaned_data['list_action'] == 'nomails':
+                        queryset.update(email=False)
+                        request.messages.set_flash(Message(_('Selected threads will no longer send e-mails with notifications when somebody replies to them.')), 'success', 'watcher')
+                    if form.cleaned_data['list_action'] == 'delete':
+                        queryset.delete()
+                        request.messages.set_flash(Message(_('Selected threads have been removed from watched threads list.')), 'success', 'watcher')
+                    return redirect(reverse('watched_threads_new' if new else 'watched_threads')) 
+                else:
+                    message = Message(_("You have to select at least one thread."), 'error')                    
+            else:
+                if 'list_action' in form.errors:
+                    message = Message(_("Action requested is incorrect."), 'error')
+                else:
+                    message = Message(form.non_field_errors()[0], 'error')
+        else:
+            form = form(request=request)
+            
+    # Display page
+    return request.theme.render_to_response('watched.html',
+                                            {
+                                             'total_items': count,
+                                             'pagination': pagination,
+                                             'new': new,
+                                             'list_form': FormFields(form).fields if form else None,
+                                             'threads': threads,
+                                             'message': message,
+                                             },
+                                            context_instance=RequestContext(request))

+ 11 - 0
templates/_email/post_notification_html.html

@@ -0,0 +1,11 @@
+{% extends "_email/base_html.html" %}
+{% load i18n %}
+{% load url from future %}
+
+{% block title %}{% trans %}New reply notification{% endtrans %}{% endblock %}
+
+{% block content %}
+<p {{ style_p|safe }}>{% trans username=user.username, author=author.username, thread=thread.name %}{{ username }}, you are receiving this message because {{ author }} has replied to thread "{{ thread }}" that you are watching.{% endtrans %}</p>
+<p {{ style_p|safe }}>{% trans %}To go to this reply follow the link below:{% endtrans %}</p>
+<a href="{{ board_address }}{% url 'thread_find' thread=thread.pk, slug=thread.slug, post=post.pk %}" {{ style_link|safe }}>{{ board_address }}{% url 'thread_find' thread=thread.pk, slug=thread.slug, post=post.pk %}</a>
+{% endblock %}

+ 12 - 0
templates/_email/post_notification_plain.html

@@ -0,0 +1,12 @@
+{% extends "_email/base_plain.html" %}
+{% load i18n %}
+{% load url from future %}
+
+{% block title %}{% trans %}New reply notification{% endtrans %}{% endblock %}
+
+{% block content %}
+{% trans username=user.username, author=author.username, thread=thread.name %}{{ username }}, you are receiving this message because {{ author }} has replied to thread "{{ thread }}" that you are watching.{% endtrans %}
+
+{% trans %}To go to this reply follow the link below:{% endtrans %}
+{{ board_address }}{% url 'thread_find' thread=thread.pk, slug=thread.slug, post=post.pk %}
+{% endblock %}

+ 2 - 1
templates/sora/profiles/content_posts.html

@@ -15,7 +15,6 @@
     {{ username }} has {{ total }} posts
     {%- endtrans -%}</small>{% endif %}</h2>
 
-{% if items_total %}
 <div class="list-nav">
   {{ pager() }}
   <ul class="nav nav-pills pull-right">
@@ -23,6 +22,8 @@
     <li class="info"><a href="{% url 'user_threads' user=profile.id, username=profile.username_slug %}">{% trans %}Threads{% endtrans %}</a></li>
   </ul>
 </div>
+
+{% if items_total %}
 <ul class="unstyled shorts-list">
   {% for item in items %}
   <li>

+ 3 - 2
templates/sora/profiles/content_threads.html

@@ -15,8 +15,7 @@
     {{ username }} started {{ total }} threads
     {%- endtrans -%}
     </small>{% endif %}</h2>
-
-{% if items_total %}
+    
 <div class="list-nav">
   {{ pager() }}
   <ul class="nav nav-pills pull-right">
@@ -24,6 +23,8 @@
     <li class="primary"><a href="{% url 'user_threads' user=profile.id, username=profile.username_slug %}">{% trans %}Threads{% endtrans %}</a></li>
   </ul>
 </div>
+
+{% if items_total %}
 <ul class="unstyled shorts-list">
   {% for item in items %}
   <li>

+ 1 - 1
templates/sora/threads/list.html

@@ -135,7 +135,7 @@
 
 {% block javascripts -%}
 {{ super() }}
-{%- if user.is_authenticated() and acl.threads.can_start_threads(forum) %}
+{%- if user.is_authenticated() and list_form %}
   <script type="text/javascript">
     $(function () {
       $('#threads_form').submit(function() {

+ 17 - 3
templates/sora/threads/thread.html

@@ -36,9 +36,23 @@
   {{ pager() }}
   {% if user.is_authenticated() %}
   <ul class="nav nav-pills pull-right">
-    {% if ignored_posts %}<li class="discourage"><form action="{% url 'thread_show_hidden' thread=thread.pk, slug=thread.slug %}" class="form-inline" method="post"><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"><button type="submit" class="btn"><i class="icon-eye-open"></i> {% trans %}Show Hidden Replies{% endtrans %}</button></form></li>{% endif %}
-    <li class="info"><a href="{% url 'thread_new' forum=forum.pk, slug=forum.slug %}"><i class="icon-ok"></i> {% trans %}Watch Thread{% endtrans %}</a></li>{% if acl.threads.can_reply(forum, thread) %}
-    <li class="primary"><a href="{% url 'thread_reply' thread=thread.pk, slug=thread.slug %}"><i class="icon-pencil"></i> {% trans %}Reply{% endtrans %}</a></li>{% endif %}
+    {% if ignored_posts %}
+    <li class="discourage"><form action="{% url 'thread_show_hidden' thread=thread.pk, slug=thread.slug %}" class="form-inline" method="post"><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"><button type="submit" class="btn"><i class="icon-eye-open"></i> {% trans %}Show Hidden Replies{% endtrans %}</button></form></li>
+    {% endif %}
+    {% if watcher %}
+    {% if watcher.email %}
+    <li class="success"><form action="{% url 'thread_unwatch_email' thread=thread.pk, slug=thread.slug %}" method="post"><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"><input type="hidden" name="retreat" value="{{ request_path }}"><button type="submit" class="btn tooltip-top" title="{% trans %}Don't e-mail me anymore if anyone replies to this thread{% endtrans %}"><i class="icon-envelope"></i></button></form></li>
+    {% else %}
+    <li class="info"><form action="{% url 'thread_watch_email' thread=thread.pk, slug=thread.slug %}" method="post"><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"><input type="hidden" name="retreat" value="{{ request_path }}"><button type="submit" class="btn tooltip-top" title="{% trans %}E-mail me if anyone replies{% endtrans %}"><i class="icon-envelope"></i></button></form></li>
+    {% endif %}
+    <li class="success"><form action="{% url 'thread_unwatch' thread=thread.pk, slug=thread.slug %}" method="post"><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"><input type="hidden" name="retreat" value="{{ request_path }}"><button type="submit" class="btn tooltip-top" title="{% trans %}Remove thread from watched list{% endtrans %}"><i class="icon-bookmark"></i></button></form></li>
+    {% else %}
+    <li class="info"><form action="{% url 'thread_watch_email' thread=thread.pk, slug=thread.slug %}" method="post"><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"><input type="hidden" name="retreat" value="{{ request_path }}"><button type="submit" class="btn tooltip-top" title="{% trans %}Add thread to watched list and e-mail me if anyone replies{% endtrans %}"><i class="icon-envelope"></i></button></form></li>
+    <li class="info"><form action="{% url 'thread_watch' thread=thread.pk, slug=thread.slug %}" method="post"><input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}"><input type="hidden" name="retreat" value="{{ request_path }}"><button type="submit" class="btn tooltip-top" title="{% trans %}Add thread to watched list{% endtrans %}"><i class="icon-bookmark"></i></button></form></li>
+    {% endif %}
+    {% if acl.threads.can_reply(forum, thread) %}
+    <li class="primary"><a href="{% url 'thread_reply' thread=thread.pk, slug=thread.slug %}"><i class="icon-pencil"></i> {% trans %}Reply{% endtrans %}</a></li>
+    {% endif %}
   </ul>
   {% endif %}
 </div>

+ 1 - 1
templates/sora/userbar.html

@@ -6,7 +6,7 @@
         <li><a href="{% url 'alerts' %}" title="{% if user.alerts %}{% trans %}You have new notifications!{% endtrans %}{% else %}{% trans %}Your Notifications{% endtrans %}{% endif %}" class="tooltip-bottom"><i class="icon-fire"></i>{% if user.alerts %}<span class="stat">{{ user.alerts }}</span>{% endif %}</a></li>
         {#<li><a href="#" title="{% trans %}Private messages{% endtrans %}" class="tooltip-bottom"><i class="icon-inbox"></i><span class="stat">2</span></a></li>#}
         <li><a href="{% url 'newsfeed' %}" title="{% trans %}Your News Feed{% endtrans %}" class="tooltip-bottom"><i class="icon-signal"></i></a></li>
-        <li><a href="#" title="{% trans %}Threads you are watching{% endtrans %}" class="tooltip-bottom"><i class="icon-bookmark"></i></a></li>{% endif %}
+        <li><a href="{% url 'watched_threads' %}" title="{% trans %}Threads you are watching{% endtrans %}" class="tooltip-bottom"><i class="icon-bookmark"></i></a></li>{% endif %}
         <li><a href="#" title="{% trans %}Today Posts{% endtrans %}" class="tooltip-bottom"><i class="icon-star"></i></a></li>
       </ul>
       <ul class="nav pull-right">{% if user.is_authenticated() %}

+ 123 - 0
templates/sora/watched.html

@@ -0,0 +1,123 @@
+{% extends "sora/layout.html" %}
+{% load i18n %}
+{% import "_forms.html" as form_theme with context %}
+{% import "sora/macros.html" as macros with context %}
+
+{% block title %}{{ macros.page_title(title=_('Threads you are watching')) }}{% endblock %}
+
+{% block content %}
+<div class="page-header">
+  <h1>{% trans %}Threads you are watching{% endtrans %}</h1>
+</div>
+
+{% if message %}{{ macros.draw_message(message) }}{% endif %}
+
+<div class="list-nav">
+  {{ pager() }}
+  <ul class="nav nav-pills pull-right">
+    <li class="{% if new %}primary{% else %}info{% endif %}"><a href="{% url 'watched_threads_new' %}">{% trans %}Unread Threads{% endtrans %}</a></li>
+    <li class="{% if not new %}primary{% else %}info{% endif %}"><a href="{% url 'watched_threads' %}">{% trans %}All Threads{% endtrans %}</a></li>
+  </ul>
+</div>
+
+{% if threads %}
+<table class="table table-striped threads-list">
+  <thead>
+    <tr>
+      <th style="width: 1%;">&nbsp;</th>
+      <th>{% trans %}Thread{% endtrans %}</th>
+      <th>{% trans %}Forum{% endtrans %}</th>
+      <th>{% trans %}Replies{% endtrans %}</th>
+      <th>{% trans %}Last Poster{% endtrans %}</th>
+      <th class="check-cell"><label class="checkbox"><input type="checkbox" class="checkbox-master"></label></th>
+    </tr>
+  </thead>
+  <tbody>
+    {% for thread in threads %}
+    <tr>
+      <td><span class="thread-icon{% if not thread.is_read %} {% if thread.closed %}thread-closed{% else %}thread-new{% endif %}{% endif %}"><i class="icon-comment icon-white"></i></span></td>
+      <td>
+        <a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}">{% if not thread.is_read %}<strong>{{ thread.name }}</strong>{% else %}{{ thread.name }}{% endif %}</a> {% if not thread.is_read -%}
+        <a href="{% url 'thread_new' thread=thread.pk, slug=thread.slug %}" class="jump jump-new tooltip-top" title="{% trans %}Jump to first unread post{% endtrans %}"><i class="icon-chevron-right"></i></a>
+        {%- else -%}
+        <a href="{% url 'thread_last' thread=thread.pk, slug=thread.slug %}" class="jump jump-last tooltip-top" title="{% trans %}Jump to last post{% endtrans %}"><i class="icon-chevron-right"></i></a>
+        {%- endif %}
+        <ul class="unstyled thread-flags">
+          {% if thread.send_email %}<li><span class="label label-success tooltip-top" title="{% trans %}You will receive notification on your e-mail when somebody replies to this thread.{% endtrans %}"><i class="icon-envelope icon-white"></i></span></li>{% endif %}
+          {% if thread.closed %}<li><span class="tooltip-top" title="{% trans %}This thread is closed for new replies.{% endtrans %}"><i class="icon-lock"></i></span></li>{% endif %}
+          {% if thread.moderated %}<li><span class="tooltip-top" title="{% trans %}This thread will not be visible to other members until moderator reviews it.{% endtrans %}"><i class="icon-eye-close"></i></span></li>{% endif %}
+          {% if thread.deleted %}<li><span class="tooltip-top" title="{% trans %}This thread has been deleted.{% endtrans %}"><i class="icon-remove"></i></span></li>{% endif %}
+        </ul>
+      </td>
+      <td class="span3 thread-author"><span class="tooltip-top" title="{{ thread.start|reltimesince }}">
+        <a href="{% url 'forum' forum=thread.forum.pk, slug=thread.forum.slug %}">{{ thread.forum.name }}</a>
+      </td>
+      <td class="span1 thread-stat">{{ thread.replies|intcomma }}</td>
+      <td class="span3 thread-poster"><span class="tooltip-top" title="{{ thread.last|reltimesince }}">{% if thread.last_poster_ignored -%}
+          {% if settings.avatars_on_threads_list %}<img src="{{ macros.avatar_guest(24) }}" alt="" class="avatar-tiny"> {% endif %}<em class="muted">{% trans %}Ignored Member{% endtrans %}</em>
+          {%- elif thread.last_poster_id -%}
+          {% if settings.avatars_on_threads_list %}<img src="{{ thread.last_poster.get_avatar(24) }}" alt="" class="avatar-tiny"> {% endif %}<a href="{% url 'user' user=thread.last_poster_id, username=thread.last_poster_slug %}">{{ thread.last_poster_name }}</a>
+          {%- else -%}
+          {% if settings.avatars_on_threads_list %}<img src="{{ macros.avatar_guest(24) }}" alt="" class="avatar-tiny"> {% endif %}<em class="muted">{{ thread.last_poster_name }}</em>
+          {%- endif %}</span></td>
+      <td class="check-cell"><label class="checkbox"><input form="threads_form" name="{{ list_form['list_items']['html_name'] }}" type="checkbox" class="checkbox-member" value="{{ thread.pk }}"{% if list_form['list_items']['has_value'] and ('' ~ thread.pk) in list_form['list_items']['value'] %} checked="checked"{% endif %}></label></td>      
+    </tr>
+    {% endfor %}
+  </tbody>
+</table>
+<div class="form-actions table-footer">
+  <form id="threads_form" class="form-inline pull-right" action="{% if new %}{% url 'watched_threads_new' %}{% else %}{% url 'watched_threads' %}{% endif %}" method="POST">
+    <input type="hidden" name="{{ csrf_id }}" value="{{ csrf_token }}">
+    {{ form_theme.input_select(list_form['list_action'],width=3) }}
+    <button type="submit" class="btn btn-primary">{% trans %}Go{% endtrans %}</button>
+  </form>
+</div>
+
+<div class="list-nav last">
+  {{ pager() }}
+</div>
+{% else %}
+<p class="lead">{% if new -%}
+    {% trans %}There are no unread threads that you are watching.{% endtrans %}
+    {%- else -%}
+    {% trans %}You are not watching any threads.{% endtrans %}
+    {%- endif %}</p>
+{% endif %}
+{% endblock %}
+
+
+{% block javascripts -%}
+{{ super() }}
+{%- if user.is_authenticated() and list_form %}
+  <script type="text/javascript">
+    $(function () {
+      $('#threads_form').submit(function() {
+        if ($('.check-cell[]:checked').length == 0) {
+          alert("{% trans %}You have to select at least one thread.{% endtrans %}");
+          return false;
+        }
+        return true;
+      });
+    });
+  </script>{% endif %}
+{%- endblock %}
+
+
+{% macro pager() -%}
+  <ul class="pager pull-left">
+    {% if new %}
+    {%- if pagination['prev'] > 1 %}<li><a href="{% url 'watched_threads_new' %}" class="tooltip-top" title="{% trans %}Latest Threads{% endtrans %}"><i class="icon-chevron-left"></i> {% trans %}Latest{% endtrans %}</a></li>{% endif -%}
+    {%- if pagination['prev'] > 0 %}<li><a href="{%- if pagination['prev'] > 1 %}{% url 'watched_threads_new' page=pagination['prev'] %}{% else %}{% url 'watched_threads_new' %}{% endif %}" class="tooltip-top" title="{% trans %}Newer Threads{% endtrans %}"><i class="icon-chevron-left"></i></a></li>{% endif -%}
+    {%- if pagination['next'] > 0 %}<li><a href="{% url 'watched_threads_new' page=pagination['next'] %}" class="tooltip-top" title="{% trans %}Older Threads{% endtrans %}"><i class="icon-chevron-right"></i></a></li>{% endif -%}
+    {% else %}
+    {%- if pagination['prev'] > 1 %}<li><a href="{% url 'watched_threads' %}" class="tooltip-top" title="{% trans %}Latest Threads{% endtrans %}"><i class="icon-chevron-left"></i> {% trans %}Latest{% endtrans %}</a></li>{% endif -%}
+    {%- if pagination['prev'] > 0 %}<li><a href="{%- if pagination['prev'] > 1 %}{% url 'watched_threads' page=pagination['prev'] %}{% else %}{% url 'watched_threads' %}{% endif %}" class="tooltip-top" title="{% trans %}Newer Threads{% endtrans %}"><i class="icon-chevron-left"></i></a></li>{% endif -%}
+    {%- if pagination['next'] > 0 %}<li><a href="{% url 'watched_threads' page=pagination['next'] %}" class="tooltip-top" title="{% trans %}Older Threads{% endtrans %}"><i class="icon-chevron-right"></i></a></li>{% endif -%}
+    {% endif %}
+    <li class="count">
+    {%- trans current_page=pagination['page'], pages=pagination['total'] -%}
+    Page {{ current_page }} of {{ pages }}
+    {%- endtrans -%}
+    </li>
+  </ul>
+{%- endmacro %}