• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zestedesavoir / zds-site / 5706108042

pending completion
5706108042

Pull #6522

github

web-flow
Merge e4d19c58d into 201717663
Pull Request #6522: Déplace get_authorized_forums() vers zds/forum/utils.py

4669 of 5847 branches covered (79.85%)

11 of 12 new or added lines in 2 files covered. (91.67%)

3 existing lines in 2 files now uncovered.

15555 of 17597 relevant lines covered (88.4%)

1.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.27
/zds/utils/templatetags/emarkdown.py
1
import re
3✔
2
import json
3✔
3
import logging
3✔
4
from requests import post, HTTPError
3✔
5

6
from django import template
3✔
7
from django.conf import settings
3✔
8
from django.template.defaultfilters import stringfilter
3✔
9
from django.utils.safestring import mark_safe
3✔
10
from django.utils.translation import gettext_lazy as _
3✔
11

12
logger = logging.getLogger(__name__)
3✔
13
register = template.Library()
3✔
14
"""
15
Markdown related filters.
16
"""
17

18
# Constants
19
MAX_ATTEMPTS = 3
3✔
20
MD_PARSING_ERROR = _("Une erreur est survenue dans la génération de texte Markdown. Veuillez rapporter le bug.")
3✔
21

22
FORMAT_ENDPOINTS = {
3✔
23
    "html": "/html",
24
    "texfile": "/latex-document",
25
    "epub": "/epub",
26
    "tex": "/latex",
27
}
28

29

30
def _render_markdown_once(md_input, *, output_format="html", **kwargs):
3✔
31
    """
32
    Returns None on error (error details are logged). No retry mechanism.
33
    """
34

35
    def log_args():
3✔
36
        logger.error(f"md_input: {md_input!r}")
×
37
        logger.error(f"kwargs: {kwargs!r}")
×
38

39
    inline = kwargs.get("inline", False) is True
3✔
40
    full_json = kwargs.pop("full_json", False)
3✔
41

42
    if settings.ZDS_APP["zmd"]["disable_pings"] is True:
3!
43
        kwargs["disable_ping"] = True
×
44

45
    endpoint = FORMAT_ENDPOINTS[output_format]
3✔
46

47
    try:
3✔
48
        timeout = 10
3✔
49
        real_input = str(md_input)
3✔
50
        if output_format.startswith("tex") or full_json:
3✔
51
            # latex may be really long to generate but it is also restrained by server configuration
52
            timeout = 120
3✔
53
            # use manifest renderer
54
            real_input = md_input
3✔
55
        response = post(
3✔
56
            "{}{}".format(settings.ZDS_APP["zmd"]["server"], endpoint),
57
            json={
58
                "opts": kwargs,
59
                "md": real_input,
60
            },
61
            timeout=timeout,
62
        )
UNCOV
63
    except HTTPError:
×
64
        logger.exception("An HTTP error happened, markdown rendering failed")
×
65
        log_args()
×
66
        return "", {}, []
×
67

68
    if response.status_code == 413:
3!
69
        return "", {}, [{"message": str(_("Texte trop volumineux."))}]
×
70

71
    if response.status_code != 200:
3!
72
        logger.error(f"The markdown server replied with status {response.status_code} (expected 200)")
×
73
        log_args()
×
74
        return "", {}, []
×
75

76
    try:
3✔
77
        content, metadata, messages = response.json()
3✔
78
        logger.debug("Result %s, %s, %s", content, metadata, messages)
3✔
79
        if messages:
3✔
80
            logger.error("Markdown errors %s", json.dumps(messages))
2✔
81
        if isinstance(content, str):
3✔
82
            content = content.strip()
3✔
83
        if inline:
3✔
84
            content = content.replace("</p>\n", "\n\n").replace("\n<p>", "\n")
3✔
85
        if full_json:
3✔
86
            return content, metadata, messages
3✔
87
        return mark_safe(content), metadata, messages
3✔
88
    except:  # noqa
×
89
        logger.exception("Unexpected exception raised")
×
90
        log_args()
×
91
        return "", {}, []
×
92

93

94
def render_markdown(md_input, *, on_error=None, disable_jsfiddle=True, **kwargs):
3✔
95
    """Render a markdown string.
96

97
    Returns a tuple ``(rendered_content, metadata)``, where
98
    ``rendered_content`` is a string and ``metadata`` is a dict.
99

100
    Handles errors gracefully by returning an user-friendly HTML
101
    string which explains that the Markdown rendering has failed
102
    (without any technical details).
103

104
    """
105
    opts = {"disable_jsfiddle": disable_jsfiddle}
3✔
106
    opts.update(kwargs)
3✔
107
    content, metadata, messages = _render_markdown_once(md_input, **opts)
3✔
108
    if messages and on_error:
3✔
109
        on_error([m["message"] for m in messages])
2✔
110
    if content is not None:
3!
111
        # Success!
112
        return content, metadata, messages
3✔
113

114
    # Oops, something went wrong
115

116
    attempts = kwargs.get("attempts", 0)
×
117
    inline = kwargs.get("inline", False) is True
×
118

119
    if attempts < MAX_ATTEMPTS:
×
120
        if not kwargs:
×
121
            kwargs = dict()
×
122
        return render_markdown(md_input, **dict(kwargs, attempts=attempts + 1))
×
123

124
    logger.error("Max attempt count reached, giving up")
×
125
    logger.error(f"md_input: {md_input!r}")
×
126
    logger.error(f"kwargs: {kwargs!r}")
×
127

128
    # FIXME: This cannot work with LaTeX.
