Browse Source

api for reading posts

Rafał Pitoń 8 years ago
parent
commit
751031b681

+ 10 - 5
misago/readtracker/categoriestracker.py

@@ -28,7 +28,8 @@ def make_read_aware(user, categories):
 
 
     if categories_dict:
     if categories_dict:
         categories_records = user.categoryread_set.filter(
         categories_records = user.categoryread_set.filter(
-            category__in=categories_dict.keys())
+            category__in=categories_dict.keys()
+        )
 
 
         for record in categories_records:
         for record in categories_records:
             category = categories_dict[record.category_id]
             category = categories_dict[record.category_id]
@@ -67,9 +68,12 @@ def sync_record(user, category):
     all_threads_count = recorded_threads.count()
     all_threads_count = recorded_threads.count()
 
 
     read_threads = user.threadread_set.filter(
     read_threads = user.threadread_set.filter(
-        category=category, last_read_on__gt=cutoff_date)
+        category=category,
+        last_read_on__gt=cutoff_date
+    )
     read_threads_count = read_threads.filter(
     read_threads_count = read_threads.filter(
-        thread__last_post_on__lte=F("last_read_on")).count()
+        thread__last_post_on__lte=F("last_read_on")
+    ).count()
 
 
     category_is_read = read_threads_count == all_threads_count
     category_is_read = read_threads_count == all_threads_count
 
 
@@ -78,7 +82,7 @@ def sync_record(user, category):
 
 
     if category_record:
     if category_record:
         if category_is_read:
         if category_is_read:
-            category_record.last_read_on = category_record.last_read_on
+            category_record.last_read_on = timezone.now()
         else:
         else:
             category_record.last_read_on = cutoff_date
             category_record.last_read_on = cutoff_date
         category_record.save(update_fields=['last_read_on'])
         category_record.save(update_fields=['last_read_on'])
@@ -90,7 +94,8 @@ def sync_record(user, category):
 
 
         category_record = user.categoryread_set.create(
         category_record = user.categoryread_set.create(
             category=category,
             category=category,
-            last_read_on=last_read_on)
+            last_read_on=last_read_on
+        )
 
 
 
 
 def read_category(user, category):
 def read_category(user, category):

+ 1 - 2
misago/readtracker/threadstracker.py

@@ -91,13 +91,12 @@ def make_thread_read_aware(user, thread):
         try:
         try:
             category_record = user.categoryread_set.get(
             category_record = user.categoryread_set.get(
                 category_id=thread.category_id)
                 category_id=thread.category_id)
-
             if thread.last_post_on > category_record.last_read_on:
             if thread.last_post_on > category_record.last_read_on:
                 try:
                 try:
                     thread_record = user.threadread_set.get(thread=thread)
                     thread_record = user.threadread_set.get(thread=thread)
                     thread.last_read_on = thread_record.last_read_on
                     thread.last_read_on = thread_record.last_read_on
-                    thread.is_new = False
                     if thread.last_post_on <= thread_record.last_read_on:
                     if thread.last_post_on <= thread_record.last_read_on:
+                        thread.is_new = False
                         thread.is_read = True
                         thread.is_read = True
                     thread.read_record = thread_record
                     thread.read_record = thread_record
                 except ThreadRead.DoesNotExist:
                 except ThreadRead.DoesNotExist:

+ 12 - 0
misago/threads/api/postendpoints/read.py

@@ -0,0 +1,12 @@
+from rest_framework.response import Response
+
+from misago.readtracker.threadstracker import make_posts_read_aware, read_thread
+
+
+def post_read_endpoint(request, thread, post):
+    make_posts_read_aware(request.user, thread, [post])
+    if not post.is_read:
+        read_thread(request.user, thread, post)
+        if thread.subscription:
+            thread.subscription.last_read_on = post.posted_on
+    return Response({'detail': 'ok'})

+ 29 - 8
misago/threads/api/threadposts.py

