Browse Source

mention other users in posts

Rafał Pitoń 8 years ago
parent
commit
d143258d4e

+ 1 - 0
misago/conf/defaults.py

@@ -135,6 +135,7 @@ MISAGO_POSTING_MIDDLEWARES = (
     'misago.threads.api.postingendpoint.protect.ProtectMiddleware',
     # 'misago.threads.api.postingendpoint.recordedit.RecordEditMiddleware',
     'misago.threads.api.postingendpoint.updatestats.UpdateStatsMiddleware',
+    'misago.threads.api.postingendpoint.mentions.MentionsMiddleware',
     # Note: always keep SaveChangesMiddleware middleware last one
     'misago.threads.api.postingendpoint.savechanges.SaveChangesMiddleware',
 )

+ 6 - 4
misago/markup/parser.py

@@ -2,6 +2,7 @@ import markdown
 
 import bleach
 from bs4 import BeautifulSoup
+from django.utils import six
 from htmlmin.minify import html_minify
 
 from .bbcode import blocks, inline
@@ -133,9 +134,10 @@ def clean_links(request, result):
             result['outgoing_links'].append(link['href'])
 
         if link.string.startswith('http://'):
-            link.string = link.string[7:].strip()
+            link.string.replace_with(link.string[7:].strip())
         if link.string.startswith('https://'):
-            link.string = link.string[8:].strip()
+            print link.string
+            link.string.replace_with(link.string[8:].strip())
 
     for img in soup.find_all('img'):
         result['images'].append(img['src'])
@@ -150,8 +152,8 @@ def clean_links(request, result):
         if img['alt'].startswith('https://'):
             img['alt'] = img['alt'][8:].strip()
 
-    if result['outgoing_links'] or result['inside_links'] or result['images']:
-        result['parsed_text'] = soup.prettify()
+    # [6:-7] trims <body></body> wrap
+    result['parsed_text'] = six.text_type(soup.body)[6:-7]
 
 
 def minify_result(result):

+ 2 - 2
misago/markup/tests/test_mentions.py

@@ -64,10 +64,10 @@ class MentionsTests(AuthenticatedUserTestCase):
 
     def test_multiple_mentions(self):
         """markup extension handles multiple mentions"""
-        before = '<p>Hello, @{0}, how you feel now, @{0}?</p>'.format(self.user.username)
+        before = '<p>Hello @{0} and @{0}, how is it going?</p>'.format(self.user.username)
 
         formats = (self.user.get_absolute_url(), self.user.username)
