]> git.0d.be Git - chloro.git/commitdiff
do not include non-feed posts on homepage main
authorFrédéric Péters <fpeters@0d.be>
Sun, 17 Dec 2023 16:44:57 +0000 (17:44 +0100)
committerFrédéric Péters <fpeters@0d.be>
Sun, 17 Dec 2023 16:44:57 +0000 (17:44 +0100)
42 files changed:
.pre-commit-config.yaml
MANIFEST.in
chloro/locale/fr/LC_MESSAGES/django.po [new file with mode: 0644]
chloro/monkeypatch.py
chloro/phyll/__init__.py
chloro/phyll/fields.py
chloro/phyll/management/commands/makemessages.py [new file with mode: 0644]
chloro/phyll/management/commands/reindex.py [new file with mode: 0644]
chloro/phyll/migrations/0001_initial.py
chloro/phyll/migrations/0002_auto_20191229_1932.py
chloro/phyll/migrations/0003_note_published.py
chloro/phyll/migrations/0004_note_plain_text.py [new file with mode: 0644]
chloro/phyll/migrations/0005_set_plain_text.py [new file with mode: 0644]
chloro/phyll/migrations/0006_interlink.py [new file with mode: 0644]
chloro/phyll/migrations/0007_note_included_in_feed.py [new file with mode: 0644]
chloro/phyll/models.py
chloro/phyll/static/css/style.scss
chloro/phyll/static/js/chloro.js
chloro/phyll/templates/phyll/base.html
chloro/phyll/templates/phyll/home.html
chloro/phyll/templates/phyll/note_confirm_delete.html
chloro/phyll/templates/phyll/note_detail.html
chloro/phyll/templates/phyll/note_form.html
chloro/phyll/templates/phyll/note_list.html
chloro/phyll/templates/registration/login.html
chloro/phyll/urls.py
chloro/phyll/views.py
chloro/rdio/__init__.py [new file with mode: 0644]
chloro/rdio/static/css/6138240034_68f81350a2_o-x-dark.jpg [new file with mode: 0644]
chloro/rdio/static/css/6138240034_68f81350a2_o-x.jpg [new file with mode: 0644]
chloro/rdio/static/css/style.scss [new file with mode: 0644]
chloro/rdio/static/css/yelp-note.png [new file with mode: 0644]
chloro/rdio/static/icon.png [new file with mode: 0644]
chloro/rdio/templates/phyll/base.html [new file with mode: 0644]
chloro/rdio/templates/phyll/home.html [new file with mode: 0644]
chloro/rdio/templates/phyll/note_detail.html [new file with mode: 0644]
chloro/settings.py
chloro/urls.py
chloro/wsgi.py
debian/control
manage.py
setup.py

index a14705c76152ef5eeb05771c1791648cce5aa3cc..3c50489021fbc156287998d7188315894ed8ce94 100644 (file)
@@ -1,8 +1,36 @@
 # See https://pre-commit.com for more information
 # See https://pre-commit.com/hooks.html for more hooks
 repos:
 # 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
 -   repo: https://github.com/psf/black
-    rev: 20.8b1
+    rev: 23.1.0
     hooks:
     - id: black
       args: ['--target-version', 'py37', '--skip-string-normalization', '--line-length', '110']
     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
index eac17a9bb3484cf1344cdb1f0ea95b81ea64a355..f5d77a869f405c9bbfc754aaf5484a869e435672 100644 (file)
@@ -1,5 +1,8 @@
 include COPYING
 include MANIFEST.in
 
 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/phyll/templates *.html
