Some refactoring on mdrender

remotes/origin/enhancement/email-actions
Jesús Espino 2014-05-21 09:25:22 +02:00
parent 3c4696962f
commit c7ee910647
16 changed files with 196 additions and 260 deletions

View File

@ -3,6 +3,7 @@
# Tested on Markdown 2.3.1 # Tested on Markdown 2.3.1
# #
# Copyright (c) 2014, Esteban Castro Borsani # Copyright (c) 2014, Esteban Castro Borsani
# Copyright (c) 2014, Jesús Espino García
# The MIT License (MIT) # The MIT License (MIT)
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy # Permission is hereby granted, free of charge, to any person obtaining a copy
@ -171,7 +172,7 @@ class EmojifyPreprocessor(Preprocessor):
url = emojis_path + emoji + u'.png' url = emojis_path + emoji + u'.png'
return u'![%(emoji)s](%(url)s)' % {'emoji': emoji, 'url': url} return '![{emoji}]({url})'.format(emoji=emoji, url=url)
for line in lines: for line in lines:
if line.strip(): if line.strip():

View File

@ -28,41 +28,44 @@ import re
import os import os
from markdown.extensions import Extension from markdown.extensions import Extension
from markdown.preprocessors import Preprocessor from markdown.inlinepatterns import Pattern
from markdown.util import etree
from taiga.users.models import User
class MentionsExtension(Extension): class MentionsExtension(Extension):
def extendMarkdown(self, md, md_globals): def extendMarkdown(self, md, md_globals):
md.registerExtension(self) MENTION_RE = r'(?<=^|(?<=[^a-zA-Z0-9-_\.]))@([A-Za-z]+[A-Za-z0-9-]+)'
md.preprocessors.add('emojify', mentionsPattern = MentionsPattern(MENTION_RE)
MentionsPreprocessor(md), mentionsPattern.md = md
'_end') md.inlinePatterns.add('mentions',
mentionsPattern,
'_begin')
class MentionsPreprocessor(Preprocessor): class MentionsPattern(Pattern):
def handleMatch(self, m):
if m.group(2).strip():
username = m.group(2)
def run(self, lines): try:
new_lines = [] user = User.objects.get(username=username)
pattern = re.compile('(?<=^|(?<=[^a-zA-Z0-9-_\.]))@([A-Za-z]+[A-Za-z0-9]+)') except User.DoesNotExist:
return "@{}".format(username)
def make_mention_link(m): url = "/#/profile/{}".format(username)
name = m.group(1)
if not User.objects.filter(username=name): link_text = "&commat;{}".format(username)
return "@{name}".format(name=name)
tpl = ('[@{name}](/#/profile/{name} "@{name}")')
return tpl.format(name=name)
for line in lines:
if line.strip():
line = pattern.sub(make_mention_link, line)
new_lines.append(line)
return new_lines
a = etree.Element('a')
a.text = link_text
a.set('href', url)
a.set('alt', user.get_full_name())
a.set('title', user.get_full_name())
a.set('class', "mention")
return a
return ''
def makeExtension(configs=None): def makeExtension(configs=None):
return MentionsExtension(configs=configs) return MentionsExtension(configs=configs)

View File

@ -0,0 +1,96 @@
#-*- coding: utf-8 -*-
# Tested on Markdown 2.3.1
#
# Copyright (c) 2014, Esteban Castro Borsani
# The MIT License (MIT)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import re
import os
from markdown.extensions import Extension
from markdown.inlinepatterns import Pattern
from markdown.util import etree
from taiga.projects.references.services import get_instance_by_ref
class TaigaReferencesExtension(Extension):
def __init__(self, project, *args, **kwargs):
self.project = project
return super().__init__(*args, **kwargs)
def extendMarkdown(self, md, md_globals):
TAIGA_REFERENCE_RE = r'(?<=^|(?<=[^a-zA-Z0-9-\[]))#(\d+)'
referencesPattern = TaigaReferencesPattern(TAIGA_REFERENCE_RE, self.project)
referencesPattern.md = md
md.inlinePatterns.add('taiga-references',
referencesPattern,
'_begin')
class TaigaReferencesPattern(Pattern):
def __init__(self, pattern, project):
self.project = project
super().__init__(pattern)
def handleMatch(self, m):
if m.group(2).strip():
obj_ref = m.group(2)
instance = get_instance_by_ref(self.project.id, obj_ref)
if instance is None:
return "#{}".format(obj_ref)
subject = instance.content_object.subject
if instance.content_type.model == "userstory":
obj_section = "user-story"
html_classes = "reference user-story"
elif instance.content_type.model == "task":
obj_section = "tasks"
html_classes = "reference task"
elif instance.content_type.model == "issue":
obj_section = "issues"
html_classes = "reference issue"
else:
return "#{}".format(obj_ref)
url = "/#/project/{}/{}/{}".format(
self.project.slug,
obj_section,
obj_ref
)
link_text = "&num;{}".format(obj_ref)
a = etree.Element('a')
a.text = link_text
a.set('href', url)
a.set('alt', subject)
a.set('title', subject)
a.set('class', html_classes)
return a
return ''
def makeExtension(configs=None):
return TaigaReferencesExtension(configs=configs)