-        after = '<p>Hello, <a href="{0}">@{1}</a>, how you feel now, <a href="{0}">@{1}</a>?</p>'.format(*formats)
+        after = '<p>Hello <a href="{0}">@{1}</a> and <a href="{0}">@{1}</a>, how is it going?</p>'.format(*formats)
 
         result = {
             'parsed_text': before,

+ 24 - 4
misago/markup/tests/test_parser.py

@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 
+from django.contrib.auth import get_user_model
 from django.test import TestCase
 
 from ..parser import parse
@@ -9,6 +10,9 @@ from ..parser import parse
 class MockRequest(object):
     scheme = 'http'
 
+    def __init__(self, user=None):
+        self.user = user
+
     def get_host(self):
         return 'test.com'
 
@@ -21,9 +25,9 @@ class BBCodeTests(TestCase):
     def test_inline_text(self):
         """inline elements are correctly parsed"""
         test_text = """
-Lorem **ipsum** dolor met.
+Lorem **ipsum**, dolor met.
 
-Lorem [b]ipsum[/b] [i]dolor[/i] [u]met[/u].
+Lorem [b]ipsum[/b], [i]dolor[/i] [u]met[/u].
 
 Lorem [b]**ipsum**[/b] [i]dolor[/i] [u]met[/u].
 
@@ -39,8 +43,8 @@ Lorem [b]ipsum[/B].
 """.strip()
 
         expected_result = """
-<p>Lorem <strong>ipsum</strong> dolor met.</p>
-<p>Lorem <b>ipsum</b> <i>dolor</i> <u>met</u>.</p>
+<p>Lorem <strong>ipsum</strong>, dolor met.</p>
+<p>Lorem <b>ipsum</b>, <i>dolor</i> <u>met</u>.</p>
 <p>Lorem <b><strong>ipsum</strong></b> <i>dolor</i> <u>met</u>.</p>
 <p>Lorem <b>**ipsum</b>** <i>dolor</i> <u>met</u>.</p>
 <p>Lorem <b>__ipsum</b>__ <i>dolor</i> <u>met</u>.</p>
@@ -101,6 +105,22 @@ Lorem ipsum.
         result = parse(test_text, MockRequest(), MockPoster(), minify=True)
         self.assertEqual(expected_result, result['parsed_text'])
 
+    def test_complex_paragraph(self):
+        """parser minifies complex paragraph"""
+        User = get_user_model()
+        user = User.objects.create_user('Bob', 'bob@test.com', 'Pass123')
+
+        test_text = """
+Hey there @{}, how's going?
+""".strip().format(user)
+
+        expected_result = """
+<p>Hey there <a href="{}">@{}</a>, how's going?</p>
+""".strip().format(user.get_absolute_url(), user)
+
+        result = parse(test_text, MockRequest(user), user, minify=True)
+        self.assertEqual(expected_result, result['parsed_text'])
+
 
 class CleanLinksTests(TestCase):
     def test_clean_current_link(self):

+ 19 - 0
misago/threads/api/postingendpoint/mentions.py

@@ -0,0 +1,19 @@
+from . import PostingEndpoint, PostingMiddleware
+
+
+class MentionsMiddleware(PostingMiddleware):
+    def post_save(self, serializer):
+        existing_mentions = []
+        if self.mode == PostingEndpoint.EDIT:
+            existing_mentions = self.get_existing_mentions()
+
+        new_mentions = []
+        for user in self.post.parsing_result['mentions']:
+            if user.pk not in existing_mentions:
+                new_mentions.append(user)
+
+        if new_mentions:
+            self.post.mentions.add(*new_mentions)
+
+    def get_existing_mentions(self):
+        return [u['id'] for u in self.post.mentions.values('id').iterator()]

+ 3 - 0
misago/threads/api/postingendpoint/reply.py

@@ -42,6 +42,9 @@ class ReplyMiddleware(PostingMiddleware):
 
         self.thread.save()
 
+        # annotate post for future middlewares
+        self.post.parsing_result = parsing_result
+
     def new_thread(self, validated_data):
         self.thread.set_title(validated_data['title'])
         self.thread.starter_name = self.user.username

+ 8 - 1
misago/threads/signals.py

@@ -13,7 +13,7 @@ from .models import Post, Thread
 
 delete_post = Signal()
 delete_thread = Signal()
-merge_post = Signal()
+merge_post = Signal(providing_args=["other_post"])
 merge_thread = Signal(providing_args=["other_thread"])
 move_post = Signal()
 move_thread = Signal()
@@ -29,6 +29,13 @@ def merge_threads_posts(sender, **kwargs):
     other_thread.post_set.update(category=sender.category, thread=sender)
 
 
+@receiver(merge_post)
+def merge_posts(sender, **kwargs):
+    other_post = kwargs['other_post']
+    for user in sender.mentions.iterator():
+        other_post.mentions.add(user)
+
+
 @receiver(move_thread)
 def move_thread_content(sender, **kwargs):
     sender.post_set.update(category=sender.category)

+ 181 - 0
misago/threads/tests/test_post_mentions.py

@@ -0,0 +1,181 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.contrib.auth import get_user_model
+from django.core.urlresolvers import reverse
+from django.test.client import BOUNDARY, MULTIPART_CONTENT, encode_multipart
+
+from misago.acl.testutils import override_acl
+from misago.categories.models import Category
+from misago.markup.mentions import MENTIONS_LIMIT
+from misago.users.testutils import AuthenticatedUserTestCase
+
+from .. import testutils
+
+
+class PostMentionsTests(AuthenticatedUserTestCase):
+    def setUp(self):
+        super(PostMentionsTests, self).setUp()
+
+        self.category = Category.objects.get(slug='first-category')
+        self.thread = testutils.post_thread(category=self.category)
+        self.override_acl()
+
+        self.post_link = reverse('misago:api:thread-post-list', kwargs={
+            'thread_pk': self.thread.pk
+        })
+
+    def override_acl(self):
+        new_acl = self.user.acl
+        new_acl['categories'][self.category.pk].update({
+            'can_see': 1,
+            'can_browse': 1,
+            'can_start_threads': 1,
+            'can_reply_threads': 1,
+            'can_edit_posts': 1
+        })
+
+        override_acl(self.user, new_acl)
+
+    def put(self, url, data=None):
+        content = encode_multipart(BOUNDARY, data or {})
+        return self.client.put(url, content, content_type=MULTIPART_CONTENT)
+
+    def test_mention_noone(self):
+        """endpoint handles no mentions in post"""
+        response = self.client.post(self.post_link, data={
+            'post': "This is test response!"
+        })
+        self.assertEqual(response.status_code, 200)
+
+        post = self.user.post_set.all()[:1][0]
+        self.assertEqual(post.mentions.count(), 0)
+
+    def test_mention_nonexistant(self):
+        """endpoint handles nonexistant mention"""
+        response = self.client.post(self.post_link, data={
+            'post': "This is test response, @InvalidUser!"
+        })
+        self.assertEqual(response.status_code, 200)
+
+        post = self.user.post_set.all()[:1][0]
+        self.assertEqual(post.mentions.count(), 0)
+
+    def test_mention_self(self):
+        """endpoint mentions author"""
+        response = self.client.post(self.post_link, data={
+            'post': "This is test response, @{}!".format(self.user)
+        })
+        self.assertEqual(response.status_code, 200)
+
+        post = self.user.post_set.all()[:1][0]
+
+        self.assertEqual(post.mentions.count(), 1)
+        self.assertEqual(post.mentions.all()[0], self.user)
+
+    def test_mention_limit(self):
+        """endpoint mentions limits mentions to 24 users"""
+        users = []
+
+        User = get_user_model()
+        for i in range(MENTIONS_LIMIT + 5):
+            users.append(User.objects.create_user(
+                'Mention{}'.format(i),
+                'mention{}@bob.com'.format(i),
+                'pass123'
+            ))
+
+        mentions = ['@{}'.format(u) for u in users]
+        response = self.client.post(self.post_link, data={
+            'post': "This is test response, {}!".format(', '.join(mentions))
+        })
+        self.assertEqual(response.status_code, 200)
+
+        post = self.user.post_set.all()[:1][0]
+
+        self.assertEqual(post.mentions.count(), 24)
+        self.assertEqual(list(post.mentions.order_by('id')), users[:24])
+
+    def test_mention_update(self):
+        """edit post endpoint updates mentions"""
+        User = get_user_model()
+        user_a = User.objects.create_user('Mention', 'mention@test.com', 'pass123')
+        user_b = User.objects.create_user('MentionB', 'mentionb@test.com', 'pass123')
+
+        response = self.client.post(self.post_link, data={
+            'post': "This is test response, @{}!".format(user_a)
+        })
+        self.assertEqual(response.status_code, 200)
+
+        post = self.user.post_set.all()[:1][0]
+
+        self.assertEqual(post.mentions.count(), 1)
+        self.assertEqual(post.mentions.all()[0], user_a)
+
+        # add mention to post
+        edit_link = reverse('misago:api:thread-post-detail', kwargs={
+            'thread_pk': self.thread.pk,
+            'pk': post.pk
+        })
+
+        self.override_acl()
+        response = self.put(edit_link, data={
+            'post': "This is test response, @{} and @{}!".format(user_a, user_b)
+        })
+        self.assertEqual(response.status_code, 200)
+
+        self.assertEqual(post.mentions.count(), 2)
+        self.assertEqual(list(post.mentions.all()), [user_a, user_b])
+
+        # remove first mention from post - should preserve mentions
+        self.override_acl()
+        response = self.put(edit_link, data={
+            'post': "This is test response, @{}!".format(user_b)
+        })
+        self.assertEqual(response.status_code, 200)
+
+        self.assertEqual(post.mentions.count(), 2)
+        self.assertEqual(list(post.mentions.all()), [user_a, user_b])
+
+        # remove mentions from post - should preserve mentions
+        self.override_acl()
+        response = self.put(edit_link, data={
+            'post': "This is test response!"
+        })
+        self.assertEqual(response.status_code, 200)
+
+        self.assertEqual(post.mentions.count(), 2)
+        self.assertEqual(list(post.mentions.all()), [user_a, user_b])
+
+    def test_mentions_merge(self):
+        """posts merge sums mentions"""
+        User = get_user_model()
+        user_a = User.objects.create_user('Mention', 'mention@test.com', 'pass123')
+        user_b = User.objects.create_user('MentionB', 'mentionb@test.com', 'pass123')
+
+        response = self.client.post(self.post_link, data={
+            'post': "This is test response, @{}!".format(user_a)
+        })
+        self.assertEqual(response.status_code, 200)
+
+        post_a = self.user.post_set.all()[:1][0]
+
+        self.assertEqual(post_a.mentions.count(), 1)
+        self.assertEqual(list(post_a.mentions.all()), [user_a])
+
+        # post second reply
+        self.user.last_post_on = None
+        self.user.save()
+
+        response = self.client.post(self.post_link, data={
+            'post': "This is test response, @{} and @{}!".format(user_a, user_b)
+        })
+        self.assertEqual(response.status_code, 200)
+
+        post_b = self.user.post_set.all()[:1][0]
+
+        # merge posts and validate that post A has all mentions
+        post_b.merge(post_a)
+
+        self.assertEqual(post_a.mentions.count(), 2)
+        self.assertEqual(list(post_a.mentions.all()), [user_a, user_b])