+recursive-include chloro/rdio/static *.css *.scss *.js *.json *.png *.jpg
+recursive-include chloro/rdio/templates *.html
diff --git a/chloro/locale/fr/LC_MESSAGES/django.po b/chloro/locale/fr/LC_MESSAGES/django.po
new file mode 100644 (file)
index 0000000..bd95153
--- /dev/null
@@ -0,0 +1,70 @@
+# 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"
index c0c71425df778dd51be43adfb6bdd744d89aae92..7802236d76082610575b3634405685328be1e43b 100644 (file)
 # 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/>.
 
 # 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.urls 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.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
 
 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, renderer=None):
     if value is None:
 
 def ckeditor_render(self, name, value, attrs=None, renderer=None):
     if value is None:
@@ -44,7 +43,7 @@ def ckeditor_render(self, name, value, attrs=None, renderer=None):
 
     # Force to text to evaluate possible lazy objects
     external_plugin_resources = [
 
     # 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(
     ]
 
     return mark_safe(
@@ -52,7 +51,7 @@ def ckeditor_render(self, name, value, attrs=None, renderer=None):
             'ckeditor/widget.html',
             {
                 'final_attrs': flatatt(final_attrs),
             '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),
                 'id': final_attrs['id'],
                 'config': ckeditor.widgets.json_encode(self.config),
                 'external_plugin_resources': ckeditor.widgets.json_encode(external_plugin_resources),
index 93e2837d2645ce52264667678ed92114fee81a35..10560b6854c13fee8e25dd866c04ae29b615702d 100644 (file)
@@ -13,5 +13,3 @@
 #
 # 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/>.
 #
 # 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'
index 2ce4b91920430639a975c1b91b94edc77167ba19..7bd2ddbef308b08f3b4f11cc17e8daca42f6e751 100644 (file)
@@ -14,9 +14,8 @@
 # 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/>.
 
 # 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
 import ckeditor.fields
+from django.conf import settings
 
 
 class RichTextField(ckeditor.fields.RichTextField):
 
 
 class RichTextField(ckeditor.fields.RichTextField):
@@ -28,12 +27,12 @@ class RichTextField(ckeditor.fields.RichTextField):
             'external_plugin_resources': self.external_plugin_resources,
         }
         defaults.update(kwargs)
             '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):
 
 
 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('&laquo; ', '«\u202f')
         if settings.LANGUAGE_CODE.startswith('fr-'):
             # apply some basic typographic rules
             value = value.replace('&laquo; ', '«\u202f')
diff --git a/chloro/phyll/management/commands/makemessages.py b/chloro/phyll/management/commands/makemessages.py
new file mode 100644 (file)
index 0000000..790e8c3
--- /dev/null
@@ -0,0 +1,32 @@
+# 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)
diff --git a/chloro/phyll/management/commands/reindex.py b/chloro/phyll/management/commands/reindex.py
new file mode 100644 (file)
index 0000000..964fefc
--- /dev/null
@@ -0,0 +1,27 @@
+# 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()
index 32c3af759a105720d2d84375e415218fec4a7c3b..0fbfc658e37138f1f0a7da48b77a03a93c5d1b7d 100644 (file)
@@ -1,14 +1,12 @@
-# -*- coding: utf-8 -*-
 # Generated by Django 1.11.17 on 2019-12-29 13:29
 # 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
 import taggit.managers
+from django.db import migrations, models
 
 
+import chloro.phyll.fields
 
 
-class Migration(migrations.Migration):
 
 
+class Migration(migrations.Migration):
     initial = True
 
     dependencies = [
     initial = True
 
     dependencies = [
index 0620b4d2b909948eb9bee906f7434aad9ffce6fe..49d0b536ef3800b67f3d0febdff8ce54eb7c926f 100644 (file)
@@ -1,12 +1,9 @@
-# -*- coding: utf-8 -*-
 # Generated by Django 1.11.17 on 2019-12-29 18:32
 # 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):
 
 from django.db import migrations
 
 
 class Migration(migrations.Migration):
-
     dependencies = [
         ('phyll', '0001_initial'),
     ]
     dependencies = [
         ('phyll', '0001_initial'),
     ]
index 7c5f05e9bf6ab91e0c0ce01528549050ae27dcbc..a99cb36814ad2d0618529cbcdcf866ca02d827c7 100644 (file)
@@ -1,12 +1,9 @@
-# -*- coding: utf-8 -*-
 # Generated by Django 1.11.17 on 2019-12-29 18:39
 # 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):
 
 from django.db import migrations, models
 
 
 class Migration(migrations.Migration):
-
     dependencies = [
         ('phyll', '0002_auto_20191229_1932'),
     ]
     dependencies = [
         ('phyll', '0002_auto_20191229_1932'),
     ]
diff --git a/chloro/phyll/migrations/0004_note_plain_text.py b/chloro/phyll/migrations/0004_note_plain_text.py
new file mode 100644 (file)
index 0000000..f02481c
--- /dev/null
@@ -0,0 +1,17 @@
+# 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),
+        ),
+    ]
diff --git a/chloro/phyll/migrations/0005_set_plain_text.py b/chloro/phyll/migrations/0005_set_plain_text.py
new file mode 100644 (file)
index 0000000..c1aded6
--- /dev/null
@@ -0,0 +1,23 @@
+# 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),
+    ]
diff --git a/chloro/phyll/migrations/0006_interlink.py b/chloro/phyll/migrations/0006_interlink.py
new file mode 100644 (file)
index 0000000..28fccdd
--- /dev/null
@@ -0,0 +1,40 @@
+# 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',
+                    ),
+                ),
+            ],
+        ),
+    ]
diff --git a/chloro/phyll/migrations/0007_note_included_in_feed.py b/chloro/phyll/migrations/0007_note_included_in_feed.py
new file mode 100644 (file)
index 0000000..5faefc7
--- /dev/null
@@ -0,0 +1,17 @@
+# 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'),
+        ),
+    ]
index 1f9914b53ca154238a78c964276874cfa38a2006..6642771ada6991ecf57f74debf76aa8198637930 100644 (file)
 # 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/>.
 
 # 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.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
 from taggit.managers import TaggableManager
 
 from .fields import RichTextField