129
    if inline:
×
130
        return mark_safe(f"<p>{json.dumps(messages)}</p>"), metadata, []
×
131
    else:
132
        return mark_safe(f'<div class="error ico-after"><p>{json.dumps(messages)}</p></div>'), metadata, []
×
133

134

135
def render_markdown_stats(md_input, **kwargs):
3✔
136
    """
137
    Returns contents statistics (words and chars)
138
    """
139
    kwargs["stats"] = True
1✔
140
    kwargs["disable_images_download"] = True
1✔
141
    logger.setLevel(logging.INFO)
1✔
142
    content, metadata, messages = _render_markdown_once(md_input, output_format="tex", **kwargs)
1✔
143
    if metadata:
1!
144
        return metadata.get("stats", {}).get("signs", {})
1✔
145
    return None
×
146

147

148
@register.filter(name="epub_markdown", needs_autoescape=False)
3✔
149
def epub_markdown(md_input, image_directory):
150
    media_root = str(settings.MEDIA_ROOT)
3✔
151
    if not media_root.endswith("/"):
3!
152
        media_root += "/"
3✔
153
    replaced_media_url = settings.MEDIA_URL
3✔
154
    if replaced_media_url.startswith("/"):
3!
155
        replaced_media_url = replaced_media_url[1:]
3✔
156
    return mark_safe(
3✔
157
        emarkdown(
158
            md_input,
159
            output_format="epub",
160
            images_download_dir=image_directory.absolute,
161
        )
162
        .replace('src"/', f'src="{media_root}')
163
        .replace(f'src="{media_root}{replaced_media_url}', f'src="{media_root}')
164
    )
165

166

167
@register.filter(needs_autoescape=False)
3✔
168
@stringfilter
3✔
169
def emarkdown(md_input, use_jsfiddle="", **kwargs):
3✔
170
    """
171
    :param str md_input: Markdown string.
172
    :return: HTML string.
173
    :rtype: str
174
    """
175
    disable_jsfiddle = use_jsfiddle != "js"
3✔
176
    content, metadata, messages = render_markdown(
3✔
177
        md_input,
178
        on_error=lambda m: logger.error("Markdown errors %s", str(m)),
179
        **dict(kwargs, disable_jsfiddle=disable_jsfiddle),
180
    )
181
    kwargs.get("metadata", {}).update(metadata)
3✔
182
    return content or ""
3✔
183

184

185
@register.filter(needs_autoescape=False)
3✔
186
@stringfilter
3✔
187
def emarkdown_preview(md_input, use_jsfiddle="", **kwargs):
3✔
188
    """
189
    Filter markdown string and render it to html.
190

191
    :param str md_input: Markdown string.
192
    :return: HTML string.
193
    :rtype: str
194
    """
195
    disable_jsfiddle = use_jsfiddle != "js"
3✔
196

197
    content, metadata, messages = render_markdown(md_input, **dict(kwargs, disable_jsfiddle=disable_jsfiddle))
3✔
198

199
    if messages:
3!
200
        content = _(
×
201
            '</div><div class="preview-error"><strong>Erreur du serveur Markdown:</strong>\n{}'.format(
202
                "<br>- ".join([m["message"] for m in messages])
203
            )
204
        )
205
        content = mark_safe(content)
×
206

207
    return content
3✔
208

209

210
@register.filter(needs_autoescape=False)
3✔
211
@stringfilter
3✔
212
def emarkdown_inline(text):
213
    """
214
    Parses inline elements only and renders HTML. Mainly for member signatures.
215
    Although they are inline elements, pings are disabled.
216

217
    :param str text: Markdown string.
218
    :return: HTML string.
219
    :rtype: str
220
    """
221
    rendered = emarkdown(text, inline=True)
3✔
222
    return mark_safe(rendered.replace("<a href=", '<a rel="nofollow" href='))
3✔
223

224

225
def sub_hd(match, count):
3✔
226
    """Replace header shifted."""
227
    subt = match.group(1)
2✔
228
    lvl = match.group("level")
2✔
229
    header = match.group("header")
2✔
230
    end = match.group(4)
2✔
231

232
    new_content = subt + "#" * count + lvl + header + end
2✔
233

234
    return new_content
2✔
235

236

237
def shift_heading(text, count):
3✔
238
    """
239
    Shift header in markdown document.
240

241
    :param str text: Text to filter.
242
    :param int count:
243
    :return: Filtered text.
244
    :rtype: str
245
    """
246
    text_by_code = re.split("(```|~~~)", text)
3✔
247
    starting_code = None
3✔
248
    for i, element in enumerate(text_by_code):
3✔
249
        if element in ["```", "~~~"] and not starting_code:
3✔
250
            starting_code = element
1✔
251
        elif element == starting_code:
3✔
252
            starting_code = None
1✔
253
        elif starting_code is None:
3✔
254
            text_by_code[i] = re.sub(
3✔
255
                r"(^|\n)(?P<level>#{1,4})(?P<header>.*?)#*(\n|$)", lambda t: sub_hd(t, count), text_by_code[i]
256
            )
257

258
    return "".join(text_by_code)
3✔
259

260

261
@register.filter("shift_heading_1")
3✔
262
def shift_heading_1(text):
263
    return shift_heading(text, 1)
3✔
264

265

266
@register.filter("shift_heading_2")
3✔
267
def shift_heading_2(text):
268
    return shift_heading(text, 2)
3✔
269

270

271
@register.filter("shift_heading_3")
3✔
272
def shift_heading_3(text):
273
    return shift_heading(text, 3)
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc