--- /dev/null
+# See https://pre-commit.com for more information
+# See https://pre-commit.com/hooks.html for more hooks
+repos:
+- repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.4.0
+ hooks:
+ - id: double-quote-string-fixer
+- repo: https://github.com/psf/black
+ rev: 23.1.0
+ hooks:
+ - id: black
+ args: ['--target-version', 'py37', '--skip-string-normalization', '--line-length', '110']
+- repo: https://github.com/PyCQA/isort
+ rev: 5.12.0
+ hooks:
+ - id: isort
+ args: ['--profile', 'black', '--line-length', '110']
+- repo: https://github.com/asottile/pyupgrade
+ rev: v3.3.1
+ hooks:
+ - id: pyupgrade
+ args: ['--keep-percent-format', '--py37-plus']
+- repo: https://github.com/adamchainz/django-upgrade
+ rev: 1.13.0
+ hooks:
+ - id: django-upgrade
+ args: ['--target-version', '3.2']
+- repo: https://github.com/rtts/djhtml
+ rev: '3.0.5'
+ hooks:
+ - id: djhtml
+ args: ['--tabwidth', '2']
+- repo: https://git.entrouvert.org/pre-commit-debian.git
+ rev: v0.3
+ hooks:
+ - id: pre-commit-debian
include COPYING
include MANIFEST.in
-recursive-include chloro/phyll/static *.css *.scss *.js *.json *.png
+recursive-include chloro/locale *.po *.mo
+recursive-include chloro/phyll/static *.css *.scss *.js *.json *.png *.jpg
recursive-include chloro/phyll/templates *.html
+recursive-include chloro/rdio/static *.css *.scss *.js *.json *.png *.jpg
+recursive-include chloro/rdio/templates *.html
--- /dev/null
+# Chloro French Translation.
+# Copyright (C) 2023 Frederic Peters
+# This file is distributed under the same license as the chloro package.
+# Frederic Peters <fpeters@0d.be>, 2023.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: chloro 0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2023-10-21 11:06+0200\n"
+"PO-Revision-Date: 2023-10-21 11:06+0200\n"
+"Last-Translator: Frederic Peters <fpeters@0d.be>\n"
+"Language: French\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: chloro/phyll/models.py
+msgid "Title"
+msgstr "Titre"
+
+#: chloro/phyll/models.py
+msgid "Slug"
+msgstr "Slug"
+
+#: chloro/phyll/models.py
+msgid "Text"
+msgstr "Texte"
+
+#: chloro/phyll/models.py
+msgid "Tags"
+msgstr "Tags"
+
+#: chloro/phyll/models.py
+msgid "Published"
+msgstr "Publié"
+
+#: chloro/phyll/templates/phyll/base.html
+msgid "New Note"
+msgstr "Nouveau billet"
+
+#: chloro/phyll/templates/phyll/note_confirm_delete.html
+msgid "Delete?"
+msgstr "Supprimer ?"
+
+#: chloro/phyll/templates/phyll/note_confirm_delete.html
+#: chloro/phyll/templates/phyll/note_detail.html
+#: chloro/phyll/templates/phyll/note_list.html
+#: chloro/rdio/templates/phyll/note_detail.html
+msgid "Delete"
+msgstr "Supprimer"
+
+#: chloro/phyll/templates/phyll/note_detail.html
+#: chloro/phyll/templates/phyll/note_list.html
+#: chloro/rdio/templates/phyll/note_detail.html
+msgid "Edit"
+msgstr "Modifier"
+
+#: chloro/phyll/templates/phyll/note_form.html
+msgid "Submit"
+msgstr "Valider"
+
+#: chloro/phyll/templates/registration/login.html
+msgid "Login"
+msgstr "Connexion"
+
+#: chloro/rdio/templates/phyll/base.html
+msgid "New Page"
+msgstr "Nouvelle page"
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-from django.core.urlresolvers import reverse
+import ckeditor.views
+import ckeditor.widgets
+from ckeditor.image import pillow_backend
from django.forms.utils import flatatt
from django.template.loader import render_to_string
-from django.utils.encoding import force_text
+from django.urls import reverse
+from django.utils.encoding import force_str
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe
from django.utils.translation import get_language
-import ckeditor.views
-import ckeditor.widgets
-from ckeditor.image import pillow_backend
-
-def ckeditor_render(self, name, value, attrs=None):
+def ckeditor_render(self, name, value, attrs=None, renderer=None):
if value is None:
value = ''
final_attrs = {'name': name}
# Force to text to evaluate possible lazy objects
external_plugin_resources = [
- [force_text(a), force_text(b), force_text(c)] for a, b, c in self.external_plugin_resources
+ [force_str(a), force_str(b), force_str(c)] for a, b, c in self.external_plugin_resources
]
return mark_safe(
'ckeditor/widget.html',
{
'final_attrs': flatatt(final_attrs),
- 'value': conditional_escape(force_text(value)),
+ 'value': conditional_escape(force_str(value)),
'id': final_attrs['id'],
'config': ckeditor.widgets.json_encode(self.config),
'external_plugin_resources': ckeditor.widgets.json_encode(external_plugin_resources),
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-default_app_config = 'chloro.phyll.apps.AppConfig'
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-from django.conf import settings
-
import ckeditor.fields
+from django.conf import settings
class RichTextField(ckeditor.fields.RichTextField):
'external_plugin_resources': self.external_plugin_resources,
}
defaults.update(kwargs)
- return super(RichTextField, self).formfield(**defaults)
+ return super().formfield(**defaults)
class RichTextFormField(ckeditor.fields.RichTextFormField):
def clean(self, value):
- value = super(RichTextFormField, self).clean(value)
+ value = super().clean(value)
if settings.LANGUAGE_CODE.startswith('fr-'):
# apply some basic typographic rules
value = value.replace('« ', '«\u202f')
--- /dev/null
+# chloro - personal space
+# Copyright (C) 2019-2023 Frederic Peters
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from django.core.management.commands import makemessages
+
+
+class Command(makemessages.Command):
+ xgettext_options = makemessages.Command.xgettext_options + ['--keyword=N_']
+
+ def add_arguments(self, parser):
+ super().add_arguments(parser)
+ parser.add_argument('--keep-obsolete', action='store_true', help='Keep obsolete message strings.')
+
+ def handle(self, *args, **options):
+ if not options.get('add_location') and self.gettext_version >= (0, 19):
+ options['add_location'] = 'file'
+ options['no_obsolete'] = not (options.get('keep_obsolete'))
+ options['ignore_patterns'].append('debian')
+ return super().handle(*args, **options)
--- /dev/null
+# chloro - personal space
+# Copyright (C) 2019-2022 Frederic Peters
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+from django.core.management.base import BaseCommand
+
+from chloro.phyll.models import Note
+
+
+class Command(BaseCommand):
+ def handle(self, verbosity, **options):
+ # recreate plain text version and interlinks
+ for note in Note.objects.all():
+ note.save()
-# -*- coding: utf-8 -*-
# Generated by Django 1.11.17 on 2019-12-29 13:29
-from __future__ import unicode_literals
-import chloro.phyll.fields
-from django.db import migrations, models
import taggit.managers
+from django.db import migrations, models
+import chloro.phyll.fields
-class Migration(migrations.Migration):
+class Migration(migrations.Migration):
initial = True
dependencies = [
-# -*- coding: utf-8 -*-
# Generated by Django 1.11.17 on 2019-12-29 18:32
-from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
-
dependencies = [
('phyll', '0001_initial'),
]
operations = [
- migrations.AlterModelOptions(name='note', options={'ordering': ['-creation_timestamp']},),
+ migrations.AlterModelOptions(
+ name='note',
+ options={'ordering': ['-creation_timestamp']},
+ ),
]
-# -*- coding: utf-8 -*-
# Generated by Django 1.11.17 on 2019-12-29 18:39
-from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
-
dependencies = [
('phyll', '0002_auto_20191229_1932'),
]
--- /dev/null
+# Generated by Django 3.2.13 on 2022-07-21 20:57
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('phyll', '0003_note_published'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='note',
+ name='plain_text',
+ field=models.TextField(blank=True, null=True),
+ ),
+ ]
--- /dev/null
+# Generated by Django 3.2.13 on 2022-07-21 20:57
+
+import html
+
+from django.db import migrations
+from django.utils.html import strip_tags
+
+
+def set_plain_text(apps, schema_editor):
+ Note = apps.get_model('phyll', 'Note')
+ for note in Note.objects.all():
+ note.plain_text = html.unescape(strip_tags(note.text))
+ note.save(update_fields=['plain_text'])
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('phyll', '0004_note_plain_text'),
+ ]
+
+ operations = [
+ migrations.RunPython(set_plain_text, reverse_code=migrations.RunPython.noop),
+ ]
--- /dev/null
+# Generated by Django 3.2.13 on 2022-07-24 15:28
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('phyll', '0005_set_plain_text'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Interlink',
+ fields=[
+ (
+ 'id',
+ models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
+ ),
+ (
+ 'note1',
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name='+',
+ to='phyll.note',
+ ),
+ ),
+ (
+ 'note2',
+ models.ForeignKey(
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name='linking_note',
+ to='phyll.note',
+ ),
+ ),
+ ],
+ ),
+ ]
--- /dev/null
+# Generated by Django 3.2.16 on 2023-12-16 12:03
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('phyll', '0006_interlink'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='note',
+ name='included_in_feed',
+ field=models.BooleanField(default=True, verbose_name='Include in feed'),
+ ),
+ ]
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import html
+import re
+import urllib.parse
+
from django.db import models, transaction
-from django.utils.translation import ugettext_lazy as _
+from django.utils.html import strip_tags
+from django.utils.translation import gettext_lazy as _
from taggit.managers import TaggableManager
from .fields import RichTextField
title = models.CharField(_('Title'), max_length=150)
slug = models.SlugField(_('Slug'), max_length=150)
text = RichTextField(_('Text'), blank=True, null=True)
+ plain_text = models.TextField(blank=True, null=True)
tags = TaggableManager(_('Tags'), blank=True)
+ included_in_feed = models.BooleanField(_('Include in feed'), default=True)
published = models.BooleanField(_('Published'), default=True)
creation_timestamp = models.DateTimeField(auto_now_add=True)
last_update_timestamp = models.DateTimeField(auto_now=True)
ordering = ['-creation_timestamp']
def get_absolute_url(self):
+ if self.slug == 'index':
+ return '/'
return '/%s/' % self.slug
+
+ def lang(self):
+ if self.tags.filter(name='lang-en').exists():
+ return 'en'
+ return 'fr'
+
+ def save(self, *args, **kwargs):
+ if kwargs.get('update_fields') is None or 'plain_text' in kwargs.get('update_fields'):
+ self.plain_text = re.sub(
+ r"[’'\"«»,;:\. ]", ' ', html.unescape(strip_tags(self.text)).replace('’', ' ')
+ )
+ if not kwargs.get('update_fields'):
+ self.maintain_interlinks()
+ return super().save(*args, **kwargs)
+
+ def maintain_interlinks(self):
+ links = re.findall(r'href="(.*?)"', self.text or '')
+ with transaction.atomic():
+ Interlink.objects.filter(note1=self).delete()
+ for link in links:
+ parsed = urllib.parse.urlparse(link)
+ if parsed.netloc != '':
+ continue
+ path_parts = parsed.path.strip('/').split()
+ if len(path_parts) != 1:
+ continue
+ try:
+ note2 = Note.objects.get(slug=path_parts[0])
+ Interlink.objects.create(note1=self, note2=note2)
+ except Note.DoesNotExist:
+ pass
+
+
+class Interlink(models.Model):
+ note1 = models.ForeignKey(Note, on_delete=models.SET_NULL, null=True, related_name='+')
+ note2 = models.ForeignKey(Note, on_delete=models.SET_NULL, null=True, related_name='linking_note')
@charset "UTF-8";
-$text-color: #222;
-$link-color: #0000ee;
-$visited-link-color: #551a8b;
+html {
+ --body-background: white;
+ --header-background: #3c113e;
+ --header-color: white;
+ --text-background: white;
+ --text-color: #222;
+ --light-text-color: lighten(#222, 20%);
+ --lighter-text-color: lighten(#222, 30%);
+ --link-color: #0000ee;
+ --visited-link-color: #551a8b;
+ --gray: gray;
+ --note-background: #fbf7c1;
+ --blockquote-background: #eeeefd;
+}
+
+@media (prefers-color-scheme: dark) {
+ html {
+ --body-background: #113;
+ --header-background: black;
+ --text-background: #113;
+ --text-color: #eef;
+ --light-text-color: lighten(#eef, 20%);
+ --lighter-text-color: lighten(#eef2, 30%);
+ --link-color: #aaf;
+ --visited-link-color: #b767ff;
+ --gray: gray;
+ --note-background: #867f11;
+ --blockquote-background: #223;
+ }
+}
@import "opensans";
body {
font-family: "Open Sans", sans-serif;
- background: white;
- color: $text-color;
+ background: var(--body-background);
+ color: var(--text-color);
font-size: 100%;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ @media screen and (max-width: 50em) {
+ flex-direction: column;
+ }
}
a {
- color: $link-color;
+ color: var(--link-color);
text-decoration: none;
- border-bottom: 0.1em dotted $link-color;
+ border-bottom: 0.1em dotted var(--link-color);
&:visited {
- color: $visited-link-color;
- border-color: $visited-link-color;
+ color: var(--visited-link-color);
+ border-color: var(--visited-link-color);
}
&:hover {
border-bottom-style: solid;
}
header {
+ background: var(--header-background);
+ color: var(--header-color);
display: inline-block;
- h1 {
+ .header-title {
+ font-size: 2em;
font-weight: normal;
text-transform: uppercase;
- margin: 1em 0 0 1em;
+ margin: 4rem 1rem 2rem 1rem;
padding: 0 0.5em;
- color: lighten($text-color, 20%);
- background: #fafaff;
display: inline-block;
- a {
- border: none;
+ transform: rotate(-2deg);
+ }
+ a {
+ color: inherit;
+ border-color: var(--header-color);
+ &:visited {
+ color: inherit;
+ border-color: inherit;
}
}
- transform: rotate(-2deg);
position: relative;
z-index: 2;
+ max-width: 20em;
+ min-height: 100vh;
+ p.contact {
+ margin: 1rem 2rem;
+ }
+ nav {
+ font-weight: bold;
+ ul {
+ margin: 1rem 2rem;
+ padding: 0;
+ list-style: none;
+ li {
+ margin: 0;
+ padding: 0;
+ }
+ }
+ span {
+ display: block;
+ font-size: 90%;
+ font-weight: normal;
+ }
+ }
+ .about {
+ margin-top: 1em;
+ }
+ @media screen and (max-width: 50em) {
+ .header-title {
+ margin-top: 1em;
+ }
+ max-width: inherit;
+ min-height: inherit;
+ nav {
+ ul {
+ margin-top: 0;
+ display: flex;
+ flex-wrap: wrap;
+ list-style: none;
+ gap: 1em;
+ }
+ span {
+ display: none;
+ }
+ }
+ .about {
+ margin-top: 0;
+ }
+ }
}
div.actions {
position: absolute;
top: 0.5rem;
- left: 1rem;
+ right: 1rem;
z-index: 100;
a {
+ background: white;
+ padding: 2px;
+ border: 1px solid black;
+ border-radius: 3px;
+ color: black;
margin-right: 1rem;
}
}
margin: 0 4em;
padding: 1em;
max-width: 70em;
+ @media screen and (max-width: 50em) {
+ margin: 0;
+ }
}
main {
position: relative;
- background: #fafaff;
+ background: var(--text-background);
min-height: 70vh;
- clip-path: polygon(0px 0px, 96.35% -24px, 101.99% 7.86%, 100.61% 103.56%, 10% 100%, 0% 100%, 0px 0px);
+ flex: 1;
}
-.home {
- nav a {
- text-transform: uppercase;
- display: inline-block;
- text-decoration: none;
- font-size: 500%;
- font-weight: bold;
- padding-top: 0.5em;
- line-height: 50%;
- width: 100%;
- span {
- font-size: 30%;
- }
- margin-bottom: 1rem;
- &.divers {
- width: 49%;
- }
- &.vrac {
- width: 49%;
- text-align: right;
- float: right;
+article {
+ margin-top: 2em;
+ max-width: 50em;
+ h1 {
+ margin: 0;
+ font-size: 400%;
+ @media screen and (max-width: 50em) {
+ font-size: 200%;
}
+ font-weight: bold;
+ text-transform: uppercase;
}
- .latest {
- margin-top: 2em;
- h2 {
- margin: 0;
- font-size: 400%;
- font-weight: bold;
- text-transform: uppercase;
- }
- .meta {
- padding-top: 0.5rem;
- padding-left: 0.5rem;
- color: lighten($text-color, 40%);
- }
+ .meta {
+ padding-top: 0.5rem;
+ padding-left: 0.5rem;
+ color: var(--lighter-text-color);
}
-}
-
-.post {
- max-width: 50em;
div.figure {
text-align: center;
margin: 1em 0;
img {
- border: 1px solid gray;
+ border: 1px solid var(--gray);
padding: 3px;
max-width: 90%;
max-height: 70vh;
background: #111;
color: white;
padding: 2px;
- }
- div.meta {
- margin-top: 3em;
- color: lighten($text-color, 40%);
+ overflow: auto;
}
div.note {
- background: #fbf7c1;;
+ background: var(--note-background);
padding: 0.2em 0.5em 0.2em 2em;
p {
margin: 0.5em 0;
}
}
blockquote {
- background: #eeeefd;
+ background: var(--blockquote-background);
padding: 0.1em 1em;
clip-path: polygon(0px 0px, 94.27% 2px, 99.63% 2.65%, 98.39% 97.48%, 10% 100%, 0% 100%, 0px 0px);
}
}
.post-list {
+ margin-top: 2em;
+ h1 {
+ margin: 0;
+ font-size: 400%;
+ @media screen and (max-width: 50em) {
+ font-size: 200%;
+ }
+ font-weight: bold;
+ text-transform: uppercase;
+ }
ul, li {
margin: 0;
padding: 0;
.older.post-list {
margin: 5em 0 2em 0;
padding: 1em 0;
- border-top: 0.1em dotted $link-color;
- border-bottom: 0.1em dotted $link-color;
+ border-top: 0.1em dotted var(--link-color);
+ border-bottom: 0.1em dotted var(--link-color);
line-height: 200%;
li {
display: inline;
}
div[contenteditable=true]:focus-within {
- outline: 1px solid gray;
+ outline: 1px solid var(--gray);
outline-offset: 3px;
}
}
}
-button#save {
+#quickedit {
position: sticky;
bottom: 10px;
+ display: flex;
+ justify-content: flex-end;
+ label {
+ display: inline-block;
+ margin-left: 6px;
+ cursor: pointer;
+ }
+ input {
+ display: none;
+ }
+
+ button,
+ span {
+ display: inline-block;
+ font-weight: normal;
+ background: white;
+ border: 1px solid #386ede;
+ box-shadow: 0 0 0 5px white;
+ padding: 1ex;
+ border-radius: 3px;
+ color: #386ede;
+ font-size: inherit;
+ letter-spacing: inherit;
+ line-height: inherit;
+ }
+ button:hover {
+ &:hover {
+ color: white;
+ background: #386ede;
+ }
+ }
+ button.error {
+ background: red;
+ color: white;
+ &:hover {
+ background: darken(red, 10%);
+ }
+ }
+
+ input:checked + span {
+ background: #386ede;
+ color: white;
+ }
}
main.post {
}
}
}
+
+.wiki-anchor-auto {
+ display: none;
+}
+
+#image-upload, #document-upload {
+ display: none;
+}
+// from django/contrib/admin/static/admin/js/urlify.js
+var LATIN_MAP = {
+ 'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Ä': 'A', 'Å': 'A', 'Æ': 'AE',
+ 'Ç': 'C', 'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E', 'Ì': 'I', 'Í': 'I',
+ 'Î': 'I', 'Ï': 'I', 'Ð': 'D', 'Ñ': 'N', 'Ò': 'O', 'Ó': 'O', 'Ô': 'O',
+ 'Õ': 'O', 'Ö': 'O', 'Ő': 'O', 'Ø': 'O', 'Ù': 'U', 'Ú': 'U', 'Û': 'U',
+ 'Ü': 'U', 'Ű': 'U', 'Ý': 'Y', 'Þ': 'TH', 'Ÿ': 'Y', 'ß': 'ss', 'à': 'a',
+ 'á': 'a', 'â': 'a', 'ã': 'a', 'ä': 'a', 'å': 'a', 'æ': 'ae', 'ç': 'c',
+ 'è': 'e', 'é': 'e', 'ê': 'e', 'ë': 'e', 'ì': 'i', 'í': 'i', 'î': 'i',
+ 'ï': 'i', 'ð': 'd', 'ñ': 'n', 'ò': 'o', 'ó': 'o', 'ô': 'o', 'õ': 'o',
+ 'ö': 'o', 'ő': 'o', 'ø': 'o', 'ù': 'u', 'ú': 'u', 'û': 'u', 'ü': 'u',
+ 'ű': 'u', 'ý': 'y', 'þ': 'th', 'ÿ': 'y'
+};
+
+function downcode(string) {
+ return string.toLowerCase().replace(/[^A-Za-z0-9\[\] ]/g,function(a){ return LATIN_MAP[a]||a }).replace(/[^-\w\s]/g, '').replace(/^\s+|\s+$/g, '').replace(/[-\s]+/g, '-');
+};
+
+function remove_auto_anchors() {
+ $('article .wiki-anchor-auto').each(function(idx, anchor) {
+ $(anchor).parent().removeAttr('id');
+ $(anchor).remove();
+ });
+}
+
+function auto_anchors() {
+ $('article h2, article h3, article h4').each(function(idx, elem) {
+ var $elem = $(elem);
+ if ($elem.attr('id')) return;
+ if ($elem.find('.wiki-anchor').length) return;
+ $elem.attr('id', downcode($elem.text()));
+ $('<a class="wiki-anchor wiki-anchor-auto" href="#' + $elem.attr('id') + '">¶</a>').appendTo($elem);
+ });
+}
+
+function create_toc() {
+ $('#toc').remove();
+ if ($('article h2').length == 0) return;
+ $div_toc = $('<div id="toc"><ul></ul></div>');
+ $div_toc_ul = $div_toc.find('ul');
+ var li_titles = Array();
+ $('article h2').each(function(idx, elem) {
+ var $elem = $(elem);
+ var slug = elem.id;
+ var $a_title = $('<a></a>', {href: '#' + slug, text: $elem.text().replace(/¶$/, '')});
+ var $li_title = $('<li></li>');
+ $li_title[0].related_position = $(elem).position().top;
+ li_titles.push($li_title[0]);
+ $a_title.appendTo($li_title);
+ $li_title.appendTo($div_toc_ul);
+ });
+ $('article h1').first().after($div_toc);
+
+ li_titles = li_titles.reverse();
+
+ $(window).on('load', function() {
+ // update positions after images have been loaded
+ $('article h2').each(function(idx, elem) {
+ $('#toc li')[idx].related_position = $(elem).position().top;
+ });
+ $(window).trigger('scroll');
+ });
+
+ var scroll_timeout_id = null;
+ $(window).on('scroll', function() {
+ if (scroll_timeout_id) clearTimeout(scroll_timeout_id);
+ scroll_timeout_id = setTimeout(function() { // throttle
+ scroll_timeout_id = null;
+ var current_position = window.scrollY;
+ $('#toc li').removeClass('active');
+ for (const li_title of li_titles) {
+ if (li_title.related_position < current_position - 25) {
+ $(li_title).addClass('active');
+ break;
+ }
+ }
+ }, 50);
+ });
+};
+
(function(window, document, undefined) {
var Phylly = {
BLOCKS: [
+ {name: 'intertitle', tag: 'H2', klass: 'intertitle'},
{name: 'code', tag: 'PRE', klass: 'screen'},
+ {name: 'list', special: 'list', tag: 'UL', klass: 'list'},
{name: 'figure', special: 'img', tag: 'DIV', subtag: true, klass: 'figure'},
{name: 'note', tag: 'DIV', subtag: true, klass: 'note'},
{name: 'quote', tag: 'BLOCKQUOTE', subtag: true, klass: 'quote'},
} else if (inline_style_toolbar) {
$(inline_style_toolbar).hide();
}
+ if ($(sel.anchorNode).is('div.figure') && $(sel.anchorNode).find('img').length) {
+ show_figure_toolbar(sel);
+ } else if ($(sel.anchorNode).parents('.figure-toolbar').length == 0) {
+ $(figure_toolbar).hide();
+ }
});
var $image_upload = $('<input type="file" nam="image" id="image-upload">');
$image_upload.on('change', upload_image);
var file = $(this).prop('files')[0];
var params = new FormData();
params.append('upload', file);
- $.post({url: '/ajax/upload/', processData: false, data: params, contentType: false}).success(function(data) {
+ $.post({url: '/ajax/upload/', processData: false, data: params, contentType: false}).done(function(data) {
var img = document.createElement('IMG');
img.src = data.url;
if (data.orig_url) {
var file = $(this).prop('files')[0];
var params = new FormData();
params.append('upload', file);
- $.post({url: '/ajax/upload/', processData: false, data: params, contentType: false}).success(function(data) {
+ $.post({url: '/ajax/upload/', processData: false, data: params, contentType: false}).done(function(data) {
var doc_link = document.createElement('A');
doc_link.className = 'button';
doc_link.textContent = 'Télécharger ' + data.filename;
var params = {};
params.title = text;
params.request_id = request_id;
- $.post('/wiki/ajax/newpage/', params).success(function(data) {
+ $.post('/ajax/newpage/', params).done(function(data) {
$('a[data-request-id=' + data.request_id + ']').attr('href', data.url).removeAttr('data-request-id');
});
param = $new_link[0].outerHTML;
}
if (action == 'createLink') {
var sel = window.getSelection();
+ var selected_link = get_parent(sel.anchorNode, 'A');
+ if (sel.anchorNode.nodeType == Node.TEXT_NODE) {
+ if (sel.anchorNode.length == sel.anchorOffset && sel.anchorNode.nextSibling.nodeName == 'A') {
+ selected_link = sel.anchorNode.nextSibling;
+ }
+ }
var $input = $('input[name=link-target]');
$input[0]._range = sel.getRangeAt(0);
- if (sel.anchorNode instanceof Element) {
- var elem = sel.anchorNode.childNodes[sel.anchorOffset];
- if (elem.tagName == 'A') {
- $input.val(elem.href);
- }
+ if (selected_link) {
+ $input[0]._selected_link = selected_link;
+ $input.val(selected_link.href);
}
$input.addClass('shown');
$input.focus();
$input.removeClass('shown');
var sel = window.getSelection();
sel.addRange(this._range);
+ var selected_link = $input[0]._selected_link;
if (url) {
- document.execCommand('createLink', false, url);
+ if (selected_link) {
+ selected_link.href = url;
+ } else {
+ var $new_link = $('<a></a>', {text: sel.toString(), href: url});
+ this._range.deleteContents();
+ this._range.insertNode($new_link[0]);
+ sel.empty();
+ sel.collapse($new_link[0]);
+ sel.empty();
+ }
} else {
- document.execCommand('unlink', false, null);
+ if (selected_link) {
+ selected_link.replaceWith(document.createTextNode(selected_link.textContent));
+ }
}
- sel.empty();
$input.val('');
+ $input[0]._selected_link = null;
}
}
function focusout_link(ev) {
'<button data-action="bold" data-accel="b"><b>b</b></button>' +
'<button data-action="code" data-accel="<"><></button>' +
'<button data-action="removeFormat" data-accel="m">×</button>' +
+ '<button data-action="wiki">W</button>' +
'<button data-action="createLink">a</button>' +
'<input name="link-target"/>' +
'</div>');
inline_style_toolbar.css('left', pos.left + window.scrollX);
inline_style_toolbar.show();
};
+
+ var figure_toolbar = null;
+ function show_figure_toolbar(sel) {
+ if (figure_toolbar === null) {
+ figure_toolbar = $('<div class="figure-toolbar"><label>Alt: <input name="figure-alt"></label></div>')
+ figure_toolbar.hide();
+ figure_toolbar.insertAfter(document.body);
+ $('[name="figure-alt"]').on('change', function() {
+ $(this.img).attr('alt', $(this).val());
+ });
+ }
+ figure_toolbar.css('position', 'absolute');
+ var pos = sel.getRangeAt(0).getClientRects()[0];
+ figure_toolbar.css('top', pos.bottom + window.scrollY);
+ figure_toolbar.css('left', pos.left + window.scrollX);
+ figure_toolbar.show();
+ $('[name="figure-alt"]')[0].img = $(sel.anchorNode).find('img');
+ $('[name="figure-alt"]').val($('[name="figure-alt"]')[0].img.attr('alt') || '');
+ };
+
}(window, document));
$(function() {
- Phylly.init(),
- $('div[contenteditable]').each(function(i, elem) {Phylly.bind_events(elem)});
- $('#save').on('click', function() {
- var text = $('div[contenteditable]')[0].innerHTML;
- var csrf = $('[name=csrfmiddlewaretoken]').val();
- $.post('api-save/',
- { text: text, csrfmiddlewaretoken: csrf}
- ).fail(function() {
- $('#save').css('background', 'red');
+ $('#quickedit input').on('change', function() {
+ var enable = $(this).is(':checked');
+ if (enable) {
+ remove_auto_anchors();
+ $('div[data-editable]').each(function(i, elem) {
+ $(elem).attr('contenteditable', 'true');
+ var $button = $('<button class="save">Enregistrer</button>');
+ $button[0].div_zone = elem;
+ $button.insertBefore($('#quickedit label'));
+ });
+ Phylly.init(),
+ $('div[data-editable]').each(function(i, elem) {
+ Phylly.bind_events(elem);
+ });
+ $('.save').on('click', function() {
+ var text = $('div[contenteditable]')[0].innerHTML;
+ var csrf = $('[name=csrfmiddlewaretoken]').val();
+ $.post('api-save/',
+ { text: text, csrfmiddlewaretoken: csrf}
+ ).fail(function() {
+ $('.save').addClass('error');
+ }).done(function() {
+ $('.save').removeClass('error');
+ });
+ return false;
+ });
+ } else {
+ auto_anchors();
+ if ($('main.phyll-toc').length) create_toc();
+ Phylly.off(),
+ $('button.save').remove();
+ $('div[data-editable]').each(function(i, elem) {
+ $(elem).attr('contenteditable', 'false');
+ Phylly.unbind_events(elem);
+ });
+ }
+ });
+ $('#quickedit input').trigger('change');
+ if ($('#quickedit input').length == 0) {
+ create_toc();
+ }
+ $('.search-results').empty();
+ $('#search-enable').on('change', function() {
+ if ($(this).is(':checked')) {
+ $('.search-field input').focus();
+ }
+ });
+ $('.search-field').on('submit', function() {
+ var value = $('input[name=q]').val();
+ $.ajax({url: '/ajax/search/', data: {q: value}}).done(function(data) {
+ $('.search-results').empty();
+ for (const hit of data.data) {
+ var $a_hit = $('<a>', {text: hit.title, href: hit.url});
+ var $li_hit = $('<li>');
+ $li_hit.append($a_hit);
+ $('.search-results').append($li_hit);
+ }
});
return false;
});
{% load gadjo i18n %}<!DOCTYPE html>
-<html>
+<html lang="{% block html-lang %}fr{% endblock %}">
<head>
<meta charset="utf-8"/> <!-- 🌱 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="/static/icon.png">
<link rel="manifest" href="/static/manifest.json">
{% if request.user.is_staff %}
- <script src="{% xstatic 'jquery' 'jquery.min.js' %}"></script>
- <script src="/static/js/chloro.js"></script>
+ <script src="{% xstatic 'jquery' 'jquery.min.js' %}"></script>
+ <script src="/static/js/chloro.js"></script>
{% endif %}
{% block bottom-head %}
{% endblock %}
</head>
<body>
<header>
- <h1><a href="/">Coin web de Frédéric Péters</a></h1>
+ <div class="header-title"><a href="/">Coin web de Frédéric Péters</a></div>
+ <nav>
+ <ul>
+ <li><a href="/tag/radio/">Radio <span>(Panik & ailleurs)</span></a></li>
+ <li><a href="/tag/code/">Logiciel libre <span>(Debian, GNOME & ce qui passe)</span></a></li>
+ <li><a class="vrac" href="/archives/">Archives <span>(en vrac)</span></a></li>
+ <li class="about"><a href="/a-propos/">À propos</a></li>
+ </ul>
+ </nav>
+ <p class="contact"><a href="mailto:fpeters@0d.be">fpeters@0d.be</a></p>
</header>
<main class="{% block content-class %}{% endblock %}">
{% block body %}
{% endblock %}
</main>
- <footer>
- <p>Contact : <a href="mailto:fpeters@0d.be">fpeters@0d.be</a></p>
- </footer>
- {% if request.user.is_staff %}
- <div class="actions">
- <a href="/new-note/">{% trans "New Note" %}</a>
- {% block bottom-actions %}
- {% endblock %}
- </div>
- {% endif %}
+ {% if request.user.is_staff %}
+ <div class="actions">
+ <a href="/new-note/">{% trans "New Note" %}</a>
+ {% block bottom-actions %}
+ {% endblock %}
+ </div>
+ {% endif %}
</body>
</html>
{% block body %}
-{% with posts.0 as latest %}
-<div class="latest">
-<h2><a href="{{ latest.get_absolute_url }}">{{ latest.title }}</a></h2>
-<div class="meta">{{ latest.creation_timestamp|date:"j E Y, H:i"|lower }}</div>
-<div class="post">
-{{ latest.text|safe }}
-</div>
-</div>
-{% endwith %}
+ {% with posts.0 as latest %}
+ <article class="latest">
+ <h1><a href="{{ latest.get_absolute_url }}">{{ latest.title }}</a></h1>
+ <div class="meta">{{ latest.creation_timestamp|date:"j E Y, H:i"|lower }}</div>
+ <div class="post">
+ {{ latest.text|safe }}
+ </div>
+ </article>
+ {% endwith %}
-<div class="older post-list">
-<ul>
-{% for post in posts|slice:"1:" %}
-<li><a href="{{ post.get_absolute_url }}">{{ post.title }} <span>{{ post.creation_timestamp|date:"Y/m/d" }}</span></a></li>
-{% endfor %}
-<li><a href="/archives/">...</a></li>
-</ul>
-</div>
-
-<nav>
-<a href="tag/radio/">Radio <span>(Panik & ailleurs)</span></a>
-<a href="tag/code/">Logiciel libre <span>(Debian, GNOME & ce qui passe)</span></a>
-<a class="divers" href="tag/divers/"><span>(Totalement)</span> divers</a>
-<a class="vrac" href="archives/"><span>(tout)</span> En vrac</a>
-</nav>
+ <section class="older post-list">
+ Avant ça :
+ <ul>
+ {% for post in posts|slice:"1:" %}
+ <li><a href="{{ post.get_absolute_url }}">{{ post.title }} <span>{{ post.creation_timestamp|date:"Y/m/d" }}</span></a></li>
+ {% endfor %}
+ <li><a href="/archives/">...</a></li>
+ </ul>
+ </section>
{% endblock %}
{% load i18n %}
{% block body %}
-<form method="post">
-{% csrf_token %}
-{% trans "Delete?" %}
-<button>{% trans "Delete" %}</button>
-</form>
+ <form method="post">
+ {% csrf_token %}
+ {% trans "Delete?" %}
+ <button>{% trans "Delete" %}</button>
+ </form>
{% endblock %}
{% extends "phyll/base.html" %}
{% load i18n %}
+{% block html-lang %}{{ object.lang }}{% endblock %}
{% block content-class %}post{% endblock %}
{% block page-title %}{{ object.title }} - {{ block.super }}{% endblock %}
{% block body %}
-<div>
-<h2>{{ object.title }}</h2>
-<div {% if request.user.is_staff %}contenteditable="true"{% endif %}>{{ object.text|safe }}</div>
-{% if request.user.is_staff %}
-{% csrf_token %}<button id="save">{% trans "Save" %}</button>
-{% endif %}
+ <article>
+ <h1>{{ object.title }}</h1>
+ {% if object.included_in_feed %}
+ <div class="meta">{{ object.creation_timestamp|date:"j E Y, H:i"|lower }}</div>
+ {% endif %}
+ <div {% if request.user.is_staff %}data-editable{% endif %}>{{ object.text|safe }}</div>
-<div class="meta">{{ object.creation_timestamp|date:"j E Y, H:i"|lower }}</div>
-</div>
+ {% if request.user.is_staff %}
+ {% csrf_token %}
+ <div id="quickedit">
+ <label><input type="checkbox"><span>Mode édition</span></label>
+ </div>
+ {% endif %}
+
+ {% if not object.included_in_feed %}
+ <div class="meta">Dernière mise à jour : {{ object.last_update_timestamp|date:"j E Y, H:i"|lower }}</div>
+ {% endif %}
+
+ </article>
{% endblock %}
{% block bottom-actions %}
-<a href="edit/">{% trans "Edit" %}</a>
-<a href="delete/">{% trans "Delete" %}</a>
+ <a href="edit/">{% trans "Edit" %}</a>
+ <a href="delete/">{% trans "Delete" %}</a>
{% endblock %}
{% load gadjo i18n static %}
{% block bottom-head %}
-<script src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script>
-<script type="text/javascript" src="{% static "ckeditor/ckeditor-init.js" %}"></script>
+ <script src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script>
+ <script type="text/javascript" src="{% static "ckeditor/ckeditor-init.js" %}"></script>
{% endblock %}
{% block body %}
-<form method="post">
-{% csrf_token %}
-{{ form.as_p }}
-<button>{% trans "Submit" %}</button>
-</form>
+ <form method="post">
+ {% csrf_token %}
+ {{ form.as_p }}
+ <button>{% trans "Submit" %}</button>
+ </form>
{% endblock %}
{% block content-class %}post-list{% endblock %}
{% block body %}
-<div>
-<h2>{{ view.kwargs.tag }}</h2>
-<ul>
-{% for post in object_list %}
-<li {% if not post.published %}class="unpublished"{% endif %}><a href="{{ post.get_absolute_url }}">{{ post.title }} <span>{{ post.creation_timestamp|date:"Y/m/d" }}</a></li>
-{% endfor %}
-</ul>
-</div>
+ <div>
+ {% if view.kwargs.tag %}
+ <h1>{{ view.kwargs.tag }}</h1>
+ {% else %}
+ <h1>Archives</h1>
+ {% endif %}
+ <ul>
+ {% for post in object_list %}
+ <li {% if not post.published %}class="unpublished"{% endif %}><a href="{{ post.get_absolute_url }}">{{ post.title }} <span>{{ post.creation_timestamp|date:"Y/m/d" }}</a></li>
+ {% endfor %}
+ </ul>
+ </div>
{% endblock %}
{% block bottom-actions %}
-<a href="edit/">{% trans "Edit" %}</a>
-<a href="delete/">{% trans "Delete" %}</a>
+ <a href="edit/">{% trans "Edit" %}</a>
+ <a href="delete/">{% trans "Delete" %}</a>
{% endblock %}
{% load i18n %}
{% block body %}
-<form method="post" action="{% url 'login' %}">
-{% csrf_token %}
-{{ form.as_p }}
-<button name="login">{% trans "Login" %}</button>
-<input type="hidden" name="next" value="{{ next }}" />
-</form>
+ <form id="login" method="post" action="{% url 'login' %}">
+ {% csrf_token %}
+ {{ form.as_p }}
+ <button name="login">{% trans "Login" %}</button>
+ <input type="hidden" name="next" value="{{ next }}" />
+ </form>
{% endblock %}
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-from django.conf.urls import url
-from django.contrib.admin.views.decorators import staff_member_required
-from django.views.decorators.cache import never_cache
+from functools import partial
import ckeditor.views as ckeditor_views
+from django.contrib.admin.views.decorators import staff_member_required
+from django.urls import path, re_path
+from django.views.decorators.cache import never_cache
from . import views
+staff_required = partial(staff_member_required, login_url='login')
+
+
urlpatterns = [
- url(
+ re_path(
r'^ckeditor/upload/',
- staff_member_required(ckeditor_views.upload, login_url='login'),
+ staff_required(ckeditor_views.upload),
name='ckeditor_upload',
),
- url(
+ re_path(
r'^ckeditor/browse/',
- never_cache(staff_member_required(ckeditor_views.browse, login_url='login')),
+ never_cache(staff_required(ckeditor_views.browse)),
name='ckeditor_browse',
),
- url(r'^(?P<slug>[\w:-]+)/edit/$', staff_member_required(views.NoteEditView.as_view(), login_url='login')),
- url(
+ re_path(r'^(?P<slug>[\w:-]+)/edit/$', staff_required(views.NoteEditView.as_view())),
+ re_path(
r'^(?P<slug>[\w:-]+)/delete/$',
- staff_member_required(views.NoteDeleteView.as_view(), login_url='login'),
+ staff_required(views.NoteDeleteView.as_view()),
+ ),
+ re_path(r'^(?P<slug>[\w:-]+)/api-save/$', staff_required(views.NoteApiSaveView.as_view())),
+ path('ajax/upload/', staff_required(views.ajax_upload)),
+ path('ajax/newpage/', staff_required(views.ajax_new_page)),
+ path('ajax/search/', views.ajax_search, name='ajax-search'),
+ path('new-note/', staff_required(views.NoteAddView.as_view())),
+ re_path(r'^feeds/(?P<sub>[\w:-]+)/atom$', views.AtomFeed()),
+ path('feed/atom', views.AtomFeed()),
+ re_path(r'^tag/(?P<tag>[\w:-]+)/$', views.ListOnTagView.as_view()),
+ path('archives/', views.ArchivesView.as_view()),
+ re_path(
+ r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[\w:-]+)/$', views.NoteView.as_view()
),
- url(r'^(?P<slug>[\w:-]+)/api-save/$', staff_member_required(views.NoteApiSaveView.as_view())),
- url(r'^ajax/upload/$', staff_member_required(views.ajax_upload)),
- url(r'^new-note/$', staff_member_required(views.NoteAddView.as_view(), login_url='login')),
- url(r'^feeds/(?P<sub>[\w:-]+)/atom$', views.AtomFeed()),
- url(r'^feed/atom$', views.AtomFeed()),
- url(r'^tag/(?P<tag>[\w:-]+)/$', views.ListOnTagView.as_view()),
- url(r'^archives/$', views.ArchivesView.as_view()),
- url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})/(?P<slug>[\w:-]+)/$', views.NoteView.as_view()),
- url(r'^(?P<slug>[\w:-]+)/$', views.NoteView.as_view()),
- url(r'^$', views.HomeView.as_view()),
+ re_path(r'^(?P<slug>[\w:-]+)/$', views.NoteView.as_view()),
+ path('', views.HomeView.as_view()),
]
import urllib.parse
from django.conf import settings
+from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector
from django.contrib.syndication.views import Feed
from django.core.exceptions import PermissionDenied
from django.core.files.storage import default_storage
-from django.http import HttpResponse, Http404, JsonResponse
+from django.http import Http404, HttpResponse, JsonResponse
from django.utils.feedgenerator import Atom1Feed
+from django.utils.text import slugify
from django.views import View
from django.views.decorators.csrf import csrf_exempt
-from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView, TemplateView
-
+from django.views.generic import CreateView, DeleteView, DetailView, ListView, TemplateView, UpdateView
from sorl.thumbnail.shortcuts import get_thumbnail
from .models import Note
raise Http404()
if not note.published and not request.user.is_staff:
raise PermissionDenied()
- return super(NoteView, self).get(request, *args, **kwargs)
+ return super().get(request, *args, **kwargs)
class NoteEditView(UpdateView):
model = Note
- fields = ['title', 'slug', 'text', 'tags', 'published']
+ fields = ['title', 'slug', 'text', 'tags', 'published', 'included_in_feed']
class NoteApiSaveView(View):
class NoteAddView(CreateView):
model = Note
- fields = ['title', 'slug', 'text', 'tags', 'published']
+ fields = ['title', 'slug', 'text', 'tags', 'published', 'included_in_feed']
class NoteDeleteView(DeleteView):
template_name = 'phyll/home.html'
def get_context_data(self, **kwargs):
- context = super(HomeView, self).get_context_data(**kwargs)
- context['posts'] = Note.objects.filter(published=True).order_by('-creation_timestamp')[:5]
+ context = super().get_context_data(**kwargs)
+ context['posts'] = Note.objects.filter(published=True, included_in_feed=True).order_by(
+ '-creation_timestamp'
+ )[:5]
+ context['recent_changes'] = Note.objects.filter(published=True, included_in_feed=True).order_by(
+ '-last_update_timestamp'
+ )
+ context['index'] = Note.objects.filter(slug='index').first()
return context
model = Note
def get_queryset(self):
- qs = super(ArchivesView, self).get_queryset()
+ qs = super().get_queryset()
if not self.request.user.is_staff:
- qs = qs.filter(published=True)
+ qs = qs.filter(published=True, included_in_feed=True)
return qs
class Atom1FeedWithBaseXml(Atom1Feed):
def root_attributes(self):
- root_attributes = super(Atom1FeedWithBaseXml, self).root_attributes()
+ root_attributes = super().root_attributes()
scheme, netloc, path, params, query, fragment = urllib.parse.urlparse(self.feed['feed_url'])
root_attributes['xml:base'] = urllib.parse.urlunparse((scheme, netloc, '/', params, query, fragment))
return root_attributes
def get_object(self, request, *args, **kwargs):
self.sub = kwargs.get('sub', 'default')
- return super(AtomFeed, self).get_object(request, *args, **kwargs)
+ return super().get_object(request, *args, **kwargs)
def items(self):
- qs = Note.objects.filter(published=True)
+ qs = Note.objects.filter(published=True, included_in_feed=True)
if self.sub == 'default':
pass
elif self.sub == 'gnome-en':
except OSError:
pass
return JsonResponse(response)
+
+
+@csrf_exempt
+def ajax_new_page(request, *args, **kwargs):
+ title = request.POST['title']
+ slug = slugify(title)
+ note, created = Note.objects.get_or_create(slug=slug)
+ if created:
+ note.title = title
+ note.text = '<p>...</p>'
+ note.save()
+ return JsonResponse(
+ {
+ 'url': '/%s/' % note.slug,
+ 'request_id': request.POST['request_id'],
+ }
+ )
+
+
+def ajax_search(request, *args, **kwargs):
+ vector = SearchVector('title', weight='A', config='french') + SearchVector(
+ 'plain_text', weight='B', config='french'
+ )
+ query = SearchQuery(request.GET.get('q', ''), config='french')
+ results = (
+ Note.objects.annotate(rank=SearchRank(vector, query)).filter(rank__gte=0.1).order_by('-rank')[:10]
+ )
+ return JsonResponse(
+ {'data': [{'title': x.title, 'rank': x.rank, 'url': x.get_absolute_url()} for x in results]}
+ )
--- /dev/null
+@charset "UTF-8";
+
+$text-color: #222;
+$link-color: #386ede;
+$visited-link-color: #551a8b;
+
+html {
+ --text-color: #222;
+ --text-background: #fafaff;
+ --light-text-color: lighten(#222, 20%);
+ --lighter-text-color: lighten(#222, 30%);
+ --header-background: #eaeaff;
+ --header-box-shadow-color: #b2987f;
+ --link-color: #386ede;
+ --visited-link-color: #551a8b;
+ --border-color: #555;
+ --note-background: #fbf7c1;
+ --blockquote-background: #eeeefd;
+ --toc-background: rgba(255, 255, 255, 0.8);
+ --toc-background-mobile: #eee;
+ --search-hit-hover-background: #eee;
+}
+
+@media (prefers-color-scheme: dark) {
+ html {
+ --text-background: #100;
+ --text-color: #eef;
+ --light-text-color: lighten(#eef, 20%);
+ --lighter-text-color: lighten(#eef, 30%);
+ --header-background: #080000;
+ --header-box-shadow-color: #211;
+ --link-color: #aaf;
+ --visited-link-color: #b767ff;
+ --border-color: #555;
+ --note-background: #331;
+ --blockquote-background: #111;
+ --toc-background: rgba(15, 15, 25, 0.8);
+ --toc-background-mobile: rgba(25, 15, 15);
+ --search-hit-hover-background: #002;
+ }
+}
+
+@import "../../../phyll/static/css/_opensans.scss";
+
+.sr-only {
+ position: absolute !important;
+ width: 1px !important;
+ height: 1px !important;
+ padding: 0 !important;
+ margin: -1px !important;
+ overflow: hidden !important;
+ clip: rect(0, 0, 0, 0) !important;
+ white-space: nowrap !important;
+ border: 0 !important;
+ background: white;
+ color: black;
+}
+
+#login {
+ input {
+ display: block;
+ width: 30em;
+ max-width: 100%;
+ }
+}
+
+body {
+ font-family: "Open Sans", sans-serif;
+ background: url(6138240034_68f81350a2_o-x.jpg) top center no-repeat fixed;
+ @media (prefers-color-scheme: dark) {
+ background-image: url(6138240034_68f81350a2_o-x-dark.jpg);
+ }
+ background-size: cover;
+ color: var(--text-color);
+ font-size: 100%;
+}
+
+a {
+ color: var(--link-color);
+ text-decoration: none;
+ border-bottom: 1px dotted var(--link-color);
+ &:visited {
+ color: var(--visited-link-color);
+ border-color: var(--visited-link-color);
+ }
+ &:hover {
+ border-bottom-style: solid;
+ }
+}
+
+header {
+ display: block;
+ margin: 0 auto;
+ padding: 0 0 0em 0;
+ max-width: 45em;
+ position: relative;
+ z-index: 10;
+ span {
+ font-weight: normal;
+ font-size: 2em;
+ text-transform: uppercase;
+ margin: 0 0 0 1em;
+ padding: 0 0.5em;
+ color: var(--light-text-color);
+ background: var(--header-background);
+ display: inline-block;
+ box-shadow: 0 0 3px 1px var(--header-box-shadow-color);
+ a {
+ border: none;
+ }
+ }
+ transform: rotate(-1deg);
+}
+
+main, footer {
+ position: relative;
+ z-index: 0;
+ margin: 0 auto;
+ padding: 1em;
+ max-width: 45em;
+ @media screen and (max-width: 50em) {
+ margin: 0;
+ }
+}
+
+main {
+ position: relative;
+ background: var(--text-background);
+ min-height: 80vh;
+ border: 1px solid var(--border-color);
+}
+
+footer {
+ border: 1px solid var(--border-color);
+ margin-top: 1em;
+ background: var(--text-background);
+ display: flex;
+ justify-content: space-between;
+ p {
+ margin: 0;
+ }
+}
+
+article {
+ h1 {
+ font-size: 1.5em;
+ }
+ h2 {
+ font-size: 1.2em;
+ margin-left: -0.9em;
+ border-left: 0.3em solid #555;
+ padding-left: 0.5em;
+ }
+ a {
+ box-shadow: inset 0 -8px 0 0 rgba(34, 27, 242, 0.1);
+ transition: box-shadow ease 0.2s;;
+ &:hover {
+ box-shadow: inset 0 -21px 0 0 rgba(34, 27, 242, 0.3);
+ }
+ }
+
+ [contenteditable=true] div.figure {
+ cursor: pointer;
+ }
+ div.figure {
+ text-align: center;
+ line-height: initial;
+ span.empty::before {
+ min-height: 50px;
+ margin: 0 auto;
+ display: block;
+ width: 90%;
+ background: #eee;
+ padding: 1rem;
+ font-size: 150px;
+ content: "(image)";
+ color: #aaa;
+ }
+ img {
+ max-width: 90%;
+ }
+ }
+ p {
+ line-height: 160%;
+ }
+ code,
+ pre.screen {
+ background: #111;
+ color: white;
+ padding: 2px;
+ overflow: scroll;
+ }
+ div.meta {
+ margin-top: 3em;
+ color: var(----lighter-text-color);
+ }
+ div.note {
+ background: var(--note-background) url(yelp-note.png) 5px 10px no-repeat;
+ clear: both;
+ margin-bottom: 0.5em;
+ position: relative;
+ top: -4em;
+ padding: 0.2em 0.5em 0.2em 2em;
+ float: right;
+ width: 15em;
+ margin-right: -19.5em;
+ p {
+ margin: 0.5em 0;
+ }
+ }
+ blockquote {
+ background: var(--blockquote-background);
+ padding: 0.1em 1em;
+ clip-path: polygon(0px 0px, 94.27% 2px, 99.63% 2.65%, 98.39% 97.48%, 10% 100%, 0% 100%, 0px 0px);
+ }
+}
+
+.latest-changes {
+ border-top: 1px solid var(--border-color);
+ padding-top: 0;
+ h3 {
+ font-weight: normal;
+ }
+ &--date {
+ font-size: 80%;
+ color: var(--light-text-color);
+ }
+}
+
+.inline-style-popup,
+.block-style-popup {
+ background: white;
+ box-shadow: 0 0 5px #666;
+ input {
+ display: none;
+ padding: 3px;
+ border: 1px inset #ccc;
+ background: white;
+ width: 0px;
+ transition: width ease 2s;
+ &.shown {
+ display: inline-block;
+ width: 400px;
+ }
+ }
+ button {
+ padding: 0 0.5em;
+ height: 2em;
+ text-align: center;
+ background: #eee;
+ border: 0px;
+ &:hover {
+ background: #ccc;
+ }
+ &[data-action=createLink] {
+ color: blue;
+ text-decoration: underline;
+ }
+ &.on {
+ background: #444;
+ color: white;
+ }
+ }
+ &.inline-style-popup button {
+ width: 2em;
+ padding: 0;
+ }
+ &.block-style-popup {
+ &.selected button {
+ display: none;
+ &.on {
+ display: block;
+ }
+ }
+ }
+}
+
+input#image-upload, input#document-upload {
+ display: none;
+}
+
+#quickedit {
+ position: sticky;
+ bottom: 10px;
+ display: flex;
+ justify-content: flex-end;
+ label {
+ display: inline-block;
+ margin-left: 6px;
+ cursor: pointer;
+ }
+ input {
+ display: none;
+ }
+
+ button,
+ span {
+ display: inline-block;
+ font-weight: normal;
+ background: white;
+ border: 1px solid #386ede;
+ box-shadow: 0 0 0 5px white;
+ padding: 1ex;
+ border-radius: 3px;
+ color: #386ede;
+ font-size: inherit;
+ letter-spacing: inherit;
+ line-height: inherit;
+ }
+ button {
+ &:hover {
+ color: white;
+ background: #386ede;
+ }
+ }
+ button.error {
+ background: red;
+ color: white;
+ &:hover {
+ background: darken(red, 10%);
+ }
+ }
+
+ input:checked + span {
+ background: #386ede;
+ color: white;
+ }
+}
+
+#search {
+ label {
+ position: absolute;
+ right: 1em;
+ }
+ #search-enable, .search-results {
+ display: none;
+ }
+ #search-enable + label::after {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: #eee;
+ border: 1px solid #aaa;
+ width: 2em;
+ height: 28px;
+ content: "🔍";
+ border-radius: 5px;
+ }
+ #search-enable:checked {
+ + label {
+ &::after {
+ color: black;
+ content: "×";
+ }
+ }
+ ~ .search-field {
+ height: 30px;
+ }
+ ~ .search-results {
+ display: block;
+ }
+ }
+ .search-field {
+ height: 0;
+ transition: all ease 0.2s;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ margin-right: 50px;
+ input {
+ height: 30px;
+ }
+ }
+ .search-results {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ li {
+ margin: 0;
+ padding: 0;
+ a {
+ display: block;
+ padding: 0.5rem 1rem;
+ border: 1px solid $link-color;
+ margin-top: -1px;
+ &:hover {
+ background: var(--search-hit-hover-background);
+ }
+ }
+ }
+ }
+}
+
+.wiki-anchor {
+ display: none;
+}
+
+article h2:hover {
+ .wiki-anchor {
+ display: inline-block;
+ box-shadow: none;
+ border: none;
+ opacity: 0.8;
+ padding-left: 0.5rem;
+ &:hover {
+ opacity: 1;
+ }
+ }
+}
+
+#toc {
+ box-sizing: border-box;
+ padding: 1em 0 1em 1em;
+ float: left;
+ width: 15em;
+ background: var(--toc-background);
+ margin-left: -17em;
+ position: sticky;
+ top: 1em;
+ ul, li {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ }
+ li {
+ margin: 0em;
+ border-right: 0.3em solid transparent;
+ padding: 5px 0.5em 5px 0;
+ &.active {
+ border-right: 0.3em solid var(--border-color);
+ }
+ }
+ a {
+ box-shadow: none;
+ }
+}
+
+@media screen and (max-width: 80em) {
+ #toc {
+ background: var(--toc-background-mobile);
+ float: none;
+ margin: 0;
+ width: auto;
+ position: static;
+ }
+ article div.note {
+ position: static;
+ float: none;
+ margin: 0;
+ width: auto;
+ }
+}
--- /dev/null
+{% load gadjo i18n %}<!DOCTYPE html>
+<html lang="{% block html-lang %}fr{% endblock %}">
+ <head>
+ <meta charset="utf-8"/> <!-- 📻 -->
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>{% block page-title %}rdio.space - panikdb - gestion radio{% endblock %}</title>
+ <link rel="stylesheet" type="text/css" href="/static/css/style.css">
+ <link rel="shortcut icon" href="/static/icon.png">
+ <link rel="manifest" href="/static/manifest.json">
+ <script src="{% xstatic 'jquery' 'jquery.min.js' %}"></script>
+ <script src="/static/js/chloro.js"></script>
+ {% block bottom-head %}
+ {% endblock %}
+ </head>
+ <body>
+ <header>
+ <span><a href="/">PanikDB - gestion radio</a></span>
+ </header>
+ <main class="{% block content-class %}{% endblock %} phyll-toc">
+ <div id="search">
+ <input type="checkbox" id="search-enable">
+ <label for="search-enable"><span class="sr-only">Afficher la recherche</span></label>
+ <form class="search-field">
+ <input name="q" type="search">
+ </form>
+ <ul class="search-results">
+ </ul>
+ </div>
+ {% block body %}
+ {% endblock %}
+ </main>
+ <footer>
+ <p>Contact : <a href="mailto:fred@rdio.space">fred@rdio.space</a></p>
+ {% if request.user.is_staff %}
+ <p class="actions">
+ <a href="/new-note/">{% trans "New Page" %}</a>
+ {% block bottom-actions %}
+ {% endblock %}
+ </p>
+ {% endif %}
+ </footer>
+ </body>
+</html>
--- /dev/null
+{% extends "phyll/base.html" %}
+
+{% block content-class %}home{% endblock %}
+
+{% block body %}
+
+ <article>
+ <h1>{{ index.title }}</h1>
+ {{ index.text|safe }}
+ </article>
+
+ {% if request.user.is_staff %}<p><a class="button" href="/index/">(page éditable)</a></p>{% endif %}
+
+ <div class="latest-changes">
+ <h3>Dernières pages modifiées</h3>
+ <ul>
+ {% for page in recent_changes|slice:":10" %}
+ <li><a href="{{ page.get_absolute_url }}">{{ page.title }}</a>
+ <span class="latest-changes--date">le {{ page.last_update_timestamp|date:"j E Y, H:i"|lower }}</span></li>
+ {% endfor %}
+ </ul>
+ </div>
+
+{% endblock %}
+
+{% block bottom-actions %}
+{% endblock %}
--- /dev/null
+{% extends "phyll/base.html" %}
+{% load i18n %}
+
+{% block html-lang %}{{ object.lang }}{% endblock %}
+{% block content-class %}post{% endblock %}
+{% block page-title %}{{ object.title }} - {{ block.super }}{% endblock %}
+
+{% block body %}
+ <article>
+ <h1>{{ object.title }}</h1>
+ <div {% if request.user.is_staff %}data-editable="true"{% endif %}>{{ object.text|safe }}</div>
+
+ <div class="meta">Dernière modification : {{ object.last_update_timestamp|date:"j E Y, H:i"|lower }}</div>
+ </article>
+
+ {% if object.linking_note.all.exists %}
+ <p id="backlinks">Pages pointant sur celle-ci :
+ {% for interlink in object.linking_note.all %}
+ <a href="{{interlink.note1.get_absolute_url}}">{{ interlink.note1.title }}</a>
+ {% if not forloop.last %} - {% endif %}
+ {% endfor %}</p>
+ {% endif %}
+
+ {% if request.user.is_staff %}
+ {% csrf_token %}
+ <div id="quickedit">
+ <label><input type="checkbox"><span>Mode édition</span></label>
+ </div>
+ {% endif %}
+
+{% endblock %}
+
+{% block bottom-actions %}
+ -
+ <a href="edit/">{% trans "Edit" %}</a>
+ -
+ <a href="delete/">{% trans "Delete" %}</a>
+{% endblock %}
# Application definition
+DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
+
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.contenttypes',
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = {
- 'default': {'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': 'chloro',},
+ 'default': {
+ 'ENGINE': 'django.db.backends.postgresql',
+ 'NAME': 'chloro',
+ },
}
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
- {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',},
- {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',},
- {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',},
- {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',},
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
]
LOGIN_REDIRECT_URL = '/'
USE_TZ = True
+LOCALE_PATHS = (os.path.join(BASE_DIR, 'chloro', 'locale'),)
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/
['JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'],
['Link', 'Unlink'],
['Image', '-', 'HorizontalRule'],
- ['RemoveFormat',],
+ [
+ 'RemoveFormat',
+ ],
['Maximize'],
],
'toolbar': 'Own',
},
}
-SITE_AUTHOR = "Frédéric Péters"
-SITE_TITLE = "Coin web de Frédéric Péters"
+SITE_AUTHOR = 'Frédéric Péters'
+SITE_TITLE = 'Coin web de Frédéric Péters'
local_settings_file = os.environ.get(
'CHLORO_SETTINGS_FILE', os.path.join(os.path.dirname(__file__), 'local_settings.py')
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf import settings
-from django.conf.urls import include, url
from django.conf.urls.static import static
from django.contrib.auth import views as auth_views
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
+from django.urls import include, path
urlpatterns = [
- url(r'^accounts/login/$', auth_views.LoginView.as_view(), name='login'),
- url(r'^accounts/logout/$', auth_views.LogoutView.as_view(), name='logout'),
+ path('accounts/login/', auth_views.LoginView.as_view(), name='login'),
+ path('accounts/logout/', auth_views.LogoutView.as_view(), name='logout'),
]
# static and media files
urlpatterns += staticfiles_urlpatterns()
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
-urlpatterns.append(url(r'', include('chloro.phyll.urls')))
+urlpatterns.append(path('', include('chloro.phyll.urls')))
from django.core.wsgi import get_wsgi_application
-os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chloro.settings")
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chloro.settings')
application = get_wsgi_application()
Priority: optional
Maintainer: Frederic Peters <fpeters@0d.be>
Build-Depends: debhelper-compat (= 12),
- sassc,
+ dh-python,
python3-all,
python3-django,
python3-setuptools,
- dh-python
+ sassc,
Standards-Version: 3.9.1
Package: chloro
Architecture: all
-Depends: ${python3:Depends}
+Depends: ${python3:Depends},
Description: Content Manager
Code to run 0d.be.
import os
import sys
-if __name__ == "__main__":
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chloro.settings")
+if __name__ == '__main__':
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chloro.settings')
try:
from django.core.management import execute_from_command_line
except ImportError:
except ImportError:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
- "available on your PYTHONPATH environment variable? Did you "
- "forget to activate a virtual environment?"
+ 'available on your PYTHONPATH environment variable? Did you '
+ 'forget to activate a virtual environment?'
)
raise
execute_from_command_line(sys.argv)
#! /usr/bin/env python3
-# -*- coding: utf-8 -*-
import os
import re
import subprocess
import sys
-
-from setuptools.command.install_lib import install_lib as _install_lib
+from distutils.cmd import Command
from distutils.command.build import build as _build
from distutils.command.sdist import sdist
-from distutils.cmd import Command
from distutils.spawn import find_executable
-from setuptools import setup, find_packages
+
+from setuptools import find_packages, setup
+from setuptools.command.install_lib import install_lib as _install_lib
class eo_sdist(sdist):
def get_version():
- '''Use the VERSION, if absent generates a version with git describe, if not
- tag exists, take 0.0- and add the length of the commit log.
- '''
+ """Use the VERSION, if absent generates a version with git describe, if not
+ tag exists, take 0.0- and add the length of the commit log.
+ """
if os.path.exists('VERSION'):
- with open('VERSION', 'r') as v:
+ with open('VERSION') as v:
return v.read()
if os.path.exists('.git'):
p = subprocess.Popen(