@@ -25,7 +30,9 @@ class Note(models.Model):
     title = models.CharField(_('Title'), max_length=150)
     slug = models.SlugField(_('Slug'), max_length=150)
     text = RichTextField(_('Text'), blank=True, null=True)
     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)
     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)
     published = models.BooleanField(_('Published'), default=True)
     creation_timestamp = models.DateTimeField(auto_now_add=True)
     last_update_timestamp = models.DateTimeField(auto_now=True)
@@ -34,4 +41,42 @@ class Note(models.Model):
         ordering = ['-creation_timestamp']
 
     def get_absolute_url(self):
         ordering = ['-creation_timestamp']
 
     def get_absolute_url(self):
+        if self.slug == 'index':
+            return '/'
         return '/%s/' % self.slug
         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')
index 8bb69bb5011af8443c0e01acb084ecea42bc2659..95ca6cafc7936bf3ee762667cfbd282865ce672e 100644 (file)
@@ -1,25 +1,58 @@
 @charset "UTF-8";
 
 @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;
 
 @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%;
        font-size: 100%;
+       padding: 0;
+       margin: 0;
+       display: flex;
+       @media screen and (max-width: 50em) {
+               flex-direction: column;
+       }
 }
 
 a {
 }
 
 a {
-       color: $link-color;
+       color: var(--link-color);
        text-decoration: none;
        text-decoration: none;
-       border-bottom: 0.1em dotted $link-color;
+       border-bottom: 0.1em dotted var(--link-color);
        &:visited {
        &: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;
        }
        &:hover {
                border-bottom-style: solid;
@@ -27,30 +60,88 @@ a {
 }
 
 header {
 }
 
 header {
+       background: var(--header-background);
+       color: var(--header-color);
        display: inline-block;
        display: inline-block;
-       h1 {
+       .header-title {
+               font-size: 2em;
                font-weight: normal;
                text-transform: uppercase;
                font-weight: normal;
                text-transform: uppercase;
-               margin: 1em 0 0 1em;
+               margin: 4rem 1rem 2rem 1rem;
                padding: 0 0.5em;
                padding: 0 0.5em;
-               color: lighten($text-color, 20%);
-               background: #fafaff;
                display: inline-block;
                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;
        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;
 }
 
 div.actions {
        position: absolute;
        top: 0.5rem;
-       left: 1rem;
+       right: 1rem;
        z-index: 100;
        a {
        z-index: 100;
        a {
+               background: white;
+               padding: 2px;
+               border: 1px solid black;
+               border-radius: 3px;
+               color: black;
                margin-right: 1rem;
        }
 }
                margin-right: 1rem;
        }
 }
@@ -66,63 +157,33 @@ main, footer {
 
 main {
        position: relative;
 
 main {
        position: relative;
-       background: #fafaff;
+       background: var(--text-background);
        min-height: 70vh;
        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%;
+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;
                @media screen and (max-width: 50em) {
                        font-size: 200%;
                }
                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;
-               }
+               text-transform: uppercase;
        }
        }
-       .latest {
-               margin-top: 2em;
-               h2 {
-                       margin: 0;
-                       font-size: 400%;
-                       @media screen and (max-width: 50em) {
-                               font-size: 200%;
-                       }
-                       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 {
        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;
                        padding: 3px;
                        max-width: 90%;
                        max-height: 70vh;
@@ -140,26 +201,33 @@ main {
                background: #111;
                color: white;
                padding: 2px;
                background: #111;
                color: white;
                padding: 2px;
-       }
-       div.meta {
-               margin-top: 3em;
-               color: lighten($text-color, 40%);
+               overflow: auto;
        }
        div.note {
        }
        div.note {
-               background: #fbf7c1;;
+               background: var(--note-background);
                padding: 0.2em 0.5em 0.2em 2em;
                p {
                        margin: 0.5em 0;
                }
        }
        blockquote {
                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 {
                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;
        ul, li {
                margin: 0;
                padding: 0;
@@ -186,8 +254,8 @@ main {
 .older.post-list {
        margin: 5em 0 2em 0;
        padding: 1em 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;
        line-height: 200%;
        li {
                display: inline;
@@ -207,7 +275,7 @@ footer {
 }
 
 div[contenteditable=true]:focus-within {
 }
 
 div[contenteditable=true]:focus-within {
-       outline: 1px solid gray;
+       outline: 1px solid var(--gray);
        outline-offset: 3px;
 }
 
        outline-offset: 3px;
 }
 
@@ -259,9 +327,52 @@ div[contenteditable=true]:focus-within {
        }
 }
 
        }
 }
 
-button#save {
+#quickedit {
        position: sticky;
        bottom: 10px;
        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 {
 }
 
 main.post {
@@ -284,3 +395,11 @@ main.post {
                }
        }
 }
                }
        }
 }
+
+.wiki-anchor-auto {
+       display: none;
+}
+
+#image-upload, #document-upload {
+       display: none;
+}
index 816d0223b08951a7f1c307f5ff71442c0260f309..ecc6c97d031f59b2f6bcc777aa9352d9415cba83 100644 (file)
@@ -1,7 +1,89 @@
+// 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: [
 (function(window, document, undefined) {
   var Phylly = {
     BLOCKS: [
+          {name: 'intertitle', tag: 'H2', klass: 'intertitle'},
           {name: 'code', tag: 'PRE', klass: 'screen'},
           {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'},
           {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();
         }
         } 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 $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);
       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 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);
       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 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;
       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();
         $('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);
       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.addClass('shown');
       $input.focus();
       $input.removeClass('shown');
       var sel = window.getSelection();
       sel.addRange(this._range);
       $input.removeClass('shown');
       var sel = window.getSelection();
       sel.addRange(this._range);
+      var selected_link = $input[0]._selected_link;
       if (url) {
       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 {
       } else {
-        document.execCommand('unlink', false, null);
+        if (selected_link) {
+          selected_link.replaceWith(document.createTextNode(selected_link.textContent));
+        }
       }
       }
-      sel.empty();
       $input.val('');
       $input.val('');
+      $input[0]._selected_link = null;
     }
   }
   function focusout_link(ev) {
     }
   }
   function focusout_link(ev) {
                       '<button data-action="bold" data-accel="b"><b>b</b></button>' +
                       '<button data-action="code" data-accel="<">&lt;&gt;</button>' +
                       '<button data-action="removeFormat" data-accel="m">×</button>' +
                       '<button data-action="bold" data-accel="b"><b>b</b></button>' +
                       '<button data-action="code" data-accel="<">&lt;&gt;</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>');
                       '<button data-action="createLink">a</button>' +
                       '<input name="link-target"/>' +
                       '</div>');
     inline_style_toolbar.css('left', pos.left + window.scrollX);
     inline_style_toolbar.show();
   };
     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() {
 }(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;
   });
     });
     return false;
   });
index e7449de4b511f715a794f541bb978d685018c263..799af9829494132dec860639e13e5c1ec867b68f 100644 (file)
@@ -1,5 +1,5 @@
 {% load gadjo i18n %}<!DOCTYPE html>
 {% 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">
   <head>
     <meta charset="utf-8"/> <!-- 🌱 -->
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -9,29 +9,35 @@
     <link rel="shortcut icon" href="/static/icon.png">
     <link rel="manifest" href="/static/manifest.json">
     {% if request.user.is_staff %}
     <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>
     {% 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 &amp; ailleurs)</span></a></li>
+          <li><a href="/tag/code/">Logiciel libre <span>(Debian, GNOME &amp; 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>
     </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>
   </body>
 </html>
index 54cfe19f967da88f88ef74d8a8c75dcc290b0a64..7bed4826de801b51d9e79e3828706af486d1635d 100644 (file)
@@ -4,30 +4,24 @@
 
 {% block body %}
 
 
 {% block body %}
 
-{% with posts.0 as latest %}
-<article 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>
-</article>
-{% 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 %}
 
 
-<section class="older post-list">
-<ul>
-{% for post in posts|slice:"1:" %}
-<li><a href="{{ post.get_absolute_url }}">{{ post.title }}&nbsp;<span>{{ post.creation_timestamp|date:"Y/m/d" }}</span></a></li>
-{% endfor %}
-<li><a href="/archives/">...</a></li>
-</ul>
-</section>
-
-<nav>
-<a href="tag/radio/">Radio <span>(Panik &amp; ailleurs)</span></a>
-<a href="tag/code/">Logiciel libre <span>(Debian, GNOME &amp; 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 }}&nbsp;<span>{{ post.creation_timestamp|date:"Y/m/d" }}</span></a></li>
+      {% endfor %}
+      <li><a href="/archives/">...</a></li>
+    </ul>
+  </section>
 
 {% endblock %}
 
 {% endblock %}
index 0f5ca8e9abade23d16c139ccf78a22157fa58a05..935e699849f1f604a9decc9a4fa743a8c6a07345 100644 (file)
@@ -2,9 +2,9 @@
 {% load i18n %}
 
 {% block body %}
 {% 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 %}
 {% endblock %}
index 493da391b4bf36a626089dd4819cf63814bb7f8e..1d7e21d43a8d68547d21006829ef73e7e2e79653 100644 (file)
@@ -1,22 +1,33 @@
 {% extends "phyll/base.html" %}
 {% load i18n %}
 
 {% 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 %}
 {% block content-class %}post{% endblock %}
 {% block page-title %}{{ object.title }} - {{ block.super }}{% endblock %}
 
 {% block body %}
-<article>
-<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>
-</article>
+    {% 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 %}
 {% 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 %}
 {% endblock %}
index 927ae53a277048c58e64d54831f229b15f0f4be0..9d6cd442af22f1025533a0ca701d9e6cd7265af3 100644 (file)
@@ -2,14 +2,14 @@
 {% load gadjo i18n static %}
 
 {% block bottom-head %}
 {% 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 %}
 {% 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 %}
 {% endblock %}
index aedf918fbd59a279a7ebf0f8a4a0647ba599ebb4..d3818ba20e69af99cca0d283b65dddd7813420b1 100644 (file)
@@ -4,17 +4,21 @@
 {% block content-class %}post-list{% endblock %}
 
 {% block body %}
 {% 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 %}
 {% 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 %}
 {% endblock %}
index 2e89da4aa9dc2b938f130527902c698d362cb04b..280d24a87360d225beac90089505517dc32672b2 100644 (file)
@@ -2,10 +2,10 @@
 {% load i18n %}
 
 {% block body %}
 {% 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 %}
 {% endblock %}
index 34db0b493c33f427f9c4121103c0bf3e5df35365..703507d1eb09a0830727fe7983d422b0aa5b5941 100644 (file)
 # 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/>.
 
 # 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
 
 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
 
 
 from . import views
 
+staff_required = partial(staff_member_required, login_url='login')
+
+
 urlpatterns = [
 urlpatterns = [
-    url(
+    re_path(
         r'^ckeditor/upload/',
         r'^ckeditor/upload/',
-        staff_member_required(ckeditor_views.upload, login_url='login'),
+        staff_required(ckeditor_views.upload),
         name='ckeditor_upload',
     ),
         name='ckeditor_upload',
     ),
-    url(
+    re_path(
         r'^ckeditor/browse/',
         r'^ckeditor/browse/',
-        never_cache(staff_member_required(ckeditor_views.browse, login_url='login')),
+        never_cache(staff_required(ckeditor_views.browse)),
         name='ckeditor_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/$',
         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()),
 ]
 ]
index 2e77fc926842270e743d37c323eb62ef2d470b12..a7f45e77059c7c7a92c61f1814a1dc2c664583a8 100644 (file)
@@ -18,15 +18,16 @@ import os
 import urllib.parse
 
 from django.conf import settings
 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.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.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 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
 from sorl.thumbnail.shortcuts import get_thumbnail
 
 from .models import Note
@@ -48,12 +49,12 @@ class NoteView(DetailView):
                 raise Http404()
         if not note.published and not request.user.is_staff:
             raise PermissionDenied()
                 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
 
 
 class NoteEditView(UpdateView):
     model = Note
-    fields = ['title', 'slug', 'text', 'tags', 'published']
+    fields = ['title', 'slug', 'text', 'tags', 'published', 'included_in_feed']
 
 
 class NoteApiSaveView(View):
 
 
 class NoteApiSaveView(View):
@@ -68,7 +69,7 @@ class NoteApiSaveView(View):
 
 class NoteAddView(CreateView):
     model = Note
 
 class NoteAddView(CreateView):
     model = Note
-    fields = ['title', 'slug', 'text', 'tags', 'published']
+    fields = ['title', 'slug', 'text', 'tags', 'published', 'included_in_feed']
 
 
 class NoteDeleteView(DeleteView):
 
 
 class NoteDeleteView(DeleteView):
@@ -82,8 +83,14 @@ class HomeView(TemplateView):
     template_name = 'phyll/home.html'
 
     def get_context_data(self, **kwargs):
     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
 
 
         return context
 
 
@@ -91,9 +98,9 @@ class ArchivesView(ListView):
     model = Note
 
     def get_queryset(self):
     model = Note
 
     def get_queryset(self):
-        qs = super(ArchivesView, self).get_queryset()
+        qs = super().get_queryset()
         if not self.request.user.is_staff:
         if not self.request.user.is_staff:
-            qs = qs.filter(published=True)
+            qs = qs.filter(published=True, included_in_feed=True)
         return qs
 
 
         return qs
 
 
@@ -109,7 +116,7 @@ class ListOnTagView(ListView):
 
 class Atom1FeedWithBaseXml(Atom1Feed):
     def root_attributes(self):
 
 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
         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
@@ -123,10 +130,10 @@ class AtomFeed(Feed):
 
     def get_object(self, request, *args, **kwargs):
         self.sub = kwargs.get('sub', 'default')
 
     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):
 
     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':
         if self.sub == 'default':
             pass
         elif self.sub == 'gnome-en':
@@ -168,3 +175,33 @@ def ajax_upload(request, *args, **kwargs):
             except OSError:
                 pass
     return JsonResponse(response)
             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]}
