mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-01-17 05:22:16 +00:00
Markdown (#214)
* Add basic markdown support site wide
* Improved MD support.
Add some styling for images in MD
Add support for the bastardisation of the MD html for RML.
* Add processing for <ul> in RML
* Add OL processing to RML
* Fix a bug with squares appearing around the last page number
* Remove rml formatting in event_detail
* Improve handling of code blocks in RML
* Add MD to rigboard
Reduce MD title sizes as they were offensively large
* Add parsing of markdown when editing event items
* Improved list handling in RML
* Add tests for markdown support.
Focuses mainly on RML as that's where it will break
* Add indications of where MD support is enabled as per comment by @samozzy in #178.
Isn't quite a full description, but for the most part this should be enough for the people who know how to use it see where they can use it.
* Add failing test for markdown processing none
* Fix for failing test in e0d56e
* Add failing test for using single line breaks as per comment on #214
* Enable line break extension for single breaks in paragraphs by new lines.
Pass tests in ef3de607c3
* Enable GH flavour linebreaks in JS rendered markdown
* Made RML bullets pretty :)
* Added WYSIWYG editor. Works for notes & description, fails miserably for items :(
* Fixed for event items. Will probably fail tests because selenium can't type in simpleMDE :(
* FIX: Re-enable markdown on paperwork
Strikethrough is broken in all sorts of places for whatever reason
* FEAT: Markdown support on asset comments
* FIX: Prevent js injection through markdown fields
* Initial fixes
* Basic dark theme for simplemde
* Swap to locally delivered SimpleMDE
* Region for selenium testing of SimpleMDE
Bleh, Javascript all around
* Tests passing!
Fixed not using region for item modal, and overflow error on paperwork with really long description. Looks junk but I'm not really bothered
* Pep8 fixes
* Fallback for null HCapatcha sitekey
I.e. when we're on a branch
* Fix item description print being broken
* Actually fix sitekey problem
* Fixes for using markdown in asset comments
* Properly initialise markdown on asset comments
Co-authored-by: David Taylor <david@taylorhq.com>
Co-authored-by: FreneticScribbler <aj@aronajones.com>
This commit is contained in:
6
RIGS/static/js/marked.min.js
vendored
Normal file
6
RIGS/static/js/marked.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,5 +1,7 @@
|
||||
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||
|
||||
{% load markdown_tags %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row my-3 py-3">
|
||||
{% if not request.is_ajax %}
|
||||
@@ -43,7 +45,7 @@
|
||||
{% if perms.RIGS.view_event %}
|
||||
<h4>Notes</h4>
|
||||
<hr>
|
||||
<p class="dont-break-out">{{ event.notes|linebreaksbr }}</p>
|
||||
<p class="dont-break-out">{{ event.notes|markdown }}</p>
|
||||
{% endif %}
|
||||
<br>
|
||||
{% include 'partials/item_table.html' %}
|
||||
|
||||
@@ -8,11 +8,13 @@
|
||||
{% block css %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'css/selects.css' %}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'css/simplemde.min.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block preload_js %}
|
||||
{{ block.super }}
|
||||
<script src="{% static 'js/selects.js' %}"></script>
|
||||
<script src="{% static 'js/simplemde.min.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
@@ -63,6 +65,16 @@
|
||||
{% endif %}
|
||||
});
|
||||
$(document).ready(function () {
|
||||
setupMDE('#id_description');
|
||||
setupMDE('#id_notes');
|
||||
setupMDE('#item_description');
|
||||
|
||||
$('#itemModal').on('shown.bs.modal', function (e) {
|
||||
$('#item_description').data('mde_editor').value(
|
||||
$('#item_description').val()
|
||||
);
|
||||
});
|
||||
|
||||
setupItemTable($("#{{ form.items_json.id_for_label }}").val());
|
||||
});
|
||||
$(function () {
|
||||
@@ -168,7 +180,7 @@
|
||||
<label for="{{ form.description.id_for_label }}"
|
||||
class="col-sm-4 col-form-label">{{ form.description.label }}</label>
|
||||
|
||||
<div class="col-sm-8">
|
||||
<div class="col-sm-12">
|
||||
{% render_field form.description class+="form-control" %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -345,7 +357,7 @@
|
||||
<div class="col-sm-12">
|
||||
<div class="form-group" data-toggle="tooltip" title="Notes on the event. This is only visible to keyholders, and is not displayed on the paperwork">
|
||||
<label for="{{ form.notes.id_for_label }}">{{ form.notes.label }}</label>
|
||||
{% render_field form.notes class+="form-control" %}
|
||||
{% render_field form.notes class+="form-control md-enabled" %}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'partials/item_table.html' %}
|
||||
|
||||
@@ -74,6 +74,14 @@
|
||||
<lineStyle kind="linebelow" start="3,0" stop="3,0" colorName="black"/>
|
||||
<lineStyle kind="linebelow" start="5,0" stop="5,0" colorName="black"/>
|
||||
</blockTableStyle>
|
||||
|
||||
<listStyle name="ol"
|
||||
bulletFormat="%s."
|
||||
bulletFontSize="10" />
|
||||
|
||||
<listStyle name="ul"
|
||||
start="bulletchar"
|
||||
bulletFontSize="10"/>
|
||||
</stylesheet>
|
||||
|
||||
<template > {# Note: page is 595x842 points (1 point=1/72in) #}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{% load markdown_tags %}
|
||||
{% load filters %}
|
||||
|
||||
<setNextFrame name="main"/>
|
||||
<nextFrame/>
|
||||
<blockTable style="headLayout" colWidths="330,165">
|
||||
@@ -10,10 +12,8 @@
|
||||
<b>{{object.start_date|date:"D jS N Y"}}</b>
|
||||
</para>
|
||||
|
||||
<keepInFrame>
|
||||
<para style="style.event_description">
|
||||
{{ object.description|default_if_none:""|linebreaksxml }}
|
||||
</para>
|
||||
<keepInFrame maxHeight="500" onOverflow="shrink">
|
||||
{{ object.description|default_if_none:""|markdown:"rml" }}
|
||||
</keepInFrame>
|
||||
</td>
|
||||
<td>
|
||||
@@ -184,7 +184,7 @@
|
||||
{% if item.description %}
|
||||
</para>
|
||||
<para style="item_description">
|
||||
<em>{{ item.description|linebreaksxml }}</em>
|
||||
{{ item.description|markdown:"rml" }}
|
||||
</para>
|
||||
<para>
|
||||
{% endif %}
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
id="item_name"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-row">
|
||||
<div class="form-group form-row" data-toggle="tooltip" title="A detailed description of the kit. MD enabled.">
|
||||
<label for="item_description" class="col-sm-2 col-form-label">Description</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea type="text" placeholder="Description" class="form-control"
|
||||
<textarea type="text" placeholder="Description" class="form-control md-enabled"
|
||||
id="item_description" rows="8"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% load namewithnotes from filters %}
|
||||
{% load markdown_tags %}
|
||||
<div class="card card-info">
|
||||
<div class="card-header">Event Info</div>
|
||||
<div class="card-body">
|
||||
@@ -46,7 +47,7 @@
|
||||
<dd class="col-sm-12"> </dd>
|
||||
|
||||
<dt class="col-sm-6">Event Description</dt>
|
||||
<dd class="dont-break-out col-sm-12">{{ event.description|linebreaksbr }}</dd>
|
||||
<dd class="dont-break-out col-sm-12">{{ event.description|markdown }}</dd>
|
||||
|
||||
<dd class="col-sm-12"> </dd>
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{% load markdown_tags %}
|
||||
<tr id="item-{{item.pk}}" data-pk="{{item.pk}}" class="item_row">
|
||||
<th scope="row">
|
||||
<span class="name">{{ item.name }}</span>
|
||||
<div class="item-description">
|
||||
<em class="description">{{item.description|linebreaksbr}}</em>
|
||||
<em class="description">{{item.description|markdown}}</em>
|
||||
</div>
|
||||
</th>
|
||||
{% if perms.RIGS.view_event %}
|
||||
|
||||
56
RIGS/templatetags/markdown_tags.py
Normal file
56
RIGS/templatetags/markdown_tags.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
import markdown
|
||||
|
||||
__author__ = 'ghost'
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter(name="markdown")
|
||||
def markdown_filter(text, input_format='html'):
|
||||
# markdown library can't handle text=None
|
||||
if text is None:
|
||||
return text
|
||||
html = markdown.markdown(text, extensions=['markdown.extensions.nl2br'])
|
||||
# Convert format to RML
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
# Prevent code injection
|
||||
for script in soup('script'):
|
||||
script.string = "Your script shall not pass!"
|
||||
if input_format == 'html':
|
||||
return mark_safe('<div class="markdown">' + str(soup) + '</div>')
|
||||
elif input_format == 'rml':
|
||||
|
||||
# Image aren't supported so remove them
|
||||
for img in soup('img'):
|
||||
img.parent.extract()
|
||||
|
||||
# <code> should become <font>
|
||||
for c in soup('code'):
|
||||
c.name = 'font'
|
||||
c['face'] = "Courier"
|
||||
|
||||
# blockquotes don't exist but we can still do something to show
|
||||
for bq in soup('blockquote'):
|
||||
bq.name = 'pre'
|
||||
bq.string = bq.text
|
||||
|
||||
for alist in soup.find_all(['ul', 'ol']):
|
||||
alist['style'] = alist.name
|
||||
for li in alist.find_all('li', recursive=False):
|
||||
text = li.find(text=True)
|
||||
text.wrap(soup.new_tag('p'))
|
||||
|
||||
if alist.parent.name != 'li':
|
||||
indent = soup.new_tag('indent')
|
||||
indent['left'] = '0.6cm'
|
||||
|
||||
alist.wrap(indent)
|
||||
|
||||
# Paragraphs have a different tag
|
||||
for p in soup('p'):
|
||||
p.name = 'para'
|
||||
|
||||
return mark_safe(str(soup))
|
||||
@@ -96,7 +96,7 @@ class CreateEvent(FormPage):
|
||||
_warning_selector = (By.XPATH, '/html/body/div[1]/div[1]')
|
||||
|
||||
form_items = {
|
||||
'description': (regions.TextBox, (By.ID, 'id_description')),
|
||||
'description': (regions.SimpleMDETextArea, (By.ID, 'id_description')),
|
||||
|
||||
'name': (regions.TextBox, (By.ID, 'id_name')),
|
||||
'start_date': (regions.DatePicker, (By.ID, 'id_start_date')),
|
||||
@@ -110,7 +110,7 @@ class CreateEvent(FormPage):
|
||||
'collected_by': (regions.TextBox, (By.ID, 'id_collector')),
|
||||
'po': (regions.TextBox, (By.ID, 'id_purchase_order')),
|
||||
|
||||
'notes': (regions.TextBox, (By.ID, 'id_notes'))
|
||||
'notes': (regions.SimpleMDETextArea, (By.ID, 'id_notes'))
|
||||
}
|
||||
|
||||
def select_event_type(self, type_name):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from pypom import Region
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from PyRIGS.tests.regions import TextBox, Modal
|
||||
from PyRIGS.tests.regions import TextBox, Modal, SimpleMDETextArea
|
||||
|
||||
|
||||
class Header(Region):
|
||||
@@ -42,7 +42,7 @@ class ItemModal(Modal):
|
||||
|
||||
form_items = {
|
||||
'name': (TextBox, (By.ID, 'item_name')),
|
||||
'description': (TextBox, (By.ID, 'item_description')),
|
||||
'description': (SimpleMDETextArea, (By.ID, 'item_description')),
|
||||
'quantity': (TextBox, (By.ID, 'item_quantity')),
|
||||
'price': (TextBox, (By.ID, 'item_cost'))
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ from datetime import date
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.utils.safestring import SafeText
|
||||
from RIGS.templatetags.markdown_tags import markdown_filter
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import timezone
|
||||
from pytest_django.asserts import assertRedirects, assertNotContains, assertContains
|
||||
@@ -170,6 +172,7 @@ class TestInvoiceDelete(TestCase):
|
||||
def setUpTestData(cls):
|
||||
cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True,
|
||||
is_active=True, is_staff=True)
|
||||
cls.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1')
|
||||
cls.events = {
|
||||
1: models.Event.objects.create(name="TE E1", start_date=date.today()),
|
||||
2: models.Event.objects.create(name="TE E2", start_date=date.today())
|
||||
@@ -363,6 +366,215 @@ def test_checklist_review(admin_client, admin_user, checklist):
|
||||
def test_ra_redirect(admin_client, admin_user, ra):
|
||||
request_url = reverse('event_ra', kwargs={'pk': ra.event.pk})
|
||||
expected_url = reverse('ra_edit', kwargs={'pk': ra.pk})
|
||||
|
||||
response = admin_client.get(request_url, follow=True)
|
||||
assertRedirects(response, expected_url, status_code=302, target_status_code=200)
|
||||
|
||||
|
||||
class TestMarkdownTemplateTags(TestCase):
|
||||
markdown = """
|
||||
An h1 header
|
||||
============
|
||||
|
||||
Paragraphs are separated by a blank line.
|
||||
|
||||
2nd paragraph. *Italic*, **bold**, and `monospace`. Itemized lists
|
||||
look like:
|
||||
|
||||
* this one
|
||||
* that one
|
||||
* the other one
|
||||
|
||||
Note that --- not considering the asterisk --- the actual text
|
||||
content starts at 4-columns in.
|
||||
|
||||
> Block quotes are
|
||||
> written like so.
|
||||
>
|
||||
> They can span multiple paragraphs,
|
||||
> if you like.
|
||||
|
||||
Use 3 dashes for an em-dash. Use 2 dashes for ranges (ex., "it's all
|
||||
in chapters 12--14"). Three dots ... will be converted to an ellipsis.
|
||||
Unicode is supported.
|
||||
|
||||
|
||||
|
||||
An h2 header
|
||||
------------
|
||||
|
||||
Here's a numbered list:
|
||||
|
||||
1. first item
|
||||
2. second item
|
||||
3. third item
|
||||
|
||||
Note again how the actual text starts at 4 columns in (4 characters
|
||||
from the left side). Here's a code sample:
|
||||
|
||||
# Let me re-iterate ...
|
||||
for i in 1 .. 10 { do-something(i) }
|
||||
|
||||
As you probably guessed, indented 4 spaces. By the way, instead of
|
||||
indenting the block, you can use delimited blocks, if you like:
|
||||
|
||||
~~~
|
||||
define foobar() {
|
||||
print "Welcome to flavor country!";
|
||||
}
|
||||
~~~
|
||||
|
||||
(which makes copying & pasting easier). You can optionally mark the
|
||||
delimited block for Pandoc to syntax highlight it:
|
||||
|
||||
~~~python
|
||||
import time
|
||||
# Quick, count to ten!
|
||||
for i in range(10):
|
||||
# (but not *too* quick)
|
||||
time.sleep(0.5)
|
||||
print i
|
||||
~~~
|
||||
|
||||
|
||||
|
||||
### An h3 header ###
|
||||
|
||||
Now a nested list:
|
||||
|
||||
1. First, get these ingredients:
|
||||
|
||||
* carrots
|
||||
* celery
|
||||
* lentils
|
||||
|
||||
2. Boil some water.
|
||||
|
||||
3. Dump everything in the pot and follow
|
||||
this algorithm:
|
||||
|
||||
find wooden spoon
|
||||
uncover pot
|
||||
stir
|
||||
cover pot
|
||||
balance wooden spoon precariously on pot handle
|
||||
wait 10 minutes
|
||||
goto first step (or shut off burner when done)
|
||||
|
||||
Do not bump wooden spoon or it will fall.
|
||||
|
||||
Notice again how text always lines up on 4-space indents (including
|
||||
that last line which continues item 3 above).
|
||||
|
||||
Here's a link to [a website](http://foo.bar). Here's a footnote [^1].
|
||||
|
||||
[^1]: Footnote text goes here.
|
||||
|
||||
Tables can look like this:
|
||||
|
||||
size material color
|
||||
---- ------------ ------------
|
||||
9 leather brown
|
||||
10 hemp canvas natural
|
||||
11 glass transparent
|
||||
|
||||
Table: Shoes, their sizes, and what they're made of
|
||||
|
||||
(The above is the caption for the table.) Pandoc also supports
|
||||
multi-line tables:
|
||||
|
||||
-------- -----------------------
|
||||
keyword text
|
||||
-------- -----------------------
|
||||
red Sunsets, apples, and
|
||||
other red or reddish
|
||||
things.
|
||||
|
||||
green Leaves, grass, frogs
|
||||
and other things it's
|
||||
not easy being.
|
||||
-------- -----------------------
|
||||
|
||||
A horizontal rule follows.
|
||||
|
||||
***
|
||||
|
||||
Here's a definition list:
|
||||
|
||||
apples
|
||||
: Good for making applesauce.
|
||||
oranges
|
||||
: Citrus!
|
||||
tomatoes
|
||||
: There's no "e" in tomatoe.
|
||||
|
||||
Again, text is indented 4 spaces. (Put a blank line between each
|
||||
term/definition pair to spread things out more.)
|
||||
|
||||
Here's a "line block":
|
||||
|
||||
| Line one
|
||||
| Line too
|
||||
| Line tree
|
||||
|
||||
and images can be specified like so:
|
||||
|
||||

|
||||
|
||||
Inline math equations go in like so: $\\omega = d\\phi / dt$. Display
|
||||
math should get its own line and be put in in double-dollarsigns:
|
||||
|
||||
$$I = \\int \rho R^{2} dV$$
|
||||
|
||||
And note that you can backslash-escape any punctuation characters
|
||||
which you wish to be displayed literally, ex.: \\`foo\\`, \\*bar\\*, etc.
|
||||
"""
|
||||
|
||||
def test_html_safe(self):
|
||||
html = markdown_filter(self.markdown)
|
||||
self.assertIsInstance(html, SafeText)
|
||||
|
||||
def test_img_strip(self):
|
||||
rml = markdown_filter(self.markdown, 'rml')
|
||||
self.assertNotIn("<img", rml)
|
||||
|
||||
def test_code(self):
|
||||
rml = markdown_filter(self.markdown, 'rml')
|
||||
self.assertIn('<font face="Courier">monospace</font>', rml)
|
||||
|
||||
def test_blockquote(self):
|
||||
rml = markdown_filter(self.markdown, 'rml')
|
||||
self.assertIn("<pre>\nBlock quotes", rml)
|
||||
|
||||
def test_lists(self):
|
||||
rml = markdown_filter(self.markdown, 'rml')
|
||||
self.assertIn("<li><para>second item</para></li>", rml) # <ol>
|
||||
self.assertIn("<li><para>that one</para></li>", rml) # <ul>
|
||||
|
||||
def test_in_print(self):
|
||||
event = models.Event.objects.create(
|
||||
name="MD Print Test",
|
||||
description=self.markdown,
|
||||
start_date='2016-01-01',
|
||||
)
|
||||
user = models.Profile.objects.create(
|
||||
username='RML test',
|
||||
is_superuser=True, # Don't care about permissions
|
||||
is_active=True,
|
||||
)
|
||||
user.set_password('rmltester')
|
||||
user.save()
|
||||
|
||||
self.assertTrue(self.client.login(username=user.username, password='rmltester'))
|
||||
response = self.client.get(reverse('event_print', kwargs={'pk': event.pk}))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# By the time we have a PDF it should be larger than the original by some margin
|
||||
# RML hard fails if something doesn't work
|
||||
self.assertGreater(len(response.content), len(self.markdown))
|
||||
|
||||
def test_nonetype(self):
|
||||
html = markdown_filter(None)
|
||||
self.assertIsNone(html)
|
||||
|
||||
def test_linebreaks(self):
|
||||
html = markdown_filter(self.markdown)
|
||||
self.assertIn("Itemized lists<br/>\nlook like", html)
|
||||
|
||||
Reference in New Issue
Block a user