View File

@ -1,23 +0,0 @@
# Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
# for details. All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.
from . import autolink
from . import automail
from . import hidden_hilite
from . import semi_sane_lists
from . import spaced_link
from . import strikethrough
from . import wikilinks
from . import emojify
from . import mentions
AutolinkExtension = autolink.AutolinkExtension
AutomailExtension = automail.AutomailExtension
HiddenHiliteExtension = hidden_hilite.HiddenHiliteExtension
SemiSaneListExtension = semi_sane_lists.SemiSaneListExtension
SpacedLinkExtension = spaced_link.SpacedLinkExtension
StrikethroughExtension = strikethrough.StrikethroughExtension
WikiLinkExtension = wikilinks.WikiLinkExtension
EmojifyExtension = emojify.EmojifyExtension
MentionsExtension = mentions.MentionsExtension

View File

@ -1,74 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012, lepture.com
# Copyright (c) 2014, taiga.io
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of the author nor the names of its contributors
# may be used to endorse or promote products derived from this
# software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import re
from taiga.projects.userstories.models import UserStory
from taiga.projects.issues.models import Issue
from taiga.projects.tasks.models import Task
def references(project, text):
pattern = re.compile('(?<=^|(?<=[^a-zA-Z0-9-]))#(us|issue|task)(\d+)')
def make_reference_link(m):
obj_type = m.group(1)
obj_ref = m.group(2)
if obj_type == "us":
model = UserStory
obj_section = "user-story"
elif obj_type == "issue":
model = Issue
obj_section = "issues"
elif obj_type == "task":
model = Task
obj_section = "tasks"
instances = model.objects.filter(project_id=project.id, ref=obj_ref)
if not instances:
return "#{type}{ref}".format(type=obj_type, ref=obj_ref)
subject = instances[0].subject
return '[#{type}{ref}](/#/project/{project_slug}/{section}/{ref} "{subject}")'.format(
type=obj_type,
section=obj_section,
ref=obj_ref,
project_slug=project.slug,
subject=subject
)
text = pattern.sub(make_reference_link, text)
return text
__all__ = ['references']

View File

