kopia lustrzana https://github.com/rtts/django-simplecms
Replace CKEditor's RichTextField with MarkdownField
Because you should not store raw HTML in database tables!readwriteweb
rodzic
bd654f5103
commit
f9ea04662b
29
cms/admin.py
29
cms/admin.py
|
@ -1,29 +0,0 @@
|
|||
from django.contrib import admin
|
||||
from django.utils.text import Truncator
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from .models import Config
|
||||
|
||||
class BasePageAdmin(admin.ModelAdmin):
|
||||
prepopulated_fields = {'slug': ('title',)}
|
||||
|
||||
class BaseSectionAdmin(admin.ModelAdmin):
|
||||
list_filter = [
|
||||
('page', admin.RelatedOnlyFieldListFilter),
|
||||
]
|
||||
list_display = ['__str__', 'get_type_display']
|
||||
|
||||
@admin.register(Config)
|
||||
class ConfigAdmin(admin.ModelAdmin):
|
||||
list_display = ['__str__', 'get_content']
|
||||
exclude = ['parameter']
|
||||
|
||||
def get_content(self, obj):
|
||||
return mark_safe(Truncator(obj.content).words(50, html=True))
|
||||
get_content.short_description = _('content')
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def has_delete_permission(self, *args, **kwargs):
|
||||
return False
|
|
@ -1,11 +1,11 @@
|
|||
# Generated by Django 3.0.1 on 2020-01-02 18:31
|
||||
# Generated by Django 3.0.1 on 2020-01-02 20:41
|
||||
|
||||
import ckeditor.fields
|
||||
import cms.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import embed_video.fields
|
||||
import markdownfield.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -34,19 +34,6 @@ class Migration(migrations.Migration):
|
|||
'swappable': 'CMS_PAGE_MODEL',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Config',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('parameter', models.PositiveIntegerField(choices=[(10, 'Footer')], unique=True)),
|
||||
('content', ckeditor.fields.RichTextField(blank=True, verbose_name='content')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'configuration parameter',
|
||||
'verbose_name_plural': 'configuration parameters',
|
||||
'ordering': ['parameter'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Section',
|
||||
fields=[
|
||||
|
@ -55,7 +42,8 @@ class Migration(migrations.Migration):
|
|||
('position', models.PositiveIntegerField(blank=True, verbose_name='position')),
|
||||
('title', cms.models.VarCharField(blank=True, verbose_name='title')),
|
||||
('color', models.PositiveIntegerField(choices=[(1, 'Wit')], default=1, verbose_name='color')),
|
||||
('content', ckeditor.fields.RichTextField(blank=True, verbose_name='content')),
|
||||
('content', markdownfield.models.MarkdownField(blank=True, verbose_name='content')),
|
||||
('content_rendered', markdownfield.models.RenderedMarkdownField(editable=False)),
|
||||
('image', models.ImageField(blank=True, upload_to='', verbose_name='image')),
|
||||
('video', embed_video.fields.EmbedVideoField(blank=True, help_text='Paste a YouTube, Vimeo, or SoundCloud link', verbose_name='video')),
|
||||
('button_text', cms.models.VarCharField(blank=True, verbose_name='button text')),
|
||||
|
|
|
@ -7,9 +7,10 @@ from django.urls import reverse
|
|||
from django.conf import settings
|
||||
from django.forms import TextInput, Select
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from ckeditor.fields import RichTextField
|
||||
from embed_video.fields import EmbedVideoField
|
||||
from polymorphic.models import PolymorphicModel
|
||||
from markdownfield.models import MarkdownField, RenderedMarkdownField
|
||||
from markdownfield.validators import VALIDATOR_NULL
|
||||
|
||||
from numberedmodel.models import NumberedModel
|
||||
|
||||
|
@ -27,9 +28,9 @@ class VarCharChoiceField(models.TextField):
|
|||
|
||||
class BasePage(NumberedModel):
|
||||
'''Abstract base model for pages'''
|
||||
position = models.PositiveIntegerField(_('position'), blank=True)
|
||||
title = VarCharField(_('title'))
|
||||
slug = models.SlugField(_('slug'), help_text=_('A short identifier to use in URLs'), blank=True, unique=True)
|
||||
title = VarCharField(_('title'))
|
||||
position = models.PositiveIntegerField(_('position'), blank=True)
|
||||
menu = models.BooleanField(_('visible in menu'), default=True)
|
||||
|
||||
def __str__(self):
|
||||
|
@ -52,11 +53,12 @@ class BaseSection(NumberedModel, PolymorphicModel):
|
|||
'''Abstract base model for sections'''
|
||||
TYPES = []
|
||||
page = models.ForeignKey(swapper.get_model_name('cms', 'Page'), verbose_name=_('page'), related_name='sections', on_delete=models.PROTECT)
|
||||
type = VarCharChoiceField(_('section type'), default='', choices=TYPES)
|
||||
position = models.PositiveIntegerField(_('position'), blank=True)
|
||||
type = VarCharChoiceField(_('type'), default='', choices=TYPES)
|
||||
title = VarCharField(_('title'), blank=True)
|
||||
position = models.PositiveIntegerField(_('position'), blank=True)
|
||||
color = models.PositiveIntegerField(_('color'), default=1, choices=settings.SECTION_COLORS)
|
||||
content = RichTextField(_('content'), blank=True)
|
||||
content = MarkdownField(_('content'), rendered_field='content_rendered', validator=VALIDATOR_NULL, use_admin_editor=False, blank=True)
|
||||
content_rendered = RenderedMarkdownField()
|
||||
image = models.ImageField(_('image'), blank=True)
|
||||
video = EmbedVideoField(_('video'), blank=True, help_text=_('Paste a YouTube, Vimeo, or SoundCloud link'))
|
||||
button_text = VarCharField(_('button text'), blank=True)
|
||||
|
@ -88,19 +90,3 @@ class Section(BaseSection):
|
|||
'''Swappable section model'''
|
||||
class Meta(BaseSection.Meta):
|
||||
swappable = swapper.swappable_setting('cms', 'Section')
|
||||
|
||||
class Config(models.Model):
|
||||
TYPES = [
|
||||
(10, _('Footer')),
|
||||
]
|
||||
|
||||
parameter = models.PositiveIntegerField(choices=TYPES, unique=True)
|
||||
content = RichTextField(_('content'), blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return "{}. {}".format(self.parameter, self.get_parameter_display())
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('configuration parameter')
|
||||
verbose_name_plural = _('configuration parameters')
|
||||
ordering = ['parameter']
|
||||
|
|
|
@ -5,6 +5,7 @@ $blue: #3573a8;
|
|||
|
||||
html, body {
|
||||
font-family: $font;
|
||||
line-height: 1.33;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -17,6 +18,16 @@ a {
|
|||
}
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
th, td {
|
||||
padding: 1em;
|
||||
}
|
||||
th {
|
||||
border-bottom: 2px solid black;
|
||||
}
|
||||
}
|
||||
|
||||
a.button, button.button {
|
||||
cursor: pointer;
|
||||
font-family: sans-serif;
|
||||
|
@ -248,6 +259,10 @@ section.contactsection {
|
|||
/* Form elements */
|
||||
|
||||
form.cms {
|
||||
div.wrapper {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
div.global_error {
|
||||
border: 2px dotted red;
|
||||
padding: 10px;
|
||||
|
@ -263,7 +278,6 @@ form.cms {
|
|||
border: 0.5px solid black;
|
||||
border-radius: 3px;
|
||||
|
||||
|
||||
legend {
|
||||
font-size: 1.15em;
|
||||
}
|
||||
|
@ -272,10 +286,26 @@ form.cms {
|
|||
div.formfield {
|
||||
margin: 5px 0;
|
||||
padding: 10px 0;
|
||||
font-size: 0;
|
||||
clear: both;
|
||||
box-sizing: border-box;
|
||||
|
||||
&#type, &#position, &#title, &#slug {
|
||||
padding: 0 1em;
|
||||
clear: none;
|
||||
float: left;
|
||||
display: inline-block;
|
||||
width: 33%;
|
||||
}
|
||||
&#type, &#slug {
|
||||
padding-left: 0;
|
||||
}
|
||||
&#title {
|
||||
padding: 0;
|
||||
}
|
||||
&#position {
|
||||
float: right;
|
||||
padding-right: 0;
|
||||
}
|
||||
div.formfield > * {
|
||||
font-size: 1rem;
|
||||
}
|
||||
div.formfield.error {
|
||||
border: 2px dotted red;
|
||||
|
@ -323,13 +353,8 @@ form.cms {
|
|||
margin: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
div.django-ckeditor-widget {
|
||||
display: block !important;
|
||||
}
|
||||
div.cke_chrome {
|
||||
box-sizing: border-box !important;
|
||||
border: 1px solid #aaa !important;
|
||||
textarea {
|
||||
height: 25em;
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
html, body {
|
||||
font-family: sans-serif;
|
||||
line-height: 1.33;
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
|
||||
|
@ -9,6 +10,13 @@ a {
|
|||
a:hover {
|
||||
text-decoration: underline; }
|
||||
|
||||
table {
|
||||
border-collapse: collapse; }
|
||||
table th, table td {
|
||||
padding: 1em; }
|
||||
table th {
|
||||
border-bottom: 2px solid black; }
|
||||
|
||||
a.button, button.button {
|
||||
cursor: pointer;
|
||||
font-family: sans-serif;
|
||||
|
@ -174,6 +182,9 @@ section.contactsection textarea {
|
|||
font-family: inherit; }
|
||||
|
||||
/* Form elements */
|
||||
form.cms div.wrapper {
|
||||
overflow: hidden; }
|
||||
|
||||
form.cms div.global_error {
|
||||
border: 2px dotted red;
|
||||
padding: 10px;
|
||||
|
@ -193,10 +204,21 @@ form.cms fieldset {
|
|||
form.cms div.formfield {
|
||||
margin: 5px 0;
|
||||
padding: 10px 0;
|
||||
font-size: 0; }
|
||||
|
||||
form.cms div.formfield > * {
|
||||
font-size: 1rem; }
|
||||
clear: both;
|
||||
box-sizing: border-box; }
|
||||
form.cms div.formfield#type, form.cms div.formfield#position, form.cms div.formfield#title, form.cms div.formfield#slug {
|
||||
padding: 0 1em;
|
||||
clear: none;
|
||||
float: left;
|
||||
display: inline-block;
|
||||
width: 33%; }
|
||||
form.cms div.formfield#type, form.cms div.formfield#slug {
|
||||
padding-left: 0; }
|
||||
form.cms div.formfield#title {
|
||||
padding: 0; }
|
||||
form.cms div.formfield#position {
|
||||
float: right;
|
||||
padding-right: 0; }
|
||||
|
||||
form.cms div.formfield.error {
|
||||
border: 2px dotted red;
|
||||
|
@ -237,12 +259,8 @@ form.cms input, form.cms select, form.cms textarea {
|
|||
margin: 0;
|
||||
padding: 5px; }
|
||||
|
||||
form.cms div.django-ckeditor-widget {
|
||||
display: block !important; }
|
||||
|
||||
form.cms div.cke_chrome {
|
||||
box-sizing: border-box !important;
|
||||
border: 1px solid #aaa !important; }
|
||||
form.cms textarea {
|
||||
height: 25em; }
|
||||
|
||||
form.cms input[type=checkbox] {
|
||||
width: auto; }
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -22,7 +22,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{% block formset %}
|
||||
{% if formset %}
|
||||
{{formset.management_form}}
|
||||
{% for form in formset %}
|
||||
{{form.media}}
|
||||
|
@ -48,7 +48,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
|
||||
<div class="edit page">
|
||||
<button>{% trans 'save' %}</button>
|
||||
|
@ -74,6 +74,9 @@
|
|||
slugfield.value = URLify(titlefield.value);
|
||||
}
|
||||
});
|
||||
slugfield.addEventListener('input', function(event) {
|
||||
virgin = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,9 +17,9 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if section.content %}
|
||||
{% if section.content_rendered %}
|
||||
<div class="content">
|
||||
{{section.content|safe}}
|
||||
{{section.content_rendered|safe}}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
|
10
cms/utils.py
10
cms/utils.py
|
@ -1,10 +0,0 @@
|
|||
from .models import Config
|
||||
|
||||
def get_config(parameter):
|
||||
'''Gets or creates the requested parameter.
|
||||
|
||||
'''
|
||||
if parameter not in [t[0] for t in Config.TYPES]:
|
||||
raise ValueError('Invalid configuration parameter requested')
|
||||
(c, created) = Config.objects.get_or_create(parameter=parameter)
|
||||
return c.content
|
|
@ -12,7 +12,6 @@ from django.http import HttpResponseRedirect, HttpResponseBadRequest
|
|||
|
||||
from .decorators import register_view
|
||||
from .forms import PageForm, SectionForm
|
||||
from .utils import get_config
|
||||
|
||||
Page = swapper.load_model('cms', 'Page')
|
||||
Section = swapper.load_model('cms', 'Section')
|
||||
|
@ -59,14 +58,12 @@ class SectionWithFormView(SectionView):
|
|||
return section
|
||||
|
||||
class MenuMixin:
|
||||
'''Add pages and footer to template context'''
|
||||
'''Add pages to template context'''
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
pages = Page.objects.filter(menu=True)
|
||||
footer = get_config(10)
|
||||
context.update({
|
||||
'pages': pages,
|
||||
'footer': footer,
|
||||
})
|
||||
return context
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
# Generated by Django 3.0.1 on 2020-01-02 18:31
|
||||
# Generated by Django 3.0.1 on 2020-01-02 20:42
|
||||
|
||||
import ckeditor.fields
|
||||
import cms.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import embed_video.fields
|
||||
import markdownfield.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -13,8 +13,8 @@ class Migration(migrations.Migration):
|
|||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
migrations.swappable_dependency(settings.CMS_PAGE_MODEL),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
@ -42,7 +42,8 @@ class Migration(migrations.Migration):
|
|||
('position', models.PositiveIntegerField(blank=True, verbose_name='position')),
|
||||
('title', cms.models.VarCharField(blank=True, verbose_name='title')),
|
||||
('color', models.PositiveIntegerField(choices=[(1, 'Wit')], default=1, verbose_name='color')),
|
||||
('content', ckeditor.fields.RichTextField(blank=True, verbose_name='content')),
|
||||
('content', markdownfield.models.MarkdownField(blank=True, verbose_name='content')),
|
||||
('content_rendered', markdownfield.models.RenderedMarkdownField(editable=False)),
|
||||
('image', models.ImageField(blank=True, upload_to='', verbose_name='image')),
|
||||
('video', embed_video.fields.EmbedVideoField(blank=True, help_text='Paste a YouTube, Vimeo, or SoundCloud link', verbose_name='video')),
|
||||
('button_text', cms.models.VarCharField(blank=True, verbose_name='button text')),
|
||||
|
|
|
@ -7,6 +7,7 @@ except ImportError:
|
|||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
|
||||
PROJECT_NAME = 'example'
|
||||
SITE_URL = 'https://example.com/'
|
||||
KEYFILE = f'/tmp/{PROJECT_NAME}.secret'
|
||||
ADMINS = [('JJ Vens', 'jj@rtts.eu')]
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
@ -23,6 +24,8 @@ MEDIA_URL = '/media/'
|
|||
MEDIA_ROOT = '/srv/' + PROJECT_NAME + '/media'
|
||||
LOGIN_REDIRECT_URL = '/'
|
||||
CMS_SECTION_MODEL = 'app.Section'
|
||||
MARKDOWN_EXTENSIONS = ['extra', 'smarty']
|
||||
MARKDOWN_EXTENSION_CONFIGS = {'extra': {}, 'smarty': {}}
|
||||
|
||||
def read(file):
|
||||
with open(file) as f:
|
||||
|
@ -36,34 +39,10 @@ except IOError:
|
|||
SECRET_KEY = ''.join(random.choice(string.printable) for x in range(50))
|
||||
write(KEYFILE, SECRET_KEY)
|
||||
|
||||
SECTION_TYPES = [
|
||||
('TextSection', 'Tekst'),
|
||||
('ImageSection', 'Afbeelding'),
|
||||
]
|
||||
|
||||
SECTION_COLORS = [
|
||||
(1, 'Wit'),
|
||||
]
|
||||
|
||||
CKEDITOR_CONFIGS = {
|
||||
'default': {
|
||||
'removePlugins': 'elementspath',
|
||||
'extraPlugins': 'format',
|
||||
'width': '100%',
|
||||
'toolbar': 'Custom',
|
||||
# 'contentsCss': STATIC_URL + 'ckeditor.css',
|
||||
# 'allowedContent': True, # this allows iframes, embeds, scripts, etc...
|
||||
'toolbar_Custom': [
|
||||
['Format'],
|
||||
['Bold', 'Italic', 'Underline', 'TextColor'],
|
||||
['NumberedList', 'BulletedList', 'Blockquote'],
|
||||
['JustifyLeft', 'JustifyCenter', 'JustifyRight'],
|
||||
['Link', 'Unlink'],
|
||||
['RemoveFormat', 'Source'],
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'app',
|
||||
'django.contrib.admin',
|
||||
|
@ -74,7 +53,6 @@ INSTALLED_APPS = [
|
|||
'django.contrib.staticfiles',
|
||||
'cms',
|
||||
'simplesass',
|
||||
'ckeditor',
|
||||
'polymorphic',
|
||||
'embed_video',
|
||||
'easy_thumbnails',
|
||||
|
|
5
setup.py
5
setup.py
|
@ -3,7 +3,7 @@ from setuptools import setup, find_packages
|
|||
|
||||
setup(
|
||||
name = 'django-simplecms',
|
||||
version = '2.1.0',
|
||||
version = '2.2.0',
|
||||
url = 'https://github.com/rtts/django-simplecms',
|
||||
author = 'Jaap Joris Vens',
|
||||
author_email = 'jj@rtts.eu',
|
||||
|
@ -13,12 +13,13 @@ setup(
|
|||
include_package_data = True,
|
||||
install_requires = [
|
||||
'django',
|
||||
'django-ckeditor',
|
||||
'django-extensions',
|
||||
'django-embed-video',
|
||||
'django-polymorphic',
|
||||
'django-markdownfield',
|
||||
'easy-thumbnails',
|
||||
'psycopg2',
|
||||
'markdown',
|
||||
'libsass',
|
||||
'swapper',
|
||||
],
|
||||
|
|
Ładowanie…
Reference in New Issue