@@ -20,15 +20,16 @@ from ..viewmodels.thread import ForumThread
 from .postingendpoint import PostingEndpoint
 from .postingendpoint import PostingEndpoint
 from .postendpoints.merge import posts_merge_endpoint
 from .postendpoints.merge import posts_merge_endpoint
 from .postendpoints.move import posts_move_endpoint
 from .postendpoints.move import posts_move_endpoint
-from .postendpoints.split import posts_split_endpoint
 from .postendpoints.patch_event import event_patch_endpoint
 from .postendpoints.patch_event import event_patch_endpoint
 from .postendpoints.patch_post import post_patch_endpoint
 from .postendpoints.patch_post import post_patch_endpoint
+from .postendpoints.read import post_read_endpoint
+from .postendpoints.split import posts_split_endpoint
 
 
 
 
 class ViewSet(viewsets.ViewSet):
 class ViewSet(viewsets.ViewSet):
     thread = None
     thread = None
     posts = None
     posts = None
-    post = None
+    post_ = None
 
 
     def get_thread(self, request, pk, read_aware=True, subscription_aware=True, select_for_update=False):
     def get_thread(self, request, pk, read_aware=True, subscription_aware=True, select_for_update=False):
         return self.thread(
         return self.thread(
@@ -52,7 +53,7 @@ class ViewSet(viewsets.ViewSet):
         return self.posts(request, thread, page)
         return self.posts(request, thread, page)
 
 
     def get_post(self, request, thread, pk, select_for_update=False):
     def get_post(self, request, thread, pk, select_for_update=False):
-        return self.post(request, thread, get_int_or_404(pk), select_for_update)
+        return self.post_(request, thread, get_int_or_404(pk), select_for_update)
 
 
     def get_post_for_update(self, request, thread, pk):
     def get_post_for_update(self, request, thread, pk):
         return self.get_post(request, thread, pk, select_for_update=True)
         return self.get_post(request, thread, pk, select_for_update=True)
@@ -182,10 +183,25 @@ class ViewSet(viewsets.ViewSet):
 
 
         return Response({})
         return Response({})
 
 
+    @detail_route(methods=['post'])
+    @transaction.atomic
+    def read(self, request, thread_pk, pk):
+        thread = self.get_thread(request, get_int_or_404(thread_pk))
+        post = self.get_post(request, thread, get_int_or_404(pk)).model
+
+        request.user.lock()
+
+        return post_read_endpoint(request, thread.model, post)
+
     @detail_route(methods=['get'], url_path='editor')
     @detail_route(methods=['get'], url_path='editor')
     def post_editor(self, request, thread_pk, pk):
     def post_editor(self, request, thread_pk, pk):
-        thread = self.thread(request, get_int_or_404(thread_pk))
-        post = self.post(request, thread, get_int_or_404(pk)).model
+        thread = self.get_thread(
+            request,
+            get_int_or_404(thread_pk),
+            read_aware=False,
+            subscription_aware=False
+        )
+        post = self.get_post(request, thread, get_int_or_404(pk)).model
 
 
         allow_edit_post(request.user, post)
         allow_edit_post(request.user, post)
 
 
@@ -200,11 +216,16 @@ class ViewSet(viewsets.ViewSet):
 
 
     @list_route(methods=['get'], url_path='editor')
     @list_route(methods=['get'], url_path='editor')
     def reply_editor(self, request, thread_pk):
     def reply_editor(self, request, thread_pk):
-        thread = self.thread(request, get_int_or_404(thread_pk))
+        thread = self.get_thread(
+            request,
+            get_int_or_404(thread_pk),
+            read_aware=False,
+            subscription_aware=False
+        )
         allow_reply_thread(request.user, thread.model)
         allow_reply_thread(request.user, thread.model)
 
 
         if 'reply' in request.query_params:
         if 'reply' in request.query_params:
-            reply_to = self.post(request, thread, get_int_or_404(request.query_params['reply'])).model
+            reply_to = self.get_post(request, thread, get_int_or_404(request.query_params['reply'])).model
 
 
             if reply_to.is_event:
             if reply_to.is_event:
                 raise PermissionDenied(_("You can't reply to events."))
                 raise PermissionDenied(_("You can't reply to events."))
@@ -223,4 +244,4 @@ class ViewSet(viewsets.ViewSet):
 class ThreadPostsViewSet(ViewSet):
 class ThreadPostsViewSet(ViewSet):
     thread = ForumThread
     thread = ForumThread
     posts = ThreadPosts
     posts = ThreadPosts
-    post = ThreadPost
+    post_ = ThreadPost

+ 3 - 0
misago/threads/models/post.py

@@ -113,6 +113,9 @@ class Post(models.Model):
     def get_editor_api_url(self):
     def get_editor_api_url(self):
         return self.thread_type.get_post_editor_api_url(self)
         return self.thread_type.get_post_editor_api_url(self)
 
 
+    def get_read_api_url(self):
+        return self.thread_type.get_post_read_api_url(self)
+
     def get_absolute_url(self):
     def get_absolute_url(self):
         return self.thread_type.get_post_absolute_url(self)
         return self.thread_type.get_post_absolute_url(self)
 
 

+ 1 - 1
misago/threads/serializers/post.py

@@ -93,7 +93,7 @@ class PostSerializer(serializers.ModelSerializer):
         return {
         return {
             'index': obj.get_api_url(),
             'index': obj.get_api_url(),
             'editor': obj.get_editor_api_url(),
             'editor': obj.get_editor_api_url(),
-            'read': 'nada',
+            'read': obj.get_read_api_url(),
         }
         }
 
 
     def get_url(self, obj):
     def get_url(self, obj):

+ 0 - 2
misago/threads/tests/test_thread_postdelete_api.py

@@ -1,5 +1,3 @@
-import time
-
 from django.core.urlresolvers import reverse
 from django.core.urlresolvers import reverse
 
 
 from .. import testutils
 from .. import testutils

+ 63 - 0
misago/threads/tests/test_thread_postread_api.py

@@ -0,0 +1,63 @@
+from django.core.urlresolvers import reverse
+from django.utils import timezone
+
+from .. import testutils
+from ..models import Post, Thread
+from .test_threads_api import ThreadsApiTestCase
+
+
+class PostReadApiTests(ThreadsApiTestCase):
+    def setUp(self):
+        super(PostReadApiTests, self).setUp()
+
+        self.post = testutils.reply_thread(
+            self.thread,
+            poster=self.user,
+            posted_on=timezone.now()
+        )
+
+        self.api_link = reverse('misago:api:thread-post-read', kwargs={
+            'thread_pk': self.thread.pk,
+            'pk': self.post.pk
+        })
+
+    def test_read_anonymous(self):
+        """api validates if reading user is authenticated"""
+        self.logout_user()
+
+        response = self.client.post(self.api_link)
+        self.assertContains(response, "This action is not available to guests.", status_code=403)
+
+    def test_read_post(self):
+        """api marks post as read"""
+        response = self.client.post(self.api_link)
+        self.assertEqual(response.status_code, 200)
+
+        thread_read = self.user.threadread_set.order_by('id').last()
+        self.assertEqual(thread_read.thread_id, self.thread.id)
+        self.assertEqual(thread_read.last_read_on, self.post.posted_on)
+
+        category_read = self.user.categoryread_set.order_by('id').last()
+        self.assertTrue(category_read.last_read_on >= self.post.posted_on)
+
+    def test_read_subscribed_thread_post(self):
+        """api marks post as read and updates subscription"""
+        self.thread.subscription_set.create(
+            user=self.user,
+            thread=self.thread,
+            category=self.thread.category,
+            last_read_on=self.thread.post_set.order_by('id').last().posted_on
+        )
+
+        response = self.client.post(self.api_link)
+        self.assertEqual(response.status_code, 200)
+
+        thread_read = self.user.threadread_set.order_by('id').last()
+        self.assertEqual(thread_read.thread_id, self.thread.id)
+        self.assertEqual(thread_read.last_read_on, self.post.posted_on)
+
+        category_read = self.user.categoryread_set.order_by('id').last()
+        self.assertTrue(category_read.last_read_on >= self.post.posted_on)
+
+        subscription = self.thread.subscription_set.order_by('id').last()
+        self.assertEqual(subscription.last_read_on, self.post.posted_on)

+ 1 - 1
misago/threads/testutils.py

@@ -50,7 +50,7 @@ def post_thread(category, title='Test thread', poster='Tester',
     reply_thread(
     reply_thread(
         thread,
         thread,
         poster=poster,
         poster=poster,
-        posted_on=thread.last_post_on,
+        posted_on=started_on,
         is_hidden=is_hidden,
         is_hidden=is_hidden,
         is_unapproved=is_unapproved,
         is_unapproved=is_unapproved,
     )
     )

+ 6 - 0
misago/threads/threadtypes/thread.py

@@ -118,3 +118,9 @@ class Thread(ThreadType):
             'thread_pk': post.thread_id,
             'thread_pk': post.thread_id,
             'pk': post.pk
             'pk': post.pk
         })
         })