@ -7,20 +7,19 @@ from django.utils.encoding import force_bytes
from markdown import markdown from markdown import markdown
from fn import F from fn import F
from .gfm import AutolinkExtension from .extensions.autolink import AutolinkExtension
from .gfm import AutomailExtension from .extensions.automail import AutomailExtension
from .gfm import HiddenHiliteExtension from .extensions.hidden_hilite import HiddenHiliteExtension
from .gfm import SemiSaneListExtension from .extensions.semi_sane_lists import SemiSaneListExtension
from .gfm import SpacedLinkExtension from .extensions.spaced_link import SpacedLinkExtension
from .gfm import StrikethroughExtension from .extensions.strikethrough import StrikethroughExtension
from .gfm import WikiLinkExtension from .extensions.wikilinks import WikiLinkExtension
from .gfm import EmojifyExtension from .extensions.emojify import EmojifyExtension
from .gfm import MentionsExtension from .extensions.mentions import MentionsExtension
from .extensions.references import TaigaReferencesExtension
from .processors.references import references
def _make_extensions_list(wikilinks_config=None): def _make_extensions_list(wikilinks_config=None, project=None):
return [AutolinkExtension(), return [AutolinkExtension(),
AutomailExtension(), AutomailExtension(),
SemiSaneListExtension(), SemiSaneListExtension(),
@ -29,6 +28,7 @@ def _make_extensions_list(wikilinks_config=None):
WikiLinkExtension(wikilinks_config), WikiLinkExtension(wikilinks_config),
EmojifyExtension(), EmojifyExtension(),
MentionsExtension(), MentionsExtension(),
TaigaReferencesExtension(project),
"extra", "extra",
"codehilite"] "codehilite"]
@ -54,22 +54,13 @@ def cache_by_sha(func):
return _decorator return _decorator
def _render_markdown(project, text):
wikilinks_config = {"base_url": "#/project/{}/wiki/".format(project.slug),
"end_url": ""}
extensions = _make_extensions_list(wikilinks_config=wikilinks_config)
return markdown(text, extensions=extensions)
def _preprocessors(project, text):
pre = F() >> F(references, project)
return pre(text)
#@cache_by_sha #@cache_by_sha
def render(project, text): def render(project, text):
renderer = F() >> F(_preprocessors, project) >> F(_render_markdown, project) wikilinks_config = {"base_url": "#/project/{}/wiki/".format(project.slug),
return renderer(text) "end_url": ""}
extensions = _make_extensions_list(wikilinks_config=wikilinks_config, project=project)
return markdown(text, extensions=extensions)
class DiffMatchPatch(diff_match_patch.diff_match_patch): class DiffMatchPatch(diff_match_patch.diff_match_patch):
def diff_pretty_html(self, diffs): def diff_pretty_html(self, diffs):

View File

@ -0,0 +1,9 @@
from .models import Reference
def get_instance_by_ref(project_id, obj_ref):
try:
instance = Reference.objects.get(project_id=project_id, ref=obj_ref)
except Reference.DoesNotExist:
instance = None
return instance

View File