+    )
diff --git a/chloro/rdio/__init__.py b/chloro/rdio/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/chloro/rdio/static/css/6138240034_68f81350a2_o-x-dark.jpg b/chloro/rdio/static/css/6138240034_68f81350a2_o-x-dark.jpg
new file mode 100644 (file)
index 0000000..f0f7b81
Binary files /dev/null and b/chloro/rdio/static/css/6138240034_68f81350a2_o-x-dark.jpg differ
diff --git a/chloro/rdio/static/css/6138240034_68f81350a2_o-x.jpg b/chloro/rdio/static/css/6138240034_68f81350a2_o-x.jpg
new file mode 100644 (file)
index 0000000..3b98267
Binary files /dev/null and b/chloro/rdio/static/css/6138240034_68f81350a2_o-x.jpg differ
diff --git a/chloro/rdio/static/css/style.scss b/chloro/rdio/static/css/style.scss
new file mode 100644 (file)
index 0000000..dd7beb8
--- /dev/null
@@ -0,0 +1,452 @@
+@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;
+       }
+}
diff --git a/chloro/rdio/static/css/yelp-note.png b/chloro/rdio/static/css/yelp-note.png
new file mode 100644 (file)
index 0000000..6c9a9fe
Binary files /dev/null and b/chloro/rdio/static/css/yelp-note.png differ
diff --git a/chloro/rdio/static/icon.png b/chloro/rdio/static/icon.png
new file mode 100644 (file)
index 0000000..f0ceb37
Binary files /dev/null and b/chloro/rdio/static/icon.png differ
diff --git a/chloro/rdio/templates/phyll/base.html b/chloro/rdio/templates/phyll/base.html
new file mode 100644 (file)
index 0000000..26522b3
--- /dev/null
@@ -0,0 +1,43 @@
+{% 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>
diff --git a/chloro/rdio/templates/phyll/home.html b/chloro/rdio/templates/phyll/home.html
new file mode 100644 (file)
index 0000000..cb49240
--- /dev/null
@@ -0,0 +1,27 @@
+{% 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 %}
diff --git a/chloro/rdio/templates/phyll/note_detail.html b/chloro/rdio/templates/phyll/note_detail.html
new file mode 100644 (file)
index 0000000..c4d7967
--- /dev/null
@@ -0,0 +1,38 @@
+{% 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 %}
index 143662f4669ce04fa3cf5e5fe490ad039bdc4101..11c840f321922bfeb943b4fc5031475b103f59d4 100644 (file)
@@ -33,6 +33,8 @@ ALLOWED_HOSTS = []
 
 # Application definition
 
 
 # Application definition
 
+DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
+
 INSTALLED_APPS = [
     'django.contrib.auth',
     'django.contrib.contenttypes',
 INSTALLED_APPS = [
     'django.contrib.auth',
     'django.contrib.contenttypes',
@@ -82,7 +84,7 @@ WSGI_APPLICATION = 'chloro.wsgi.application'
 
 DATABASES = {
     'default': {
 
 DATABASES = {
     'default': {
-        'ENGINE': 'django.db.backends.postgresql_psycopg2',
+        'ENGINE': 'django.db.backends.postgresql',
         'NAME': 'chloro',
     },
 }
         'NAME': 'chloro',
     },
 }
@@ -122,6 +124,7 @@ USE_L10N = True
 
 USE_TZ = True
 
 
 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/
 
 # Static files (CSS, JavaScript, Images)
 # https://docs.djangoproject.com/en/1.11/howto/static-files/
@@ -158,8 +161,8 @@ CKEDITOR_CONFIGS = {
     },
 }
 
     },
 }
 
