mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-02-10 16:49:41 +00:00
Merge branch 'master' into training
This commit is contained in:
@@ -188,12 +188,8 @@ LOGOUT_URL = '/user/logout/'
|
|||||||
ACCOUNT_ACTIVATION_DAYS = 7
|
ACCOUNT_ACTIVATION_DAYS = 7
|
||||||
|
|
||||||
# CAPTCHA settings
|
# CAPTCHA settings
|
||||||
if DEBUG or CI:
|
HCAPTCHA_SITEKEY = env('HCAPTCHA_SITEKEY', '10000000-ffff-ffff-ffff-000000000001')
|
||||||
HCAPTCHA_SITEKEY = '10000000-ffff-ffff-ffff-000000000001'
|
HCAPTCHA_SECRET = env('HCAPTCHA_SECRET', '0x0000000000000000000000000000000000000000')
|
||||||
HCAPTCHA_SECRET = '0x0000000000000000000000000000000000000000'
|
|
||||||
else:
|
|
||||||
HCAPTCHA_SITEKEY = env('HCAPTCHA_SITEKEY')
|
|
||||||
HCAPTCHA_SECRET = env('HCAPTCHA_SECRET')
|
|
||||||
|
|
||||||
# Email
|
# Email
|
||||||
EMAILER_TEST = False
|
EMAILER_TEST = False
|
||||||
|
|||||||
@@ -117,6 +117,15 @@ class TextBox(Region):
|
|||||||
self.root.send_keys(value)
|
self.root.send_keys(value)
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleMDETextArea(Region):
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
return self.driver.execute_script("return document.querySelector('#' + arguments[0]).nextSibling.nextSibling.CodeMirror.getDoc().getValue();", self.root.get_attribute("id"))
|
||||||
|
|
||||||
|
def set_value(self, value):
|
||||||
|
self.driver.execute_script("document.querySelector('#' + arguments[0]).nextSibling.nextSibling.CodeMirror.getDoc().setValue(arguments[1]);", self.root.get_attribute("id"), value)
|
||||||
|
|
||||||
|
|
||||||
class CheckBox(Region):
|
class CheckBox(Region):
|
||||||
def toggle(self):
|
def toggle(self):
|
||||||
self.root.click()
|
self.root.click()
|
||||||
|
|||||||
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" %}
|
{% extends request.is_ajax|yesno:"base_ajax.html,base_rigs.html" %}
|
||||||
|
|
||||||
|
{% load markdown_tags %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row my-3 py-3">
|
<div class="row my-3 py-3">
|
||||||
{% if not request.is_ajax %}
|
{% if not request.is_ajax %}
|
||||||
@@ -43,7 +45,7 @@
|
|||||||
{% if perms.RIGS.view_event %}
|
{% if perms.RIGS.view_event %}
|
||||||
<h4>Notes</h4>
|
<h4>Notes</h4>
|
||||||
<hr>
|
<hr>
|
||||||
<p class="dont-break-out">{{ event.notes|linebreaksbr }}</p>
|
<p class="dont-break-out">{{ event.notes|markdown }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<br>
|
<br>
|
||||||
{% include 'partials/item_table.html' %}
|
{% include 'partials/item_table.html' %}
|
||||||
|
|||||||
@@ -8,11 +8,13 @@
|
|||||||
{% block css %}
|
{% block css %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'css/selects.css' %}"/>
|
<link rel="stylesheet" type="text/css" href="{% static 'css/selects.css' %}"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static 'css/simplemde.min.css' %}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block preload_js %}
|
{% block preload_js %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<script src="{% static 'js/selects.js' %}"></script>
|
<script src="{% static 'js/selects.js' %}"></script>
|
||||||
|
<script src="{% static 'js/simplemde.min.js' %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
@@ -63,6 +65,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
});
|
});
|
||||||
$(document).ready(function () {
|
$(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());
|
setupItemTable($("#{{ form.items_json.id_for_label }}").val());
|
||||||
});
|
});
|
||||||
$(function () {
|
$(function () {
|
||||||
@@ -168,7 +180,7 @@
|
|||||||
<label for="{{ form.description.id_for_label }}"
|
<label for="{{ form.description.id_for_label }}"
|
||||||
class="col-sm-4 col-form-label">{{ form.description.label }}</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" %}
|
{% render_field form.description class+="form-control" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -345,7 +357,7 @@
|
|||||||
<div class="col-sm-12">
|
<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">
|
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{% include 'partials/item_table.html' %}
|
{% include 'partials/item_table.html' %}
|
||||||
|
|||||||
@@ -74,6 +74,14 @@
|
|||||||
<lineStyle kind="linebelow" start="3,0" stop="3,0" colorName="black"/>
|
<lineStyle kind="linebelow" start="3,0" stop="3,0" colorName="black"/>
|
||||||
<lineStyle kind="linebelow" start="5,0" stop="5,0" colorName="black"/>
|
<lineStyle kind="linebelow" start="5,0" stop="5,0" colorName="black"/>
|
||||||
</blockTableStyle>
|
</blockTableStyle>
|
||||||
|
|
||||||
|
<listStyle name="ol"
|
||||||
|
bulletFormat="%s."
|
||||||
|
bulletFontSize="10" />
|
||||||
|
|
||||||
|
<listStyle name="ul"
|
||||||
|
start="bulletchar"
|
||||||
|
bulletFontSize="10"/>
|
||||||
</stylesheet>
|
</stylesheet>
|
||||||
|
|
||||||
<template > {# Note: page is 595x842 points (1 point=1/72in) #}
|
<template > {# Note: page is 595x842 points (1 point=1/72in) #}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
{% load markdown_tags %}
|
||||||
{% load filters %}
|
{% load filters %}
|
||||||
|
|
||||||
<setNextFrame name="main"/>
|
<setNextFrame name="main"/>
|
||||||
<nextFrame/>
|
<nextFrame/>
|
||||||
<blockTable style="headLayout" colWidths="330,165">
|
<blockTable style="headLayout" colWidths="330,165">
|
||||||
@@ -10,10 +12,8 @@
|
|||||||
<b>{{object.start_date|date:"D jS N Y"}}</b>
|
<b>{{object.start_date|date:"D jS N Y"}}</b>
|
||||||
</para>
|
</para>
|
||||||
|
|
||||||
<keepInFrame>
|
<keepInFrame maxHeight="500" onOverflow="shrink">
|
||||||
<para style="style.event_description">
|
{{ object.description|default_if_none:""|markdown:"rml" }}
|
||||||
{{ object.description|default_if_none:""|linebreaksxml }}
|
|
||||||
</para>
|
|
||||||
</keepInFrame>
|
</keepInFrame>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -184,7 +184,7 @@
|
|||||||
{% if item.description %}
|
{% if item.description %}
|
||||||
</para>
|
</para>
|
||||||
<para style="item_description">
|
<para style="item_description">
|
||||||
<em>{{ item.description|linebreaksxml }}</em>
|
{{ item.description|markdown:"rml" }}
|
||||||
</para>
|
</para>
|
||||||
<para>
|
<para>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% load namewithnotes from filters %}
|
{% load namewithnotes from filters %}
|
||||||
|
{% load markdown_tags %}
|
||||||
<div class="card card-info">
|
<div class="card card-info">
|
||||||
<div class="card-header">Event Info</div>
|
<div class="card-header">Event Info</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -46,7 +47,7 @@
|
|||||||
<dd class="col-sm-12"> </dd>
|
<dd class="col-sm-12"> </dd>
|
||||||
|
|
||||||
<dt class="col-sm-6">Event Description</dt>
|
<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>
|
<dd class="col-sm-12"> </dd>
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,10 @@
|
|||||||
id="item_name"/>
|
id="item_name"/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<label for="item_description" class="col-sm-2 col-form-label">Description</label>
|
||||||
<div class="col-sm-10">
|
<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>
|
id="item_description" rows="8"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
{% load markdown_tags %}
|
||||||
<tr id="item-{{item.pk}}" data-pk="{{item.pk}}" class="item_row">
|
<tr id="item-{{item.pk}}" data-pk="{{item.pk}}" class="item_row">
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<span class="name">{{ item.name }}</span>
|
<span class="name">{{ item.name }}</span>
|
||||||
<div class="item-description">
|
<div class="item-description">
|
||||||
<em class="description">{{item.description|linebreaksbr}}</em>
|
<em class="description">{{item.description|markdown}}</em>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
{% if perms.RIGS.view_event %}
|
{% 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]')
|
_warning_selector = (By.XPATH, '/html/body/div[1]/div[1]')
|
||||||
|
|
||||||
form_items = {
|
form_items = {
|
||||||
'description': (regions.TextBox, (By.ID, 'id_description')),
|
'description': (regions.SimpleMDETextArea, (By.ID, 'id_description')),
|
||||||
|
|
||||||
'name': (regions.TextBox, (By.ID, 'id_name')),
|
'name': (regions.TextBox, (By.ID, 'id_name')),
|
||||||
'start_date': (regions.DatePicker, (By.ID, 'id_start_date')),
|
'start_date': (regions.DatePicker, (By.ID, 'id_start_date')),
|
||||||
@@ -110,7 +110,7 @@ class CreateEvent(FormPage):
|
|||||||
'collected_by': (regions.TextBox, (By.ID, 'id_collector')),
|
'collected_by': (regions.TextBox, (By.ID, 'id_collector')),
|
||||||
'po': (regions.TextBox, (By.ID, 'id_purchase_order')),
|
'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):
|
def select_event_type(self, type_name):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from pypom import Region
|
from pypom import Region
|
||||||
from selenium.webdriver.common.by import By
|
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):
|
class Header(Region):
|
||||||
@@ -42,7 +42,7 @@ class ItemModal(Modal):
|
|||||||
|
|
||||||
form_items = {
|
form_items = {
|
||||||
'name': (TextBox, (By.ID, 'item_name')),
|
'name': (TextBox, (By.ID, 'item_name')),
|
||||||
'description': (TextBox, (By.ID, 'item_description')),
|
'description': (SimpleMDETextArea, (By.ID, 'item_description')),
|
||||||
'quantity': (TextBox, (By.ID, 'item_quantity')),
|
'quantity': (TextBox, (By.ID, 'item_quantity')),
|
||||||
'price': (TextBox, (By.ID, 'item_cost'))
|
'price': (TextBox, (By.ID, 'item_cost'))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ from datetime import date
|
|||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.utils import override_settings
|
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.urls import reverse, reverse_lazy
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from pytest_django.asserts import assertRedirects, assertNotContains, assertContains
|
from pytest_django.asserts import assertRedirects, assertNotContains, assertContains
|
||||||
@@ -170,6 +172,7 @@ class TestInvoiceDelete(TestCase):
|
|||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True,
|
cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True,
|
||||||
is_active=True, is_staff=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 = {
|
cls.events = {
|
||||||
1: models.Event.objects.create(name="TE E1", start_date=date.today()),
|
1: models.Event.objects.create(name="TE E1", start_date=date.today()),
|
||||||
2: models.Event.objects.create(name="TE E2", 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):
|
def test_ra_redirect(admin_client, admin_user, ra):
|
||||||
request_url = reverse('event_ra', kwargs={'pk': ra.event.pk})
|
request_url = reverse('event_ra', kwargs={'pk': ra.event.pk})
|
||||||
expected_url = reverse('ra_edit', kwargs={'pk': ra.pk})
|
expected_url = reverse('ra_edit', kwargs={'pk': ra.pk})
|
||||||
|
|
||||||
response = admin_client.get(request_url, follow=True)
|
response = admin_client.get(request_url, follow=True)
|
||||||
assertRedirects(response, expected_url, status_code=302, target_status_code=200)
|
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)
|
||||||
|
|||||||
@@ -5,11 +5,14 @@
|
|||||||
{% block css %}
|
{% block css %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
|
<link rel="stylesheet" href="{% static 'css/selects.css' %}"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static 'css/simplemde.min.css' %}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block preload_js %}
|
{% block preload_js %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<script src="{% static 'js/selects.js' %}"></script>
|
<script src="{% static 'js/selects.js' %}"></script>
|
||||||
|
<script src="{% static 'js/simplemde.min.js' %}"></script>
|
||||||
|
<script src="{% static 'js/interaction.js' %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
@@ -72,6 +75,11 @@
|
|||||||
preserveSelected: false
|
preserveSelected: false
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function () {
|
||||||
|
setupMDE('#id_comments');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
{% load widget_tweaks %}
|
{% load widget_tweaks %}
|
||||||
|
{% load markdown_tags %}
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
Asset Details
|
Asset Details
|
||||||
@@ -38,14 +40,14 @@
|
|||||||
<!---TODO: Lower default number of lines in comments box-->
|
<!---TODO: Lower default number of lines in comments box-->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="{{ form.comments.id_for_label }}">Comments</label>
|
<label for="{{ form.comments.id_for_label }}">Comments</label>
|
||||||
{% render_field form.comments|add_class:'form-control' %}
|
{% render_field form.comments|add_class:'form-control md-enabled' %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<dt>Asset ID</dt>
|
<dt>Asset ID</dt>
|
||||||
<dd>{{ object.asset_id }}</dd>
|
<dd>{{ object.asset_id }}</dd>
|
||||||
|
|
||||||
<dt>Description</dt>
|
<dt>Description</dt>
|
||||||
<dd style="overflow-wrap: break-word;">{{ object.description }}</dd>
|
<dd>{{ object.description }}</dd>
|
||||||
|
|
||||||
<dt>Category</dt>
|
<dt>Category</dt>
|
||||||
<dd>{{ object.category }}</dd>
|
<dd>{{ object.category }}</dd>
|
||||||
@@ -57,7 +59,7 @@
|
|||||||
<dd>{{ object.serial_number|default:'-' }}</dd>
|
<dd>{{ object.serial_number|default:'-' }}</dd>
|
||||||
|
|
||||||
<dt>Comments</dt>
|
<dt>Comments</dt>
|
||||||
<dd style="overflow-wrap: break-word;">{{ object.comments|default:'-'|linebreaksbr }}</dd>
|
<dd style="overflow-wrap: break-word;">{{ object.comments|default:'-'|markdown }}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class AssetForm(FormPage):
|
|||||||
'description': (regions.TextBox, (By.ID, 'id_description')),
|
'description': (regions.TextBox, (By.ID, 'id_description')),
|
||||||
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
|
'is_cable': (regions.CheckBox, (By.ID, 'id_is_cable')),
|
||||||
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
|
'serial_number': (regions.TextBox, (By.ID, 'id_serial_number')),
|
||||||
'comments': (regions.TextBox, (By.ID, 'id_comments')),
|
'comments': (regions.SimpleMDETextArea, (By.ID, 'id_comments')),
|
||||||
'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')),
|
'purchase_price': (regions.TextBox, (By.ID, 'id_purchase_price')),
|
||||||
'salvage_value': (regions.TextBox, (By.ID, 'id_salvage_value')),
|
'salvage_value': (regions.TextBox, (By.ID, 'id_salvage_value')),
|
||||||
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
|
'date_acquired': (regions.DatePicker, (By.ID, 'id_date_acquired')),
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ function styles(done) {
|
|||||||
'node_modules/fullcalendar/main.css',
|
'node_modules/fullcalendar/main.css',
|
||||||
'node_modules/bootstrap-select/dist/css/bootstrap-select.css',
|
'node_modules/bootstrap-select/dist/css/bootstrap-select.css',
|
||||||
'node_modules/ajax-bootstrap-select/dist/css/ajax-bootstrap-select.css',
|
'node_modules/ajax-bootstrap-select/dist/css/ajax-bootstrap-select.css',
|
||||||
'node_modules/flatpickr/dist/flatpickr.css',])
|
'node_modules/flatpickr/dist/flatpickr.css',
|
||||||
|
'node_modules/simplemde/dist/simplemde.min.css'
|
||||||
|
])
|
||||||
.pipe(sourcemaps.init())
|
.pipe(sourcemaps.init())
|
||||||
.pipe(sass().on('error', sass.logError))
|
.pipe(sass().on('error', sass.logError))
|
||||||
.pipe(gulpif(function(file) { return bs_select.includes(file.relative);}, con('selects.css')))
|
.pipe(gulpif(function(file) { return bs_select.includes(file.relative);}, con('selects.css')))
|
||||||
@@ -62,6 +64,7 @@ function scripts() {
|
|||||||
'node_modules/fullcalendar/main.js',
|
'node_modules/fullcalendar/main.js',
|
||||||
'node_modules/bootstrap-select/dist/js/bootstrap-select.js',
|
'node_modules/bootstrap-select/dist/js/bootstrap-select.js',
|
||||||
'node_modules/ajax-bootstrap-select/dist/js/ajax-bootstrap-select.js',
|
'node_modules/ajax-bootstrap-select/dist/js/ajax-bootstrap-select.js',
|
||||||
|
'node_modules/simplemde/dist/simplemde.min.js',
|
||||||
'node_modules/konami/konami.js',
|
'node_modules/konami/konami.js',
|
||||||
'pipeline/source_assets/js/**/*.js',])
|
'pipeline/source_assets/js/**/*.js',])
|
||||||
.pipe(gulpif(function(file) { return base_scripts.includes(file.relative);}, con('base.js')))
|
.pipe(gulpif(function(file) { return base_scripts.includes(file.relative);}, con('base.js')))
|
||||||
|
|||||||
33
package-lock.json
generated
33
package-lock.json
generated
@@ -965,6 +965,19 @@
|
|||||||
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
||||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
|
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
|
||||||
},
|
},
|
||||||
|
"codemirror": {
|
||||||
|
"version": "5.65.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.0.tgz",
|
||||||
|
"integrity": "sha512-gWEnHKEcz1Hyz7fsQWpK7P0sPI2/kSkRX2tc7DFA6TmZuDN75x/1ejnH/Pn8adYKrLEA1V2ww6L00GudHZbSKw=="
|
||||||
|
},
|
||||||
|
"codemirror-spell-checker": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/codemirror-spell-checker/-/codemirror-spell-checker-1.1.2.tgz",
|
||||||
|
"integrity": "sha1-HGYPkIlIPMtRE7m6nKGcP0mTNx4=",
|
||||||
|
"requires": {
|
||||||
|
"typo-js": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"collection-map": {
|
"collection-map": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz",
|
||||||
@@ -3691,6 +3704,11 @@
|
|||||||
"object-visit": "^1.0.0"
|
"object-visit": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"marked": {
|
||||||
|
"version": "4.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/marked/-/marked-4.0.8.tgz",
|
||||||
|
"integrity": "sha512-dkpJMIlJpc833hbjjg8jraw1t51e/eKDoG8TFOgc5O0Z77zaYKigYekTDop5AplRoKFGIaoazhYEhGkMtU3IeA=="
|
||||||
|
},
|
||||||
"matchdep": {
|
"matchdep": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz",
|
||||||
@@ -5641,6 +5659,16 @@
|
|||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz",
|
||||||
"integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ=="
|
"integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ=="
|
||||||
},
|
},
|
||||||
|
"simplemde": {
|
||||||
|
"version": "1.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/simplemde/-/simplemde-1.11.2.tgz",
|
||||||
|
"integrity": "sha1-ojo12XjSxA7wfewAjJLwcNjggOM=",
|
||||||
|
"requires": {
|
||||||
|
"codemirror": "*",
|
||||||
|
"codemirror-spell-checker": "*",
|
||||||
|
"marked": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"snapdragon": {
|
"snapdragon": {
|
||||||
"version": "0.8.2",
|
"version": "0.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
|
||||||
@@ -6445,6 +6473,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
|
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
|
||||||
},
|
},
|
||||||
|
"typo-js": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-bTGLjbD3WqZDR3CgEFkyi9Q/SS2oM29ipXrWfDb4M74ea69QwKAECVceYpaBu0GfdnASMg9Qfl67ttB23nePHg=="
|
||||||
|
},
|
||||||
"ua-parser-js": {
|
"ua-parser-js": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.2.tgz",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
"node-sass": "^7.0.0",
|
"node-sass": "^7.0.0",
|
||||||
"popper.js": "^1.16.1",
|
"popper.js": "^1.16.1",
|
||||||
"postcss": "^8.4.5",
|
"postcss": "^8.4.5",
|
||||||
|
"simplemde": "^1.11.2",
|
||||||
"uglify-js": "^3.14.5"
|
"uglify-js": "^3.14.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
marked.setOptions({
|
||||||
|
breaks: true,
|
||||||
|
})
|
||||||
|
|
||||||
function setupItemTable(items_json) {
|
function setupItemTable(items_json) {
|
||||||
objectitems = JSON.parse(items_json)
|
objectitems = JSON.parse(items_json)
|
||||||
$.each(objectitems, function (key, val) {
|
$.each(objectitems, function (key, val) {
|
||||||
@@ -6,12 +10,12 @@ function setupItemTable(items_json) {
|
|||||||
newitem = -1;
|
newitem = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function nl2br (str, is_xhtml) {
|
function nl2br(str, is_xhtml) {
|
||||||
var breakTag = (is_xhtml || typeof is_xhtml === 'undefined') ? '<br />' : '<br>';
|
var breakTag = (is_xhtml || typeof is_xhtml === 'undefined') ? '<br />' : '<br>';
|
||||||
return (str + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1'+ breakTag +'$2');
|
return (str + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1'+ breakTag +'$2');
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml (str) {
|
function escapeHtml(str) {
|
||||||
return $('<div/>').text(str).html();
|
return $('<div/>').text(str).html();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +36,16 @@ function updatePrices() {
|
|||||||
$('#total').text(parseFloat(sum + vat).toFixed(2));
|
$('#total').text(parseFloat(sum + vat).toFixed(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupMDE(selector) {
|
||||||
|
editor = new SimpleMDE({
|
||||||
|
element: $(selector)[0],
|
||||||
|
forceSync: true,
|
||||||
|
toolbar: ["bold", "italic", "strikethrough", "|", "unordered-list", "ordered-list", "|", "link", "|", "preview", "guide"],
|
||||||
|
status: true,
|
||||||
|
});
|
||||||
|
$(selector).data('mde_editor',editor);
|
||||||
|
}
|
||||||
|
|
||||||
$('#item-table').on('click', '.item-delete', function () {
|
$('#item-table').on('click', '.item-delete', function () {
|
||||||
delete objectitems[$(this).data('pk')]
|
delete objectitems[$(this).data('pk')]
|
||||||
$('#item-' + $(this).data('pk')).remove();
|
$('#item-' + $(this).data('pk')).remove();
|
||||||
@@ -106,7 +120,7 @@ $('body').on('submit', '#item-form', function (e) {
|
|||||||
// update the table
|
// update the table
|
||||||
$row = $('#item-' + pk);
|
$row = $('#item-' + pk);
|
||||||
$row.find('.name').html(escapeHtml(fields.name));
|
$row.find('.name').html(escapeHtml(fields.name));
|
||||||
$row.find('.description').html(nl2br(escapeHtml(fields.description)));
|
$row.find('.description').html(marked(fields.description));
|
||||||
$row.find('.cost').html(parseFloat(fields.cost).toFixed(2));
|
$row.find('.cost').html(parseFloat(fields.cost).toFixed(2));
|
||||||
$row.find('.quantity').html(fields.quantity);
|
$row.find('.quantity').html(fields.quantity);
|
||||||
|
|
||||||
|
|||||||
@@ -133,4 +133,21 @@
|
|||||||
-webkit-box-shadow: 0 0 0px 1000px rgba($info, .3) inset;
|
-webkit-box-shadow: 0 0 0px 1000px rgba($info, .3) inset;
|
||||||
transition: background-color 5000s ease-in-out 0s;
|
transition: background-color 5000s ease-in-out 0s;
|
||||||
}
|
}
|
||||||
|
.editor-toolbar > a {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
.editor-toolbar > a:hover {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
.editor-toolbar > a.active {
|
||||||
|
background: $info !important;
|
||||||
|
}
|
||||||
|
.cm-s-paper {
|
||||||
|
color: white;
|
||||||
|
background-color: $darktheme;
|
||||||
|
border-color: #bbb;
|
||||||
|
}
|
||||||
|
.CodeMirror-cursor {
|
||||||
|
border-color: white !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,3 +226,33 @@ html.embedded {
|
|||||||
max-width: 3em;
|
max-width: 3em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown {
|
||||||
|
h1 {
|
||||||
|
font-size: $h1-font-size * 0.75;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: $h2-font-size * 0.8;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: $h3-font-size * 0.85;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
font-size: $h4-font-size * 0.9;
|
||||||
|
}
|
||||||
|
h5 {
|
||||||
|
font-size: $h5-font-size * 0.95;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#rigboard {
|
||||||
|
.markdown {
|
||||||
|
img {
|
||||||
|
max-width: 30rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -77,8 +77,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal fade" id="modal" role="dialog" tabindex=-1></div>
|
<div class="modal fade" id="modal" role="dialog" tabindex=-1></div>
|
||||||
|
|
||||||
<script src="{% static 'js/base.js' %}"></script>
|
<script src="{% static 'js/base.js' %}"></script>
|
||||||
|
<script src="{% static 'js/marked.min.js' %}"></script>
|
||||||
{% include 'partials/dark_theme.html' %}
|
{% include 'partials/dark_theme.html' %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user