@ -1,17 +1,17 @@
from unittest import mock from unittest.mock import patch, MagicMock
import pytest import pytest
import taiga.base import taiga.base
from taiga.mdrender.gfm import mentions from taiga.mdrender.extensions import mentions
from taiga.mdrender.gfm import emojify from taiga.mdrender.extensions import emojify
from taiga.mdrender.processors import references
from taiga.mdrender.service import render from taiga.mdrender.service import render
class DummyClass: from taiga.projects.references import services
pass
dummy_project = DummyClass() from taiga.users.models import User
dummy_project = MagicMock()
dummy_project.id = 1 dummy_project.id = 1
dummy_project.slug = "test" dummy_project.slug = "test"
@ -24,119 +24,52 @@ def test_proccessor_invalid_emoji():
assert result == ["**:notvalidemoji:**"] assert result == ["**:notvalidemoji:**"]
def test_proccessor_valid_user_mention(): def test_proccessor_valid_user_mention():
DummyModel = DummyClass() with patch("taiga.mdrender.extensions.mentions.User") as mock:
DummyModel.objects = DummyClass() instance = mock.objects.get.return_value
DummyModel.objects.filter = lambda username: ["test"] instance.get_full_name.return_value = "test name"
result = render(dummy_project, "**@user1**")
mentions.User = DummyModel expected_result = "<p><strong><a alt=\"test name\" class=\"mention\" href=\"/#/profile/user1\" title=\"test name\">&commat;user1</a></strong></p>"
assert result == expected_result
result = mentions.MentionsPreprocessor().run(["**@user1**"])
assert result == ["**[@user1](/#/profile/user1 \"@user1\")**"]
def test_proccessor_invalid_user_mention(): def test_proccessor_invalid_user_mention():
DummyModel = DummyClass() with patch("taiga.mdrender.extensions.mentions.User") as mock:
DummyModel.objects = DummyClass() mock.DoesNotExist = User.DoesNotExist
DummyModel.objects.filter = lambda username: [] mock.objects.get.side_effect = User.DoesNotExist
result = render(dummy_project, "**@notvaliduser**")
mentions.User = DummyModel assert result == '<p><strong>@notvaliduser</strong></p>'
result = mentions.MentionsPreprocessor().run(["**@notvaliduser**"])
assert result == ['**@notvaliduser**']
def test_proccessor_valid_us_reference(): def test_proccessor_valid_us_reference():
class MockModelWithInstance: with patch("taiga.mdrender.extensions.references.get_instance_by_ref") as mock:
class objects: instance = mock.return_value
def filter(*args, **kwargs): instance.content_type.model = "userstory"
dummy_instance = DummyClass() instance.content_object.subject = "test"
dummy_instance.subject = "test-subject" result = render(dummy_project, "**#1**")
return [dummy_instance] expected_result = '<p><strong><a alt="test" class="reference user-story" href="/#/project/test/user-story/1" title="test">&num;1</a></strong></p>'
UserStoryBack = references.UserStory assert result == expected_result
references.UserStory = MockModelWithInstance
result = references.references(dummy_project, "**#us1**")
assert result == '**[#us1](/#/project/test/user-story/1 "test-subject")**'
references.UserStory = UserStoryBack
def test_proccessor_invalid_us_reference():
class MockModelEmpty:
class objects:
def filter(*args, **kwargs):
return []
UserStoryBack = references.UserStory
references.UserStory = MockModelEmpty
result = references.references(dummy_project, "**#us1**")
assert result == "**#us1**"
references.UserStory = UserStoryBack
def test_proccessor_valid_issue_reference(): def test_proccessor_valid_issue_reference():
class MockModelWithInstance: with patch("taiga.mdrender.extensions.references.get_instance_by_ref") as mock:
class objects: instance = mock.return_value
def filter(*args, **kwargs): instance.content_type.model = "issue"
dummy_instance = DummyClass() instance.content_object.subject = "test"
dummy_instance.subject = "test-subject" result = render(dummy_project, "**#1**")
return [dummy_instance] expected_result = '<p><strong><a alt="test" class="reference issue" href="/#/project/test/issues/1" title="test">&num;1</a></strong></p>'
IssueBack = references.Issue assert result == expected_result
references.Issue = MockModelWithInstance
result = references.references(dummy_project, "**#issue1**")
assert result == '**[#issue1](/#/project/test/issues/1 "test-subject")**'
references.Issue = IssueBack
def test_proccessor_invalid_issue_reference():
class MockModelEmpty:
class objects:
def filter(*args, **kwargs):
return []
IssueBack = references.Issue
references.Issue = MockModelEmpty
result = references.references(dummy_project, "**#issue1**")
assert result == "**#issue1**"
references.Issue = IssueBack
def test_proccessor_valid_task_reference(): def test_proccessor_valid_task_reference():
class MockModelWithInstance: with patch("taiga.mdrender.extensions.references.get_instance_by_ref") as mock:
class objects: instance = mock.return_value
def filter(*args, **kwargs): instance.content_type.model = "task"
dummy_instance = DummyClass() instance.content_object.subject = "test"
dummy_instance.subject = "test-subject" result = render(dummy_project, "**#1**")
return [dummy_instance] expected_result = '<p><strong><a alt="test" class="reference task" href="/#/project/test/tasks/1" title="test">&num;1</a></strong></p>'
TaskBack = references.Task assert result == expected_result
references.Task = MockModelWithInstance
result = references.references(dummy_project, "**#task1**") def test_proccessor_invalid_reference():
assert result == '**[#task1](/#/project/test/tasks/1 "test-subject")**' with patch("taiga.mdrender.extensions.references.get_instance_by_ref") as mock:
mock.return_value = None
references.Task = TaskBack result = render(dummy_project, "**#1**")
assert result == "<p><strong>#1</strong></p>"
def test_proccessor_invalid_task_reference():
class MockModelEmpty:
class objects:
def filter(*args, **kwargs):
return []
TaskBack = references.Task
references.Task = MockModelEmpty
result = references.references(dummy_project, "**#task1**")
assert result == "**#task1**"
references.Task = TaskBack
def test_proccessor_invalid_type_reference():
result = references.references(dummy_project, "**#invalid1**")
assert result == "**#invalid1**"
def test_render_wiki_strong(): def test_render_wiki_strong():
assert render(dummy_project, "**test**") == "<p><strong>test</strong></p>" assert render(dummy_project, "**test**") == "<p><strong>test</strong></p>"