Browse Source

Misc fixes in markdown parser and mentions functionality.

Ralfp 12 years ago
parent
commit
3451c5a39a

+ 13 - 0
misago/acl/builder.py

@@ -1,4 +1,5 @@
 from django.conf import settings
+from django.core.cache import cache, InvalidCacheBackendError
 from django.utils.importlib import import_module
 from misago.forms import Form
 from misago.forums.models import Forum
@@ -45,6 +46,18 @@ class ACL(object):
                 yield self.__dict__[attr]
 
 
+def get_acl(request, user):
+    acl_key = user.make_acl_key()
+    try:
+        user_acl = cache.get(acl_key)
+        if user_acl.version != request.monitor['acl_version']:
+            raise InvalidCacheBackendError()
+    except (AttributeError, InvalidCacheBackendError):
+        user_acl = build_acl(request, request.user.get_roles())
+        cache.set(acl_key, user_acl, 2592000)
+    return user_acl
+
+
 def build_acl(request, roles):
     acl = ACL(request.monitor['acl_version'])
     forums = Forum.objects.get(token='root').get_descendants().order_by('lft')

+ 3 - 12
misago/acl/middleware.py

@@ -1,18 +1,9 @@
-from django.core.cache import cache, InvalidCacheBackendError
-from misago.acl.builder import build_acl
+from misago.acl.builder import get_acl
 
 class ACLMiddleware(object):
     def process_request(self, request):
-        acl_key = request.user.make_acl_key()
-        try:
-            user_acl = cache.get(acl_key)
-            if user_acl.version != request.monitor['acl_version']:
-                raise InvalidCacheBackendError()
-        except (AttributeError, InvalidCacheBackendError):
-            user_acl = build_acl(request, request.user.get_roles())
-            cache.set(acl_key, user_acl, 2592000)
-
-        request.acl = user_acl
+        request.acl = get_acl(request, request.user)
+        
         if request.user.is_authenticated() and (request.acl.team or request.user.is_god()) != request.user.is_team:
             request.user.is_team = (request.acl.team or request.user.is_god())
             request.user.save(force_update=True)

+ 0 - 34
misago/markdown/extensions/ats.py

@@ -1,34 +0,0 @@
-import re
-import markdown
-from markdown.util import etree
-
-# Global vars
-QUOTE_AUTHOR_RE = re.compile(r'^(?P<arrows>(>|\s)+)?@(?P<username>(\w|\d)+)$')
-
-class AtsExtension(markdown.Extension):
-    def extendMarkdown(self, md):
-        md.registerExtension(self)
-        md.preprocessors.add('mi_usernames',
-                             AtsPreprocessor(md),
-                             '>mi_quote_title')
-        md.postprocessors.add('mi_usernames',
-                              AtsPostprocessor(md),
-                              '>mi_quote_title')
-
-
-class AtsPreprocessor(markdown.preprocessors.Preprocessor):
-    def __init__(self, md):
-        markdown.preprocessors.Preprocessor.__init__(self, md)
-
-    def run(self, lines):
-        clean = []
-        for l, line in enumerate(lines):
-            clean.append(line)
-        return clean
-
-
-class AtsPostprocessor(markdown.postprocessors.Postprocessor):
-    def run(self, text):
-        text = text.replace('&lt;%s:username&gt;' % self.markdown.mi_token, '<username>')
-        text = text.replace('&lt;/%s:username&gt;' % self.markdown.mi_token, '</username>')
-        return text

+ 58 - 0
misago/markdown/extensions/mentions.py

@@ -0,0 +1,58 @@
+import re
+import markdown
+from markdown.util import etree
+from django.core.urlresolvers import reverse
+from misago.users.models import User
+from misago.utils import slugify
+
+# Global vars
+MENTION_RE = re.compile(r'([^\w\d]?)@(?P<username>(\w|\d)+)')
+
+class MentionsExtension(markdown.Extension):
+    def extendMarkdown(self, md):
+        md.mentions = {}
+        md.registerExtension(self)
+        md.preprocessors.add('mi_mentions',
+                             MentionsPreprocessor(md),
+                             '>mi_quote_title')
+        md.postprocessors.add('mi_mentions',
+                              MentionsPostprocessor(md),
+                              '>mi_quote_title')
+
+
+class MentionsPreprocessor(markdown.preprocessors.Preprocessor):
+    def __init__(self, md):
+        markdown.preprocessors.Preprocessor.__init__(self, md)
+        self.md = md
+
+    def run(self, lines):
+        def mention(match):
+            slug = slugify(match.group(0)[1:])
+            if slug in self.md.mentions:
+                user = self.md.mentions[slug]
+                return '%s[@%s](%s)' % (match.group(1), user.username, reverse('user', kwargs={
+                                                                                              'user': user.pk,
+                                                                                              'username': user.username_slug,
+                                                                                              }))
+            elif len(self.md.mentions) < 32:
+                try:
+                    user = User.objects.get(username_slug=slug)
+                    self.md.mentions[slug] = user
+                    return '%s[@%s](%s)' % (match.group(1), user.username, reverse('user', kwargs={
+                                                                                                  'user': user.pk,
+                                                                                                  'username': user.username_slug,
+                                                                                                  }))
+                except User.DoesNotExist:
+                    pass
+            return match.group(0)
+        clean = []
+        for l, line in enumerate(lines):
+            if line.strip():
+                line = MENTION_RE.sub(mention, line)
+            clean.append(line)
+        return clean
+
+
+class MentionsPostprocessor(markdown.postprocessors.Postprocessor):
+    def run(self, text):
+        return text

