dmptrluke / django-markdownfield Goto Github PK
View Code? Open in Web Editor NEWA simple custom field for Django that can safely render Markdown and store it in the database.
License: MIT License
A simple custom field for Django that can safely render Markdown and store it in the database.
License: MIT License
Hello! I am running into an issue where this plugin is throwing an error when I try to post a new Model Object with a markdownfield. Specifically, it seems it's having issues when trying to parse URLs. It happens when I try adding the URL manually or using the editor's function. When the links are removed, everything POSTs just fine. Am I missing some install or configuration step?
Here is the traceback from Django:
Request Method: POST
Request URL: http://localhost:8000/admin/blog/blog/add/
Django Version: 4.0.4
Python Version: 3.9.12
Installed Applications:
['fontawesomefree',
'markdownfield',
'blog.apps.BlogConfig',
'projects.apps.ProjectsConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware']
Traceback (most recent call last):
File "/home/bryson/.local/lib/python3.9/site-packages/django/core/handlers/exception.py", line 55, in inner
response = get_response(request)
File "/home/bryson/.local/lib/python3.9/site-packages/django/core/handlers/base.py", line 197, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/home/bryson/.local/lib/python3.9/site-packages/django/contrib/admin/options.py", line 683, in wrapper
return self.admin_site.admin_view(view)(*args, **kwargs)
File "/home/bryson/.local/lib/python3.9/site-packages/django/utils/decorators.py", line 133, in _wrapped_view
response = view_func(request, *args, **kwargs)
File "/home/bryson/.local/lib/python3.9/site-packages/django/views/decorators/cache.py", line 62, in _wrapped_view_func
response = view_func(request, *args, **kwargs)
File "/home/bryson/.local/lib/python3.9/site-packages/django/contrib/admin/sites.py", line 242, in inner
return view(request, *args, **kwargs)
File "/home/bryson/.local/lib/python3.9/site-packages/django/contrib/admin/options.py", line 1885, in add_view
return self.changeform_view(request, None, form_url, extra_context)
File "/home/bryson/.local/lib/python3.9/site-packages/django/utils/decorators.py", line 46, in _wrapper
return bound_method(*args, **kwargs)
File "/home/bryson/.local/lib/python3.9/site-packages/django/utils/decorators.py", line 133, in _wrapped_view
response = view_func(request, *args, **kwargs)
File "/home/bryson/.local/lib/python3.9/site-packages/django/contrib/admin/options.py", line 1745, in changeform_view
return self._changeform_view(request, object_id, form_url, extra_context)
File "/home/bryson/.local/lib/python3.9/site-packages/django/contrib/admin/options.py", line 1797, in _changeform_view
self.save_model(request, new_object, form, not add)
File "/home/bryson/.local/lib/python3.9/site-packages/django/contrib/admin/options.py", line 1220, in save_model
obj.save()
File "/home/bryson/.local/lib/python3.9/site-packages/django/db/models/base.py", line 806, in save
self.save_base(
File "/home/bryson/.local/lib/python3.9/site-packages/django/db/models/base.py", line 857, in save_base
updated = self._save_table(
File "/home/bryson/.local/lib/python3.9/site-packages/django/db/models/base.py", line 1000, in _save_table
results = self._do_insert(
File "/home/bryson/.local/lib/python3.9/site-packages/django/db/models/base.py", line 1041, in _do_insert
return manager._insert(
File "/home/bryson/.local/lib/python3.9/site-packages/django/db/models/manager.py", line 85, in manager_method
return getattr(self.get_queryset(), name)(*args, **kwargs)
File "/home/bryson/.local/lib/python3.9/site-packages/django/db/models/query.py", line 1434, in _insert
return query.get_compiler(using=using).execute_sql(returning_fields)
File "/home/bryson/.local/lib/python3.9/site-packages/django/db/models/sql/compiler.py", line 1620, in execute_sql
for sql, params in self.as_sql():
File "/home/bryson/.local/lib/python3.9/site-packages/django/db/models/sql/compiler.py", line 1547, in as_sql
value_rows = [
File "/home/bryson/.local/lib/python3.9/site-packages/django/db/models/sql/compiler.py", line 1548, in <listcomp>
[
File "/home/bryson/.local/lib/python3.9/site-packages/django/db/models/sql/compiler.py", line 1549, in <listcomp>
self.prepare_value(field, self.pre_save_val(field, obj))
File "/home/bryson/.local/lib/python3.9/site-packages/django/db/models/sql/compiler.py", line 1497, in pre_save_val
return field.pre_save(obj, add=True)
File "/home/bryson/.local/lib/python3.9/site-packages/markdownfield/models.py", line 98, in pre_save
clean = cleaner.clean(dirty)
File "/home/bryson/.local/lib/python3.9/site-packages/bleach/sanitizer.py", line 184, in clean
return self.serializer.render(filtered)
File "/home/bryson/.local/lib/python3.9/site-packages/bleach/_vendor/html5lib/serializer.py", line 398, in render
return "".join(list(self.serialize(treewalker)))
File "/home/bryson/.local/lib/python3.9/site-packages/bleach/html5lib_shim.py", line 705, in serialize
for stoken in super().serialize(treewalker, encoding):
File "/home/bryson/.local/lib/python3.9/site-packages/bleach/_vendor/html5lib/serializer.py", line 265, in serialize
for token in treewalker:
File "/home/bryson/.local/lib/python3.9/site-packages/bleach/linkifier.py", line 520, in __iter__
yield from self.handle_a_tag(token_buffer)
File "/home/bryson/.local/lib/python3.9/site-packages/bleach/linkifier.py", line 481, in handle_a_tag
attrs = self.apply_callbacks(attrs, False)
File "/home/bryson/.local/lib/python3.9/site-packages/bleach/linkifier.py", line 254, in apply_callbacks
attrs = cb(attrs, is_new)
File "/home/bryson/.local/lib/python3.9/site-packages/markdownfield/util.py", line 34, in format_link
c = urlparse(settings.SITE_URL)
File "/home/bryson/.local/lib/python3.9/site-packages/django/conf/__init__.py", line 88, in __getattr__
val = getattr(self._wrapped, name)
Exception Type: AttributeError at /admin/blog/blog/add/
Exception Value: 'Settings' object has no attribute 'SITE_URL'
The model in concern:
class Project(models.Model):
title = models.CharField(max_length=200)
current = models.BooleanField(default=True)
overview = models.TextField()
description = MarkdownField(rendered_field='text_rendered',use_editor=False, use_admin_editor=True, validator=VALIDATOR_STANDARD)
text_rendered = RenderedMarkdownField()
image = models.ImageField(upload_to='projects/static/projects/images/')
Let me know if you need more information.
#33 by @kavdev fixed a layout issue with django-markdownfield in newer versions of Django, but has caused a minor regression in Django 3.2.
This issue isn't as impactful as the issue the original code had in newer versions, so I don't want to revert the change, but I would like to find a solution to fix the regression on 3.2 while it's still supported.
How to migrate from a TextField/CharField to the MarkdownField.
I picked up this package for my demo site and was really struggling to include internal links into markdown files, at least in my development environment. I want to be able to include relative URL file paths, i.e. /djangoapp/view/
, into my .md files, rather than absolute file paths, i.e. http://localhost:3000/djangoapp/view
.
My settings are divided into local.py
and production.py
. In my local.py
, I have SITE_URL = "http://localhost:3000"
, but when I set a link in my .md files such as [link](/djangoapp/view/)
, it saved the HTML as an external link <a ref="/djangoapp/view/" target="_blank" class=" external" rel="nofollow noopener noreferrer">djangomodel</a>
. If I put the absolute file path in the .md file, [link](http://localhost:3000/djangoapp/view/)
, the link was rendered as a local link. However, I want to use my .md files in development and production without modifying all the absolute file paths.
I first tried a solution whereby I include Django url tags ({% url "djangoapp:view" %}
in my .md files and render the .md HTML equivalents as Template objects in my view, which I then served to the context. This seemed messy. Then I figured out the following one that seems pretty hacky and not that performant, but does work like I want it to.
from django.urls import URLPattern, URLResolver
def list_urls(lis, acc=None):
#https://stackoverflow.com/questions/1275486/how-can-i-list-urlpatterns-endpoints-on-django#answer-54531546
if acc is None:
acc = []
if not lis:
return
l = lis[0]
if isinstance(l, URLPattern):
yield acc + [str(l.pattern)]
elif isinstance(l, URLResolver):
yield from list_urls(l.url_patterns, acc + [str(l.pattern)])
yield from list_urls(lis[1:], acc)
def format_link(attrs: Dict[tuple, str], new: bool = False):
"""
This is really weird and ugly, but that's how bleach linkify filters work.
"""
try:
p = urlparse(attrs[(None, 'href')])
except KeyError:
# no href, probably an anchor
return attrs
if not any([p.scheme, p.netloc, p.path]) and p.fragment:
# the link isn't going anywhere, probably a fragment link
return attrs
if hasattr(settings, 'SITE_URL'):
c = urlparse(settings.SITE_URL)
link_is_external = p.netloc != c.netloc
else:
# Assume true for safety
link_is_external = True
if link_is_external:
# create a list of all the app's URLs and check if the hyperlink path is in that list
urlconf = __import__(settings.ROOT_URLCONF, {}, {}, [''])
app_urls = ["/" + ''.join(url_part_list) for url_part_list in list_urls(urlconf.urlpatterns)]
if p.path not in app_urls:
# link is external - secure and mark
attrs[(None, 'target')] = '_blank'
attrs[(None, 'class')] = attrs.get((None, 'class'), '') + ' external'
attrs[(None, 'rel')] = 'nofollow noopener noreferrer'
return attrs
class OverwrittenMarkdownField(MarkdownField):
def pre_save(self, model_instance, add):
value = super().pre_save(model_instance, add)
if not self.rendered_field:
return value
dirty = markdown(
text=value,
extensions=EXTENSIONS,
extension_configs=EXTENSION_CONFIGS
)
if self.validator.sanitize:
if self.validator.linkify:
cleaner = bleach.Cleaner(tags=self.validator.allowed_tags,
attributes=self.validator.allowed_attrs,
css_sanitizer=self.validator.css_sanitizer,
filters=[partial(LinkifyFilter,
callbacks=[format_link, blacklist_link])])
else:
cleaner = bleach.Cleaner(tags=self.validator.allowed_tags,
attributes=self.validator.allowed_attrs,
css_sanitizer=self.validator.css_sanitizer)
clean = cleaner.clean(dirty)
setattr(model_instance, self.rendered_field, clean)
else:
# danger!
setattr(model_instance, self.rendered_field, dirty)
return value
This results in the desired behavior whereby relative internal links are saved and rendered with the HTML for an internal link (no target="_blank"
).
Appreciate any feedback.
Would be great to preview. Since there is rendering included already, it doesn't sound hard to implement the preview
feature. Seems to miss just a place(modal, whatever else) where this preview might be. WDYT? @benjaoming @dmptrluke @kpavlovsky
Otherwise thanks for the tool!
Since this project has been more widely-adopted, the assumption that django-csp is being used should be removed from widget.html.
Support for CSP_NONCE should be maintained, but the nonce
attribute should not be added to the HTML if CSP_NONCE is not defined in the template.
I have a simple model that I only need to have this markdown field. I set up everything like was mentioned in the README, but when I go to run the migration, I constantly get this error:
File "/usr/local/lib/python3.8/site-packages/markdownfield/models.py", line 71, in formfield
if defaults['widget'] == admin_widgets.AdminTextareaWidget:
KeyError: 'widget'
It seems like the 'widget' property isn't defined on defaults and this always errors out. Any thoughts on how to fix this?
Hello!
First of all thanks for this awesome library.
I am really grateful for this work!
Now the problem:
Multiline code block with three backticks is represented as P tag with CODE tag inside.
<p>
<code>print(f"Hello, {username}")</code>
</p>
This leads to ignored formatting, newlines, etc
Is it possible to make code blocks being wrapped in PRE tag?
<pre>
<code>alert();</code>
</pre>
Thank you, have a great day!
I'm in a markdown editor research nightmare. I've installed and uninstalled many markdown and WYSIWYG editors at this point.
My needs are:
Does django-markdownfield give access to the EasyMDE object to modify the toolbar for example?
Currently in this project it uses easymde version 2.14.0
Please consider updating easymde
. Thanks in advance
I do see the performance advantage of storing rendered Markdown, but also wonder why there is no documented means for avoiding it. It seems a perfectly valid and common use case to me, to store only the markdown and render on, well, rendering the template. That is rather than:
from django.db import models
from markdownfield.models import MarkdownField, RenderedMarkdownField
from markdownfield.validators import VALIDATOR_STANDARD
class Page(models.Model):
text = MarkdownField(rendered_field='text_rendered', validator=VALIDATOR_STANDARD)
text_rendered = RenderedMarkdownField()
and in templates:
{{ post.text_rendered | safe }}
something more like:
from django.db import models
from markdownfield.models import MarkdownField
from markdownfield.validators import VALIDATOR_STANDARD
class Page(models.Model):
text = MarkdownField(validator=VALIDATOR_STANDARD)
and in templates:
{{ post.text | render_markdown }}
Not only is that use case to my mind very valid, I think that the render_markdown
filter would find broader use in templates.
Title says it all. Just isn't obvious, and I'm having to work it out as best possible. Be nice to have a little doc on it, or a mention in the README. Basically my forms are rendering with a standard TextArea it seems and I'm thinking I need to do something to get the MDEWidget in its place.
Hi,
I want to know if this is maintained. As i plan to use this project in a production build of mine i would gladly be willing to help modernize this project
Hi,
Since bleach is deprecated, I think its better if we switch to nh3
apologies i am not familiar with how bleach
integrates with django-markdownfield
This (from documentation):
{{ post.text_rendered | safe }}
fails to render images, renders the text with a link to the image instead. Is there a renderer that will render the imagines in situ or an option to pass or another filter?
When running the library in django 4, the following warning is emitted:
RemovedInDjango41Warning: 'markdownfield' defines default_app_config = 'markdownfield.apps.MarkdownFieldConfig'. Django now detects this configuration automatically. You can remove default_app_config.
I've been doing some PR merges. I'd like to run a manual test before releasing 0.11. But the changes have all looked non-breaking.
CC: @dmptrluke
As a beginner it was definitely not obvious that I had to add "markdownfield" to my INSTALLED_APPS. I spent almost an hour banging my head against a wall until I finally decided to look for a different package - only when looking at docs for other packages did I realise that maybe I had to add it to my INSTALLED APPS.
A single sentence on the package documentation page would save a lot of headache. Please add this step to the docs. It's not obvious for beginners.
Hey,
The admin widget didn't work for me until I added markdownfield
to my INSTALLED_APPS
.
I think that should be noted in the instruction.
Admin widgets use auto-generated JS like this (see widgets.html for source):
{{ options | json_script:options_id }}
<script nonce="{{ csp_nonce }}" type="text/javascript">
const {{ widget.name }}_options = JSON.parse(document.getElementById('{{ options_id }}').textContent);
const {{ widget.name }}_editor = new EasyMDE(Object.assign({
element: document.getElementById('{{ widget.attrs.id }}'),
hideIcons: ["side-by-side", "preview"],
spellChecker: false,
parsingConfig: {
allowAtxHeaderWithoutSpace: true,
}
}, {{ widget.name }}_options));
</script>
It's fine for usual ModelAdmin instances, but inline admins would produce invalid code like this:
<script id="options_MZdmTr6jNmZtxZhU5tjWzw" type="application/json">{}</script>
<script nonce="" type="text/javascript">
const tasks-0-description_options = JSON.parse(document.getElementById('options_MZdmTr6jNmZtxZhU5tjWzw').textContent);
const tasks-0-description_editor = new EasyMDE(Object.assign({
element: document.getElementById('id_tasks-0-description'),
hideIcons: ["side-by-side", "preview"],
spellChecker: false,
parsingConfig: {
allowAtxHeaderWithoutSpace: true,
}
}, tasks-0-description_options));
</script>
Obvious problem is that dash is illegal in variable names.
An obvious solution is to define the variable prefix in widget context based on self.uuid (see widgets.py) and use it instead of widget.name
. As a workaround, monkey-patching markdownfield/widget.html
via copying the source to any internal app and replacing {{ widget.name }}_options
with {{ options_id }}_options
works, although generated names like options_MZdmTr6jNmZtxZhU5tjWzw_options
aren't that pretty.
P.S. I'll try to provide a MR if I have time ;)
I've already written readme files, I just want to store source code of markdown files, is there a way we can do this?
Hi there,
It seems the documentation is somewhat out of date. It refers to the module markdownfields.fields
which doesn't actually exist.
Greetings,
JJ
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.