Reinstate Markdown support for symposion

Allow Markdown within the speaker bio and proposal abstract,
with a restricted set of tags for safety.
Render direct to markdown instead of storing plain and HTML
in the database.
Fix issue in timetable when a day had no slots created.
This commit is contained in:
Joel Addison 2025-01-03 12:00:06 +00:00
parent fb3fcc2faa
commit 447ed3eaf7
12 changed files with 67 additions and 42 deletions

View file

@ -230,6 +230,7 @@ INSTALLED_APPS = [
"django_jsonfield_backport", "django_jsonfield_backport",
"pinax.eventlog", "pinax.eventlog",
"timezone_field", "timezone_field",
'markdownify.apps.MarkdownifyConfig',
# symposion # symposion
"symposion", "symposion",
@ -347,6 +348,7 @@ LOGGING = {
'level': 'DEBUG' 'level': 'DEBUG'
}, },
} }
FIXTURE_DIRS = [ FIXTURE_DIRS = [
os.path.join(PROJECT_ROOT, "fixtures"), os.path.join(PROJECT_ROOT, "fixtures"),
] ]
@ -360,6 +362,30 @@ AUTHENTICATION_BACKENDS = [
LOGIN_URL = '/saml2/login/' LOGIN_URL = '/saml2/login/'
SESSION_EXPIRE_AT_BROWSER_CLOSE = True SESSION_EXPIRE_AT_BROWSER_CLOSE = True
MARKDOWNIFY = {
"default": {
"WHITELIST_TAGS": [
'a',
'abbr',
'acronym',
'b',
'blockquote',
'code',
'em',
'i',
'li',
'ol',
'p',
'pre',
'strong',
'ul',
],
"MARKDOWN_EXTENSIONS": [
"markdown.extensions.fenced_code",
]
}
}
CONFERENCE_ID = 1 CONFERENCE_ID = 1
PROPOSAL_FORMS = { PROPOSAL_FORMS = {
"talk": "pinaxcon.proposals.forms.TalkProposalForm", "talk": "pinaxcon.proposals.forms.TalkProposalForm",

View file

@ -1,5 +1,6 @@
{% load i18n %} {% load i18n %}
{% load lca2018_tags %} {% load lca2018_tags %}
{% load markdownify %}
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
@ -98,26 +99,24 @@
<div class="row"> <div class="row">
<label class="list-label col-md-2">Abstract</label> <label class="list-label col-md-2">Abstract</label>
<div class="col-md-10"> <div class="col-md-10">
<div class="abstract monospace-text">{{ proposal.abstract_html|safe }}&nbsp;</div> <div class="abstract">{{ proposal.abstract | markdownify }}</div>
<p></p>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<label class="list-label col-md-2">Private Abstract</label> <label class="list-label col-md-2">Private Abstract</label>
<div class="col-md-10"> <div class="col-md-10">
<div class="private_abstract monospace-text">{{ proposal.private_abstract_html|safe }}&nbsp;</div> <div class="private_abstract">{{ proposal.private_abstract | markdownify }}</div>
<p></p>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<label class="list-label col-md-2">Content Warning</label> <label class="list-label col-md-2">Content Warning</label>
<div class="col-md-10"> <div class="col-md-10">
{% if proposal.content_warning_html %} {% if proposal.content_warning %}
<div class="content_warning monospace-text">{{ proposal.content_warning_html|safe }}</div> <div class="content_warning">{{ proposal.content_warning | markdownify }}</div>
{% else %} {% else %}
<div class="content_warning monospace-text"><b>No Content Warning Provided</b></div> <div class="content_warning"><p><b>None Provided</b></p></div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@ -137,7 +136,7 @@
<label class="list-label col-md-2">Project URL</label> <label class="list-label col-md-2">Project URL</label>
<div class="col-md-10"> <div class="col-md-10">
{% if proposal.project_url %} {% if proposal.project_url %}
<p><a href="{{ proposal.project_url|safe }}">{{ proposal.project_url|safe }}</a>&nbsp;</p> <p><a href="{{ proposal.project_url|safe }}">{{ proposal.project_url|safe }}</a></p>
{% else %} {% else %}
<p><b>None Provided</b></p> <p><b>None Provided</b></p>
{% endif %} {% endif %}
@ -148,7 +147,7 @@
<label class="list-label col-md-2">Video URL</label> <label class="list-label col-md-2">Video URL</label>
<div class="col-md-10"> <div class="col-md-10">
{% if proposal.video_url %} {% if proposal.video_url %}
<p><a href="{{ proposal.video_url|safe }}">{{ proposal.video_url|safe }}</a>&nbsp;</p> <p><a href="{{ proposal.video_url|safe }}">{{ proposal.video_url|safe }}</a></p>
{% else %} {% else %}
<p><b>None Provided</b></p> <p><b>None Provided</b></p>
{% endif %} {% endif %}
@ -158,10 +157,10 @@
<div class="row"> <div class="row">
<label class="list-label col-md-2">Special Requirements</label> <label class="list-label col-md-2">Special Requirements</label>
<div class="col-md-10"> <div class="col-md-10">
{% if proposal.technical_requirements_html %} {% if proposal.technical_requirements %}
<div class="special_requirements monospace-text">{{ proposal.technical_requirements_html|safe }}</div> <div class="special_requirements">{{ proposal.technical_requirements | markdownify }}</div>
{% else %} {% else %}
<div class="special_requirements monospace-text"><b>No Special Talk Requirements Requested</b></div> <div class="special_requirements"><p><b>None Provided</b></p></div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@ -169,21 +168,21 @@
<div class="row"> <div class="row">
<label class="list-label col-md-2">Requires approval from employer?</label> <label class="list-label col-md-2">Requires approval from employer?</label>
<div class="col-md-10"> <div class="col-md-10">
<p>{{ proposal.require_approval }}&nbsp;</p> <p>{{ proposal.require_approval }}</p>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<label class="list-label col-md-2">Recording Release</label> <label class="list-label col-md-2">Recording Release</label>
<div class="col-md-10"> <div class="col-md-10">
<p>{{ proposal.recording_release }}&nbsp;</p> <p>{{ proposal.recording_release }}</p>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<label class="list-label col-md-2">Materials Release</label> <label class="list-label col-md-2">Materials Release</label>
<div class="col-md-10"> <div class="col-md-10">
<p>{{ proposal.materials_release }}&nbsp;</p> <p>{{ proposal.materials_release }}</p>
</div> </div>
</div> </div>
@ -210,16 +209,16 @@
</div> </div>
<div class="row"> <div class="row">
<label class="list-label col-md-2">Biography</label> <label class="list-label col-md-2">Biography</label>
<div class="col-md-10 monospace-text">{{ speaker.biography_html|safe }}&nbsp;</div> <div class="col-md-10">{{ speaker.biography | markdownify }}</div>
</div> </div>
<div class="row"> <div class="row">
<label class="list-label col-md-2">Experience</label> <label class="list-label col-md-2">Experience</label>
<div class="col-md-10 monospace-text">{{ speaker.experience_html|safe }}&nbsp;</div> <div class="col-md-10">{{ speaker.experience | markdownify }}</div>
</div> </div>
{% if speaker.accessibility_html %} {% if speaker.accessibility %}
<div class="row"> <div class="row">
<label class="list-label col-md-2">Accessibility Requirements</label> <label class="list-label col-md-2">Accessibility Requirements</label>
<div class="col-md-10 monospace-text">{{ speaker.accessibility_html|safe }}&nbsp;</div> <div class="col-md-10">{{ speaker.accessibility | markdownify }}</div>
</div> </div>
{% endif %} {% endif %}
</div> </div>

View file

@ -1,6 +1,6 @@
{% extends "site_base.html" %} {% extends "site_base.html" %}
{% load static %} {% load static %}
{% load markdownify %}
{% load bootstrap %} {% load bootstrap %}
{% block body_class %}review{% endblock %} {% block body_class %}review{% endblock %}
@ -28,7 +28,7 @@
<h3>Abstract</h3> <h3>Abstract</h3>
<div class="abstract"> <div class="abstract">
{{ proposal.abstract_html|safe }} {{ proposal.abstract | markdownify }}
</div> </div>
<p><b>Audience level</b>: {{ proposal.get_audience_level_display }}</p> <p><b>Audience level</b>: {{ proposal.get_audience_level_display }}</p>

View file

@ -5,6 +5,7 @@
{% load sitetree %} {% load sitetree %}
{% load static %} {% load static %}
{% load thumbnail %} {% load thumbnail %}
{% load markdownify %}
{% block head_title %}Presentation: {{ presentation.title }}{% endblock %} {% block head_title %}Presentation: {{ presentation.title }}{% endblock %}
{% block page_title %}{{ presentation.title }}{% endblock %} {% block page_title %}{{ presentation.title }}{% endblock %}
@ -42,7 +43,7 @@
<a href="{{ speaker.homepage }}">{{ speaker.homepage }}</a> <a href="{{ speaker.homepage }}">{{ speaker.homepage }}</a>
{% endif %} {% endif %}
</p> </p>
<div class="bio">{{ speaker.biography_html|safe}}</div> <div class="bio">{{ speaker.biography | markdownify }}</div>
</p> </p>
</li> </li>
{% endfor %} {% endfor %}
@ -51,9 +52,7 @@
<div class="col-md-9 presentation-abstract"> <div class="col-md-9 presentation-abstract">
<h2 class="mt-4">Abstract</h4> <h2 class="mt-4">Abstract</h4>
{% autoescape off %} <div class="abstract pb-4">{{ presentation.abstract | markdownify }}</div>
<div class="abstract pb-4"><p>{{ presentation.abstract_html|safe|clean_text|urlize }}</p></div>
{% endautoescape %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -4,6 +4,7 @@
{% load lca2018_tags %} {% load lca2018_tags %}
{% load lca2019_tags %} {% load lca2019_tags %}
{% load thumbnail %} {% load thumbnail %}
{% load markdownify %}
{% block head_title %}Speaker - {{ speaker.name }}{% endblock %} {% block head_title %}Speaker - {{ speaker.name }}{% endblock %}
{% block page_title %}Speaker - {{ speaker.name }}{% endblock %} {% block page_title %}Speaker - {{ speaker.name }}{% endblock %}
@ -26,7 +27,7 @@
<h3>Biography</h3> <h3>Biography</h3>
<div class="bio">{{ speaker.biography_html|safe }}</div> <div class="bio">{{ speaker.biography | markdownify }}</div>
<h3 class="my-4">Presentations</h3> <h3 class="my-4">Presentations</h3>

View file

@ -30,9 +30,11 @@ django-sitetree==1.16.0
django-taggit==1.3.0 django-taggit==1.3.0
django-timezone-field==4.1.2 django-timezone-field==4.1.2
easy-thumbnails==2.8.5 easy-thumbnails==2.8.5
bleach==3.2.1 bleach==6.1.0
pytz>=2020.1 pytz>=2020.1
django-ical==1.7.1 django-ical==1.7.1
django-markdownify==0.9.5
markdown==3.7
# Registrasion reqs # Registrasion reqs
django-nested-admin==3.3.2 django-nested-admin==3.3.2

View file

@ -111,7 +111,7 @@ h3, .h3 {
label.label-required:after { content: ' *'; } label.label-required:after { content: ' *'; }
.abstract, .bio, .monospace-text { .monospace-text {
font-family: Hack, monospace; font-family: Hack, monospace;
white-space: pre-wrap; white-space: pre-wrap;
} }

View file

@ -1,5 +1,3 @@
TEXT_FIELD_MONOSPACE_NOTE=( TEXT_FIELD_FORMAT_NOTE=(
"This field is rendered with the monospace font " "This field supports <a href='https://daringfireball.net/projects/markdown/syntax' target='_blank'>Markdown</a>, " +
"<a tabindex=\"-1\" href=\"https://sourcefoundry.org/hack/\">Hack</a> with " "with limitations on allowed elements.")
"whitespace preserved")

View file

@ -100,7 +100,7 @@ class ProposalBase(models.Model):
abstract = models.TextField( abstract = models.TextField(
_("Abstract"), _("Abstract"),
help_text=_("This will appear in the conference programme. Up to about " help_text=_("This will appear in the conference programme. Up to about "
"500 words. " + constants.TEXT_FIELD_MONOSPACE_NOTE) "500 words. " + constants.TEXT_FIELD_FORMAT_NOTE)
) )
abstract_html = models.TextField(blank=True) abstract_html = models.TextField(blank=True)
@ -109,7 +109,7 @@ class ProposalBase(models.Model):
help_text=_("This will only be shown to organisers and reviewers. You " help_text=_("This will only be shown to organisers and reviewers. You "
"should provide any details about your proposal that you " "should provide any details about your proposal that you "
"don't want to be public here. " + "don't want to be public here. " +
constants.TEXT_FIELD_MONOSPACE_NOTE) constants.TEXT_FIELD_FORMAT_NOTE)
) )
private_abstract_html = models.TextField(blank=True) private_abstract_html = models.TextField(blank=True)
@ -121,7 +121,7 @@ class ProposalBase(models.Model):
"please list your technical requirements here. Such as: a " "please list your technical requirements here. Such as: a "
"static IP address, A/V equipment or will be demonstrating " "static IP address, A/V equipment or will be demonstrating "
"security-related techniques on the conference network. " + "security-related techniques on the conference network. " +
constants.TEXT_FIELD_MONOSPACE_NOTE) constants.TEXT_FIELD_FORMAT_NOTE)
) )
technical_requirements_html = models.TextField(blank=True) technical_requirements_html = models.TextField(blank=True)