+ 12 - 6
misago/markdown/extensions/quotes.py

@@ -27,15 +27,11 @@ class QuoteTitlesPreprocessor(markdown.preprocessors.Preprocessor):
                 if line.strip():
                     at_match = QUOTE_AUTHOR_RE.match(line.strip())
                     if at_match and lines[l + 1].strip()[0] == '>':
-                        username = '<%(token)s:quotetitle><%(token)s:username>%(name)s</%(token)s:username></%(token)s:quotetitle>' % {'token': self.markdown.mi_token, 'name': at_match.group('username')}
+                        username = '<%(token)s:quotetitle>@%(name)s</%(token)s:quotetitle>' % {'token': self.markdown.mi_token, 'name': at_match.group('username')}
                         if at_match.group('arrows'):
                             clean.append('> %s%s' % (at_match.group('arrows'), username))
                         else:
                             clean.append('> %s' % username)
-                        try:
-                            self.markdown.mi_usernames.append(username)
-                        except AttributeError:
-                            self.markdown.mi_usernames = [username]
                     else:
                         clean.append(line)
                 else:
@@ -49,4 +45,14 @@ class QuoteTitlesPostprocessor(markdown.postprocessors.Postprocessor):
     def run(self, text):
         text = text.replace('&lt;%s:quotetitle&gt;' % self.markdown.mi_token, '<h3><quotetitle>')
         text = text.replace('&lt;/%s:quotetitle&gt;' % self.markdown.mi_token, '</quotetitle></h3>')
-        return text
+        lines = text.splitlines()
+        clean = []
+        for l, line in enumerate(lines):
+            clean.append(line)
+            try:
+                if line == '<blockquote>':
+                    if lines[l + 1][0:7] != '<p><h3>':
+                        clean.append('<h3><quotesingletitle></h3>')
+            except IndexError:
+                pass
+        return '\r\n'.join(clean)

+ 3 - 2
misago/markdown/factory.py

@@ -88,10 +88,11 @@ def post_markdown(request, text):
     # Final cleanups
     text = text.replace('<p><h3><quotetitle>', '<h3><quotetitle>')
     text = text.replace('</quotetitle></h3></p>', '</quotetitle></h3>')
-    text = text.replace('</quotetitle></h3><br>\n', '</quotetitle></h3>\n<p>')
-    text = text.replace('\n<p></p>', '')
+    text = text.replace('</quotetitle></h3><br>\r\n', '</quotetitle></h3>\r\n<p>')
+    text = text.replace('\r\n<p></p>', '')
     def trans_quotetitle(match):
         return _("Posted by %(user)s") % {'user': match.group('content')}
     text = re.sub(r'<quotetitle>(?P<content>.+)</quotetitle>', trans_quotetitle, text)
+    text = re.sub(r'<quotesingletitle>', _("Quote"), text)
 
     return md, text

+ 1 - 1
misago/settings_base.py

@@ -128,7 +128,7 @@ PROFILE_EXTENSIONS = (
 # List of Markdown Extensions
 MARKDOWN_EXTENSIONS = (
     'misago.markdown.extensions.quotes.QuoteTitlesExtension',
-    'misago.markdown.extensions.ats.AtsExtension',
+    'misago.markdown.extensions.mentions.MentionsExtension',
 )
 
 # Name of root urls configuration

+ 23 - 1
misago/threads/models.py

@@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy as _
 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
+from misago.utils import slugify, ugettext_lazy
 
 class ThreadManager(models.Manager):
     def filter_stats(self, start, end):
@@ -108,6 +108,7 @@ class Post(models.Model):
     post_preparsed = models.TextField()
     upvotes = models.PositiveIntegerField(default=0)
     downvotes = models.PositiveIntegerField(default=0)
+    mentions = models.ManyToManyField('users.User', related_name="mention_set")
     checkpoints = models.BooleanField(default=False, db_index=True)
     date = models.DateTimeField()
     edits = models.PositiveIntegerField(default=0)
@@ -152,6 +153,27 @@ class Post(models.Model):
                                        ip=request.session.get_ip(request),
                                        agent=request.META.get('HTTP_USER_AGENT'),
                                        )