-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')
 
 local_settings_file = os.environ.get(
     'CHLORO_SETTINGS_FILE', os.path.join(os.path.dirname(__file__), 'local_settings.py')
index d84a5e5ac2aeaea89658b27613a2e6353edae212..3e1cf869d1efbd9416a8ed5ee4ba9a66f1241f8f 100644 (file)
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from django.conf import settings
 # 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.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 = [
 
 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)
 
 ]
 
 # 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')))
index b3688bfed207a9511486f658843fbebf4cb74608..8dd41329ecf06c1c212d0c100a239ad5af325248 100644 (file)
@@ -11,6 +11,6 @@ import os
 
 from django.core.wsgi import get_wsgi_application
 
 
 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()
 
 application = get_wsgi_application()
index e682cdd5dd62bfc0054c3f043cea4388db835b33..848912fb697fe61e43a5714cfe14edaa84fc745c 100644 (file)
@@ -3,15 +3,15 @@ Section: python
 Priority: optional
 Maintainer: Frederic Peters <fpeters@0d.be>
 Build-Depends: debhelper-compat (= 12),
 Priority: optional
 Maintainer: Frederic Peters <fpeters@0d.be>
 Build-Depends: debhelper-compat (= 12),
-               sassc,
+               dh-python,
                python3-all,
                python3-django,
                python3-setuptools,
                python3-all,
                python3-django,
                python3-setuptools,