+
+    def get_post_read_api_url(self, post):
+        return reverse('misago:api:thread-post-read', kwargs={
+            'thread_pk': post.thread_id,
+            'pk': post.pk
+        })

+ 10 - 12
misago/threads/views/goto.py

@@ -70,7 +70,7 @@ class ThreadGotoLastView(GotoView):
     thread = ForumThread
     thread = ForumThread
 
 
     def get_target_post(self, thread, posts_queryset, **kwargs):
     def get_target_post(self, thread, posts_queryset, **kwargs):
-        return posts_queryset.order_by('-id')[:1][0]
+        return posts_queryset.order_by('id').last()
 
 
 
 
 class ThreadGotoNewView(GotoView):
 class ThreadGotoNewView(GotoView):
@@ -78,13 +78,10 @@ class ThreadGotoNewView(GotoView):
     read_aware = True
     read_aware = True
 
 
     def get_target_post(self, thread, posts_queryset, **kwargs):
     def get_target_post(self, thread, posts_queryset, **kwargs):
-        return posts_queryset.order_by('-id')[:1][0]
-
-    def get_target_post(self, thread, posts_queryset, **kwargs):
-        try:
-            return posts_queryset.filter(posted_on__gt=thread.last_read_on).order_by('id')[:1][0]
-        except IndexError:
-            return posts_queryset.order_by('-id')[:1][0]
+        if thread.is_new:
+            return posts_queryset.filter(posted_on__gt=thread.last_read_on).order_by('id').first()
+        else:
+            return posts_queryset.order_by('id').last()
 
 
 
 
 class ThreadGotoUnapprovedView(GotoView):
 class ThreadGotoUnapprovedView(GotoView):
@@ -96,7 +93,8 @@ class ThreadGotoUnapprovedView(GotoView):
                 _("You need permission to approve content to be able to go to first unapproved post."))
                 _("You need permission to approve content to be able to go to first unapproved post."))
 
 
     def get_target_post(self, thread, posts_queryset, **kwargs):
     def get_target_post(self, thread, posts_queryset, **kwargs):
-        try:
-            return posts_queryset.filter(is_unapproved=True).order_by('id')[:1][0]
-        except IndexError:
-            return posts_queryset.order_by('-id')[:1][0]
+        unapproved_post = posts_queryset.filter(is_unapproved=True).order_by('id').first()
+        if unapproved_post:
+            return unapproved_post
+        else:
+            return posts_queryset.order_by('id').last()