View file

@ -34,9 +34,10 @@ class TimeTable(object):
return self._rooms return self._rooms
def __iter__(self): def __iter__(self):
slots = self.slots() if not self._times:
return
row = [] slots = self.slots()
total_room_count = self.rooms().count() total_room_count = self.rooms().count()
for time, next_time in pairwise(self._times): for time, next_time in pairwise(self._times):
row = {"time": time, "end": next_time, "slots": []} row = {"time": time, "end": next_time, "slots": []}

View file

@ -41,7 +41,7 @@ class Speaker(models.Model):
help_text=_("This will appear on the conference website and in the " help_text=_("This will appear on the conference website and in the "
"programme. Please write in the third person, eg 'Alice " "programme. Please write in the third person, eg 'Alice "
"is a Moblin hacker...', 150-200 words. " + "is a Moblin hacker...', 150-200 words. " +
constants.TEXT_FIELD_MONOSPACE_NOTE), constants.TEXT_FIELD_FORMAT_NOTE),
verbose_name=_("Biography"), verbose_name=_("Biography"),
) )
biography_html = models.TextField(blank=True) biography_html = models.TextField(blank=True)
@ -51,7 +51,7 @@ class Speaker(models.Model):
"we'd like to know. Anything you put here will only be " "we'd like to know. Anything you put here will only be "
"seen by the organisers and reviewers; use it to convince " "seen by the organisers and reviewers; use it to convince "
"them why they should accept your proposal. " + "them why they should accept your proposal. " +
constants.TEXT_FIELD_MONOSPACE_NOTE), constants.TEXT_FIELD_FORMAT_NOTE),
verbose_name=_("Speaking experience"), verbose_name=_("Speaking experience"),
) )
experience_html = models.TextField(blank=True) experience_html = models.TextField(blank=True)

View file

@ -1,7 +1,6 @@
import bleach import bleach
tags = bleach.sanitizer.ALLOWED_TAGS[:] tags = bleach.sanitizer.ALLOWED_TAGS | {'p', 'pre'}
tags.extend(['p', 'pre'])
def parse(text): def parse(text):