-               dh-python
+               sassc,
 Standards-Version: 3.9.1
 
 Package: chloro
 Architecture: all
 Standards-Version: 3.9.1
 
 Package: chloro
 Architecture: all
-Depends: ${python3:Depends}
+Depends: ${python3:Depends},
 Description: Content Manager
  Code to run 0d.be.
 Description: Content Manager
  Code to run 0d.be.
index bdca6e1df2d1f7ee76a510cf4202ffb3cfa46881..8e309bad7538a6dc715e44d9ccf231f203ae3e9c 100644 (file)
--- a/manage.py
+++ b/manage.py
@@ -2,8 +2,8 @@
 import os
 import sys
 
 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:
     try:
         from django.core.management import execute_from_command_line
     except ImportError:
@@ -15,8 +15,8 @@ if __name__ == "__main__":
         except ImportError:
             raise ImportError(
                 "Couldn't import Django. Are you sure it's installed and "
         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)
             )
         raise
     execute_from_command_line(sys.argv)
index c11cbb80cac10037bcb4b175808628c1c03fc706..01bf6b66b226aee145afb44f04de2b6fc3765fe0 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -1,17 +1,16 @@
 #! /usr/bin/env python3
 #! /usr/bin/env python3
-# -*- coding: utf-8 -*-
 
 import os
 import re
 import subprocess
 import sys
 
 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.command.build import build as _build
 from distutils.command.sdist import sdist
-from distutils.cmd import Command
 from distutils.spawn import find_executable
 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):
 
 
 class eo_sdist(sdist):
@@ -31,7 +30,7 @@ def get_version():
     tag exists, take 0.0- and add the length of the commit log.
     """
     if os.path.exists('VERSION'):
     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(
             return v.read()
     if os.path.exists('.git'):
         p = subprocess.Popen(