+            
+    def notify_mentioned(self, request, users):
+        from misago.acl.builder import get_acl
+        from misago.acl.utils import ACLError403, ACLError404
+        
+        mentioned = self.mentions.all()
+        for slug, user in users.items():
+            if user.pk != request.user.pk and user not in mentioned:
+                self.mentions.add(user)
+                try:                    
+                    acl = get_acl(request, user)
+                    acl.forums.allow_forum_view(self.forum)
+                    acl.threads.allow_thread_view(user, self.thread)
+                    acl.threads.allow_post_view(user, self.thread, self)
+                    if not user.is_ignoring(request.user):
+                        alert = user.alert(ugettext_lazy("%(username)s has mentioned you in his reply in thread %(thread)s").message)
+                        alert.profile('username', request.user)
+                        alert.post('thread', self.thread, self)
+                        alert.save_all()
+                except (ACLError403, ACLError404):
+                    pass
 
 
 class Change(models.Model):

+ 1 - 1
misago/threads/views/changelog.py

@@ -114,7 +114,7 @@ class ChangelogRevertView(ChangelogDiffView):
 
         if self.change.post_content != self.post.post:
             self.post.post = self.change.post_content
-            self.post.post_preparsed = post_markdown(request, self.change.post_content)
+            md, self.post.post_preparsed = post_markdown(request, self.change.post_content)
             self.post.save(force_update=True)
 
         request.messages.set_flash(Message(_("Post has been reverted previous state.")), 'success', 'threads_%s' % self.post.pk)

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

@@ -98,7 +98,7 @@ class PostingView(BaseView):
         message = request.messages.get_message('threads')
         if request.method == 'POST':
             form = self.get_form(True)
-            if form.is_valid():
+            if form.is_valid():                
                 # Record original vars if user is editing 
                 if self.mode in ['edit_thread', 'edit_post']:
                     old_name = self.thread.name
@@ -110,6 +110,7 @@ class PostingView(BaseView):
 
                 # Some extra initialisation
                 now = timezone.now()
+                md = None
                 moderation = False
                 if not request.acl.threads.acl[self.forum.pk]['can_approve']:
                     if self.mode == 'new_thread' and request.acl.threads.acl[self.forum.pk]['can_start_threads'] == 1:
@@ -148,12 +149,13 @@ class PostingView(BaseView):
                         post.moderated = moderation
                         post.date = now
                         post.post = '%s\n\n- - -\n**%s**\n%s' % (post.post, _("Added on %(date)s:") % {'date': date(now, 'SHORT_DATETIME_FORMAT')}, form.cleaned_data['post'])
-                        post.post_preparsed = post_markdown(request, post.post)
+                        md, post.post_preparsed = post_markdown(request, post.post)
                         post.save(force_update=True)
                         # Ignore rest of posting action
                         request.messages.set_flash(Message(_("Your reply has been added to previous one.")), 'success', 'threads_%s' % post.pk)
                         return self.redirect_to_post(post)
                     else:
+                        md, post_preparsed = post_markdown(request, form.cleaned_data['post'])
                         post = Post.objects.create(
                                                    forum=self.forum,
                                                    thread=thread,
@@ -163,7 +165,7 @@ class PostingView(BaseView):
                                                    ip=request.session.get_ip(request),
                                                    agent=request.META.get('HTTP_USER_AGENT'),
                                                    post=form.cleaned_data['post'],
-                                                   post_preparsed=post_markdown(request, form.cleaned_data['post']),
+                                                   post_preparsed=post_preparsed,
                                                    date=now,
                                                    moderated=moderation,
                                                    )
@@ -171,7 +173,7 @@ class PostingView(BaseView):
                     # Change message
                     post = self.post
                     post.post = form.cleaned_data['post']
-                    post.post_preparsed = post_markdown(request, form.cleaned_data['post'])
+                    md, post.post_preparsed = post_markdown(request, form.cleaned_data['post'])
                     post.edits += 1
                     post.edit_date = now
                     post.edit_user = request.user
@@ -273,6 +275,17 @@ class PostingView(BaseView):
                 if self.mode in ['new_thread', 'new_post', 'new_post_quick']:
                     request.user.last_post = thread.last
                     request.user.save(force_update=True)
+                    
+                # Notify users about post
+                if md:
+                    try:
+                        if self.quote and self.quote.user_id:
+                            del md.mentions[self.quote.user.username_slug]
+                    except KeyError:
+                        pass
+                    if md.mentions:
+                        self.post.notify_mentioned(request, md.mentions)
+                        self.post.save(force_update=True)
 
                 # Set flash and redirect user to his post
                 if self.mode == 'new_thread':

+ 1 - 1
misago/threads/views/thread.py

@@ -147,7 +147,7 @@ class ThreadView(BaseView):
         for post in posts[1:]:
             post.merge_with(new_post)
             post.delete()
-        new_post.post_preparsed = post_markdown(self.request, new_post.post)
+        md, new_post.post_preparsed = post_markdown(self.request, new_post.post)
         new_post.save(force_update=True)
         self.thread.sync()
         self.thread.save(force_update=True)