From: Frédéric Péters Date: Sun, 17 Dec 2023 16:44:57 +0000 (+0100) Subject: do not include non-feed posts on homepage X-Git-Url: https://git.0d.be/?p=chloro.git;a=commitdiff_plain;h=HEAD;hp=425f34e0cbf80f79f6cf6c7ab7f89ee50b4b2572 do not include non-feed posts on homepage --- diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a14705c..3c50489 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,36 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: double-quote-string-fixer - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 23.1.0 hooks: - id: black args: ['--target-version', 'py37', '--skip-string-normalization', '--line-length', '110'] +- repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort + args: ['--profile', 'black', '--line-length', '110'] +- repo: https://github.com/asottile/pyupgrade + rev: v3.3.1 + hooks: + - id: pyupgrade + args: ['--keep-percent-format', '--py37-plus'] +- repo: https://github.com/adamchainz/django-upgrade + rev: 1.13.0 + hooks: + - id: django-upgrade + args: ['--target-version', '3.2'] +- repo: https://github.com/rtts/djhtml + rev: '3.0.5' + hooks: + - id: djhtml + args: ['--tabwidth', '2'] +- repo: https://git.entrouvert.org/pre-commit-debian.git + rev: v0.3 + hooks: + - id: pre-commit-debian diff --git a/MANIFEST.in b/MANIFEST.in index eac17a9..f5d77a8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,8 @@ include COPYING include MANIFEST.in -recursive-include chloro/phyll/static *.css *.scss *.js *.json *.png +recursive-include chloro/locale *.po *.mo +recursive-include chloro/phyll/static *.css *.scss *.js *.json *.png *.jpg recursive-include chloro/phyll/templates *.html +recursive-include chloro/rdio/static *.css *.scss *.js *.json *.png *.jpg +recursive-include chloro/rdio/templates *.html diff --git a/chloro/locale/fr/LC_MESSAGES/django.po b/chloro/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000..bd95153 --- /dev/null +++ b/chloro/locale/fr/LC_MESSAGES/django.po @@ -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 , 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 \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" diff --git a/chloro/monkeypatch.py b/chloro/monkeypatch.py index c0c7142..7802236 100644 --- a/chloro/monkeypatch.py +++ b/chloro/monkeypatch.py @@ -14,18 +14,17 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -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.utils.encoding import force_text +from django.urls import reverse +from django.utils.encoding import force_str from django.utils.html import conditional_escape from django.utils.safestring import mark_safe from django.utils.translation import get_language -import ckeditor.views -import ckeditor.widgets -from ckeditor.image import pillow_backend - def ckeditor_render(self, name, value, attrs=None, 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_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( @@ -52,7 +51,7 @@ def ckeditor_render(self, name, value, attrs=None, renderer=None): '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), diff --git a/chloro/phyll/__init__.py b/chloro/phyll/__init__.py index 93e2837..10560b6 100644 --- a/chloro/phyll/__init__.py +++ b/chloro/phyll/__init__.py @@ -13,5 +13,3 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . - -default_app_config = 'chloro.phyll.apps.AppConfig' diff --git a/chloro/phyll/fields.py b/chloro/phyll/fields.py index 2ce4b91..7bd2ddb 100644 --- a/chloro/phyll/fields.py +++ b/chloro/phyll/fields.py @@ -14,9 +14,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.conf import settings - import ckeditor.fields +from django.conf import settings class RichTextField(ckeditor.fields.RichTextField): @@ -28,12 +27,12 @@ class RichTextField(ckeditor.fields.RichTextField): 'external_plugin_resources': self.external_plugin_resources, } defaults.update(kwargs) - return super(RichTextField, self).formfield(**defaults) + return super().formfield(**defaults) class RichTextFormField(ckeditor.fields.RichTextFormField): def clean(self, value): - value = super(RichTextFormField, self).clean(value) + value = super().clean(value) if settings.LANGUAGE_CODE.startswith('fr-'): # apply some basic typographic rules value = value.replace('« ', '«\u202f') diff --git a/chloro/phyll/management/commands/makemessages.py b/chloro/phyll/management/commands/makemessages.py new file mode 100644 index 0000000..790e8c3 --- /dev/null +++ b/chloro/phyll/management/commands/makemessages.py @@ -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 . + +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 index 0000000..964fefc --- /dev/null +++ b/chloro/phyll/management/commands/reindex.py @@ -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 . + + +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() diff --git a/chloro/phyll/migrations/0001_initial.py b/chloro/phyll/migrations/0001_initial.py index 32c3af7..0fbfc65 100644 --- a/chloro/phyll/migrations/0001_initial.py +++ b/chloro/phyll/migrations/0001_initial.py @@ -1,14 +1,12 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.11.17 on 2019-12-29 13:29 -from __future__ import unicode_literals -import chloro.phyll.fields -from django.db import migrations, models import taggit.managers +from django.db import migrations, models +import chloro.phyll.fields -class Migration(migrations.Migration): +class Migration(migrations.Migration): initial = True dependencies = [ diff --git a/chloro/phyll/migrations/0002_auto_20191229_1932.py b/chloro/phyll/migrations/0002_auto_20191229_1932.py index 0620b4d..49d0b53 100644 --- a/chloro/phyll/migrations/0002_auto_20191229_1932.py +++ b/chloro/phyll/migrations/0002_auto_20191229_1932.py @@ -1,12 +1,9 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.11.17 on 2019-12-29 18:32 -from __future__ import unicode_literals from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ('phyll', '0001_initial'), ] diff --git a/chloro/phyll/migrations/0003_note_published.py b/chloro/phyll/migrations/0003_note_published.py index 7c5f05e..a99cb36 100644 --- a/chloro/phyll/migrations/0003_note_published.py +++ b/chloro/phyll/migrations/0003_note_published.py @@ -1,12 +1,9 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.11.17 on 2019-12-29 18:39 -from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ('phyll', '0002_auto_20191229_1932'), ] diff --git a/chloro/phyll/migrations/0004_note_plain_text.py b/chloro/phyll/migrations/0004_note_plain_text.py new file mode 100644 index 0000000..f02481c --- /dev/null +++ b/chloro/phyll/migrations/0004_note_plain_text.py @@ -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 index 0000000..c1aded6 --- /dev/null +++ b/chloro/phyll/migrations/0005_set_plain_text.py @@ -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 index 0000000..28fccdd --- /dev/null +++ b/chloro/phyll/migrations/0006_interlink.py @@ -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 index 0000000..5faefc7 --- /dev/null +++ b/chloro/phyll/migrations/0007_note_included_in_feed.py @@ -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'), + ), + ] diff --git a/chloro/phyll/models.py b/chloro/phyll/models.py index 1f9914b..6642771 100644 --- a/chloro/phyll/models.py +++ b/chloro/phyll/models.py @@ -14,8 +14,13 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import html +import re +import urllib.parse + from django.db import models, transaction -from django.utils.translation import ugettext_lazy as _ +from django.utils.html import strip_tags +from django.utils.translation import gettext_lazy as _ from taggit.managers import TaggableManager from .fields import RichTextField @@ -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) + plain_text = models.TextField(blank=True, null=True) tags = TaggableManager(_('Tags'), blank=True) + included_in_feed = models.BooleanField(_('Include in feed'), default=True) published = models.BooleanField(_('Published'), default=True) creation_timestamp = models.DateTimeField(auto_now_add=True) last_update_timestamp = models.DateTimeField(auto_now=True) @@ -34,4 +41,42 @@ class Note(models.Model): ordering = ['-creation_timestamp'] def get_absolute_url(self): + if self.slug == 'index': + return '/' return '/%s/' % self.slug + + def lang(self): + if self.tags.filter(name='lang-en').exists(): + return 'en' + return 'fr' + + def save(self, *args, **kwargs): + if kwargs.get('update_fields') is None or 'plain_text' in kwargs.get('update_fields'): + self.plain_text = re.sub( + r"[’'\"«»,;:\. ]", ' ', html.unescape(strip_tags(self.text)).replace('’', ' ') + ) + if not kwargs.get('update_fields'): + self.maintain_interlinks() + return super().save(*args, **kwargs) + + def maintain_interlinks(self): + links = re.findall(r'href="(.*?)"', self.text or '') + with transaction.atomic(): + Interlink.objects.filter(note1=self).delete() + for link in links: + parsed = urllib.parse.urlparse(link) + if parsed.netloc != '': + continue + path_parts = parsed.path.strip('/').split() + if len(path_parts) != 1: + continue + try: + note2 = Note.objects.get(slug=path_parts[0]) + Interlink.objects.create(note1=self, note2=note2) + except Note.DoesNotExist: + pass + + +class Interlink(models.Model): + note1 = models.ForeignKey(Note, on_delete=models.SET_NULL, null=True, related_name='+') + note2 = models.ForeignKey(Note, on_delete=models.SET_NULL, null=True, related_name='linking_note') diff --git a/chloro/phyll/static/css/style.scss b/chloro/phyll/static/css/style.scss index 8bb69bb..95ca6ca 100644 --- a/chloro/phyll/static/css/style.scss +++ b/chloro/phyll/static/css/style.scss @@ -1,25 +1,58 @@ @charset "UTF-8"; -$text-color: #222; -$link-color: #0000ee; -$visited-link-color: #551a8b; +html { + --body-background: white; + --header-background: #3c113e; + --header-color: white; + --text-background: white; + --text-color: #222; + --light-text-color: lighten(#222, 20%); + --lighter-text-color: lighten(#222, 30%); + --link-color: #0000ee; + --visited-link-color: #551a8b; + --gray: gray; + --note-background: #fbf7c1; + --blockquote-background: #eeeefd; +} + +@media (prefers-color-scheme: dark) { + html { + --body-background: #113; + --header-background: black; + --text-background: #113; + --text-color: #eef; + --light-text-color: lighten(#eef, 20%); + --lighter-text-color: lighten(#eef2, 30%); + --link-color: #aaf; + --visited-link-color: #b767ff; + --gray: gray; + --note-background: #867f11; + --blockquote-background: #223; + } +} @import "opensans"; body { font-family: "Open Sans", sans-serif; - background: white; - color: $text-color; + background: var(--body-background); + color: var(--text-color); font-size: 100%; + padding: 0; + margin: 0; + display: flex; + @media screen and (max-width: 50em) { + flex-direction: column; + } } a { - color: $link-color; + color: var(--link-color); text-decoration: none; - border-bottom: 0.1em dotted $link-color; + border-bottom: 0.1em dotted var(--link-color); &:visited { - color: $visited-link-color; - border-color: $visited-link-color; + color: var(--visited-link-color); + border-color: var(--visited-link-color); } &:hover { border-bottom-style: solid; @@ -27,30 +60,88 @@ a { } header { + background: var(--header-background); + color: var(--header-color); display: inline-block; - h1 { + .header-title { + font-size: 2em; font-weight: normal; text-transform: uppercase; - margin: 1em 0 0 1em; + margin: 4rem 1rem 2rem 1rem; padding: 0 0.5em; - color: lighten($text-color, 20%); - background: #fafaff; display: inline-block; - a { - border: none; + transform: rotate(-2deg); + } + a { + color: inherit; + border-color: var(--header-color); + &:visited { + color: inherit; + border-color: inherit; } } - transform: rotate(-2deg); position: relative; z-index: 2; + max-width: 20em; + min-height: 100vh; + p.contact { + margin: 1rem 2rem; + } + nav { + font-weight: bold; + ul { + margin: 1rem 2rem; + padding: 0; + list-style: none; + li { + margin: 0; + padding: 0; + } + } + span { + display: block; + font-size: 90%; + font-weight: normal; + } + } + .about { + margin-top: 1em; + } + @media screen and (max-width: 50em) { + .header-title { + margin-top: 1em; + } + max-width: inherit; + min-height: inherit; + nav { + ul { + margin-top: 0; + display: flex; + flex-wrap: wrap; + list-style: none; + gap: 1em; + } + span { + display: none; + } + } + .about { + margin-top: 0; + } + } } div.actions { position: absolute; top: 0.5rem; - left: 1rem; + right: 1rem; z-index: 100; a { + background: white; + padding: 2px; + border: 1px solid black; + border-radius: 3px; + color: black; margin-right: 1rem; } } @@ -66,63 +157,33 @@ main, footer { main { position: relative; - background: #fafaff; + background: var(--text-background); min-height: 70vh; - clip-path: polygon(0px 0px, 96.35% -24px, 101.99% 7.86%, 100.61% 103.56%, 10% 100%, 0% 100%, 0px 0px); + flex: 1; } -.home { - nav a { - text-transform: uppercase; - display: inline-block; - text-decoration: none; - font-size: 500%; +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; - 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 { - border: 1px solid gray; + border: 1px solid var(--gray); padding: 3px; max-width: 90%; max-height: 70vh; @@ -140,26 +201,33 @@ main { background: #111; color: white; padding: 2px; - } - div.meta { - margin-top: 3em; - color: lighten($text-color, 40%); + overflow: auto; } div.note { - background: #fbf7c1;; + background: var(--note-background); padding: 0.2em 0.5em 0.2em 2em; p { margin: 0.5em 0; } } blockquote { - background: #eeeefd; + background: var(--blockquote-background); padding: 0.1em 1em; clip-path: polygon(0px 0px, 94.27% 2px, 99.63% 2.65%, 98.39% 97.48%, 10% 100%, 0% 100%, 0px 0px); } } .post-list { + margin-top: 2em; + h1 { + margin: 0; + font-size: 400%; + @media screen and (max-width: 50em) { + font-size: 200%; + } + font-weight: bold; + text-transform: uppercase; + } ul, li { margin: 0; padding: 0; @@ -186,8 +254,8 @@ main { .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; @@ -207,7 +275,7 @@ footer { } div[contenteditable=true]:focus-within { - outline: 1px solid gray; + outline: 1px solid var(--gray); outline-offset: 3px; } @@ -259,9 +327,52 @@ div[contenteditable=true]:focus-within { } } -button#save { +#quickedit { position: sticky; bottom: 10px; + display: flex; + justify-content: flex-end; + label { + display: inline-block; + margin-left: 6px; + cursor: pointer; + } + input { + display: none; + } + + button, + span { + display: inline-block; + font-weight: normal; + background: white; + border: 1px solid #386ede; + box-shadow: 0 0 0 5px white; + padding: 1ex; + border-radius: 3px; + color: #386ede; + font-size: inherit; + letter-spacing: inherit; + line-height: inherit; + } + button:hover { + &:hover { + color: white; + background: #386ede; + } + } + button.error { + background: red; + color: white; + &:hover { + background: darken(red, 10%); + } + } + + input:checked + span { + background: #386ede; + color: white; + } } main.post { @@ -284,3 +395,11 @@ main.post { } } } + +.wiki-anchor-auto { + display: none; +} + +#image-upload, #document-upload { + display: none; +} diff --git a/chloro/phyll/static/js/chloro.js b/chloro/phyll/static/js/chloro.js index 816d022..ecc6c97 100644 --- a/chloro/phyll/static/js/chloro.js +++ b/chloro/phyll/static/js/chloro.js @@ -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())); + $('¶').appendTo($elem); + }); +} + +function create_toc() { + $('#toc').remove(); + if ($('article h2').length == 0) return; + $div_toc = $('
    '); + $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 = $('', {href: '#' + slug, text: $elem.text().replace(/¶$/, '')}); + var $li_title = $('
  • '); + $li_title[0].related_position = $(elem).position().top; + li_titles.push($li_title[0]); + $a_title.appendTo($li_title); + $li_title.appendTo($div_toc_ul); + }); + $('article h1').first().after($div_toc); + + li_titles = li_titles.reverse(); + + $(window).on('load', function() { + // update positions after images have been loaded + $('article h2').each(function(idx, elem) { + $('#toc li')[idx].related_position = $(elem).position().top; + }); + $(window).trigger('scroll'); + }); + + var scroll_timeout_id = null; + $(window).on('scroll', function() { + if (scroll_timeout_id) clearTimeout(scroll_timeout_id); + scroll_timeout_id = setTimeout(function() { // throttle + scroll_timeout_id = null; + var current_position = window.scrollY; + $('#toc li').removeClass('active'); + for (const li_title of li_titles) { + if (li_title.related_position < current_position - 25) { + $(li_title).addClass('active'); + break; + } + } + }, 50); + }); +}; + (function(window, document, undefined) { var Phylly = { BLOCKS: [ + {name: 'intertitle', tag: 'H2', klass: 'intertitle'}, {name: 'code', tag: 'PRE', klass: 'screen'}, + {name: 'list', special: 'list', tag: 'UL', klass: 'list'}, {name: 'figure', special: 'img', tag: 'DIV', subtag: true, klass: 'figure'}, {name: 'note', tag: 'DIV', subtag: true, klass: 'note'}, {name: 'quote', tag: 'BLOCKQUOTE', subtag: true, klass: 'quote'}, @@ -96,6 +178,11 @@ } 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 = $(''); $image_upload.on('change', upload_image); @@ -159,7 +246,7 @@ 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) { @@ -175,7 +262,7 @@ 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; @@ -348,20 +435,24 @@ var params = {}; params.title = text; params.request_id = request_id; - $.post('/wiki/ajax/newpage/', params).success(function(data) { + $.post('/ajax/newpage/', params).done(function(data) { $('a[data-request-id=' + data.request_id + ']').attr('href', data.url).removeAttr('data-request-id'); }); param = $new_link[0].outerHTML; } if (action == 'createLink') { var sel = window.getSelection(); + var selected_link = get_parent(sel.anchorNode, 'A'); + if (sel.anchorNode.nodeType == Node.TEXT_NODE) { + if (sel.anchorNode.length == sel.anchorOffset && sel.anchorNode.nextSibling.nodeName == 'A') { + selected_link = sel.anchorNode.nextSibling; + } + } var $input = $('input[name=link-target]'); $input[0]._range = sel.getRangeAt(0); - if (sel.anchorNode instanceof Element) { - var elem = sel.anchorNode.childNodes[sel.anchorOffset]; - if (elem.tagName == 'A') { - $input.val(elem.href); - } + if (selected_link) { + $input[0]._selected_link = selected_link; + $input.val(selected_link.href); } $input.addClass('shown'); $input.focus(); @@ -378,13 +469,25 @@ $input.removeClass('shown'); var sel = window.getSelection(); sel.addRange(this._range); + var selected_link = $input[0]._selected_link; if (url) { - document.execCommand('createLink', false, url); + if (selected_link) { + selected_link.href = url; + } else { + var $new_link = $('', {text: sel.toString(), href: url}); + this._range.deleteContents(); + this._range.insertNode($new_link[0]); + sel.empty(); + sel.collapse($new_link[0]); + sel.empty(); + } } else { - document.execCommand('unlink', false, null); + if (selected_link) { + selected_link.replaceWith(document.createTextNode(selected_link.textContent)); + } } - sel.empty(); $input.val(''); + $input[0]._selected_link = null; } } function focusout_link(ev) { @@ -402,6 +505,7 @@ '' + '' + '' + + '' + '' + '' + ''); @@ -416,18 +520,86 @@ 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 = $('
    ') + figure_toolbar.hide(); + figure_toolbar.insertAfter(document.body); + $('[name="figure-alt"]').on('change', function() { + $(this.img).attr('alt', $(this).val()); + }); + } + figure_toolbar.css('position', 'absolute'); + var pos = sel.getRangeAt(0).getClientRects()[0]; + figure_toolbar.css('top', pos.bottom + window.scrollY); + figure_toolbar.css('left', pos.left + window.scrollX); + figure_toolbar.show(); + $('[name="figure-alt"]')[0].img = $(sel.anchorNode).find('img'); + $('[name="figure-alt"]').val($('[name="figure-alt"]')[0].img.attr('alt') || ''); + }; + }(window, document)); $(function() { - Phylly.init(), - $('div[contenteditable]').each(function(i, elem) {Phylly.bind_events(elem)}); - $('#save').on('click', function() { - var text = $('div[contenteditable]')[0].innerHTML; - var csrf = $('[name=csrfmiddlewaretoken]').val(); - $.post('api-save/', - { text: text, csrfmiddlewaretoken: csrf} - ).fail(function() { - $('#save').css('background', 'red'); + $('#quickedit input').on('change', function() { + var enable = $(this).is(':checked'); + if (enable) { + remove_auto_anchors(); + $('div[data-editable]').each(function(i, elem) { + $(elem).attr('contenteditable', 'true'); + var $button = $(''); + $button[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 = $('', {text: hit.title, href: hit.url}); + var $li_hit = $('
  • '); + $li_hit.append($a_hit); + $('.search-results').append($li_hit); + } }); return false; }); diff --git a/chloro/phyll/templates/phyll/base.html b/chloro/phyll/templates/phyll/base.html index e7449de..799af98 100644 --- a/chloro/phyll/templates/phyll/base.html +++ b/chloro/phyll/templates/phyll/base.html @@ -1,5 +1,5 @@ {% load gadjo i18n %} - + @@ -9,29 +9,35 @@ {% if request.user.is_staff %} - - + + {% endif %} {% block bottom-head %} {% endblock %}
    -

    Coin web de Frédéric Péters

    + + +

    fpeters@0d.be

    {% block body %} {% endblock %}
    - - {% if request.user.is_staff %} -
    - {% trans "New Note" %} - {% block bottom-actions %} - {% endblock %} -
    - {% endif %} + {% if request.user.is_staff %} +
    + {% trans "New Note" %} + {% block bottom-actions %} + {% endblock %} +
    + {% endif %} diff --git a/chloro/phyll/templates/phyll/home.html b/chloro/phyll/templates/phyll/home.html index 54cfe19..7bed482 100644 --- a/chloro/phyll/templates/phyll/home.html +++ b/chloro/phyll/templates/phyll/home.html @@ -4,30 +4,24 @@ {% block body %} -{% with posts.0 as latest %} -
    -

    {{ latest.title }}

    -
    {{ latest.creation_timestamp|date:"j E Y, H:i"|lower }}
    -
    -{{ latest.text|safe }} -
    -
    -{% endwith %} + {% with posts.0 as latest %} +
    +

    {{ latest.title }}

    +
    {{ latest.creation_timestamp|date:"j E Y, H:i"|lower }}
    +
    + {{ latest.text|safe }} +
    +
    + {% endwith %} -
    - -
    - - +
    + Avant ça : + +
    {% endblock %} diff --git a/chloro/phyll/templates/phyll/note_confirm_delete.html b/chloro/phyll/templates/phyll/note_confirm_delete.html index 0f5ca8e..935e699 100644 --- a/chloro/phyll/templates/phyll/note_confirm_delete.html +++ b/chloro/phyll/templates/phyll/note_confirm_delete.html @@ -2,9 +2,9 @@ {% load i18n %} {% block body %} -
    -{% csrf_token %} -{% trans "Delete?" %} - -
    +
    + {% csrf_token %} + {% trans "Delete?" %} + +
    {% endblock %} diff --git a/chloro/phyll/templates/phyll/note_detail.html b/chloro/phyll/templates/phyll/note_detail.html index 493da39..1d7e21d 100644 --- a/chloro/phyll/templates/phyll/note_detail.html +++ b/chloro/phyll/templates/phyll/note_detail.html @@ -1,22 +1,33 @@ {% 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 %} -
    -

    {{ object.title }}

    -
    {{ object.text|safe }}
    -{% if request.user.is_staff %} -{% csrf_token %} -{% endif %} +
    +

    {{ object.title }}

    + {% if object.included_in_feed %} +
    {{ object.creation_timestamp|date:"j E Y, H:i"|lower }}
    + {% endif %} +
    {{ object.text|safe }}
    -
    {{ object.creation_timestamp|date:"j E Y, H:i"|lower }}
    -
    + {% if request.user.is_staff %} + {% csrf_token %} +
    + +
    + {% endif %} + + {% if not object.included_in_feed %} +
    Dernière mise à jour : {{ object.last_update_timestamp|date:"j E Y, H:i"|lower }}
    + {% endif %} + +
    {% endblock %} {% block bottom-actions %} -{% trans "Edit" %} -{% trans "Delete" %} + {% trans "Edit" %} + {% trans "Delete" %} {% endblock %} diff --git a/chloro/phyll/templates/phyll/note_form.html b/chloro/phyll/templates/phyll/note_form.html index 927ae53..9d6cd44 100644 --- a/chloro/phyll/templates/phyll/note_form.html +++ b/chloro/phyll/templates/phyll/note_form.html @@ -2,14 +2,14 @@ {% load gadjo i18n static %} {% block bottom-head %} - - + + {% endblock %} {% block body %} -
    -{% csrf_token %} -{{ form.as_p }} - -
    +
    + {% csrf_token %} + {{ form.as_p }} + +
    {% endblock %} diff --git a/chloro/phyll/templates/phyll/note_list.html b/chloro/phyll/templates/phyll/note_list.html index aedf918..d3818ba 100644 --- a/chloro/phyll/templates/phyll/note_list.html +++ b/chloro/phyll/templates/phyll/note_list.html @@ -4,17 +4,21 @@ {% block content-class %}post-list{% endblock %} {% block body %} -
    -

    {{ view.kwargs.tag }}

    - -
    +
    + {% if view.kwargs.tag %} +

    {{ view.kwargs.tag }}

    + {% else %} +

    Archives

    + {% endif %} + +
    {% endblock %} {% block bottom-actions %} -{% trans "Edit" %} -{% trans "Delete" %} + {% trans "Edit" %} + {% trans "Delete" %} {% endblock %} diff --git a/chloro/phyll/templates/registration/login.html b/chloro/phyll/templates/registration/login.html index 2e89da4..280d24a 100644 --- a/chloro/phyll/templates/registration/login.html +++ b/chloro/phyll/templates/registration/login.html @@ -2,10 +2,10 @@ {% load i18n %} {% block body %} -
    -{% csrf_token %} -{{ form.as_p }} - - -
    +
    + {% csrf_token %} + {{ form.as_p }} + + +
    {% endblock %} diff --git a/chloro/phyll/urls.py b/chloro/phyll/urls.py index 34db0b4..703507d 100644 --- a/chloro/phyll/urls.py +++ b/chloro/phyll/urls.py @@ -14,38 +14,46 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.conf.urls import url -from django.contrib.admin.views.decorators import staff_member_required -from django.views.decorators.cache import never_cache +from functools import partial import ckeditor.views as ckeditor_views +from django.contrib.admin.views.decorators import staff_member_required +from django.urls import path, re_path +from django.views.decorators.cache import never_cache from . import views +staff_required = partial(staff_member_required, login_url='login') + + urlpatterns = [ - url( + re_path( r'^ckeditor/upload/', - staff_member_required(ckeditor_views.upload, login_url='login'), + staff_required(ckeditor_views.upload), name='ckeditor_upload', ), - url( + re_path( r'^ckeditor/browse/', - never_cache(staff_member_required(ckeditor_views.browse, login_url='login')), + never_cache(staff_required(ckeditor_views.browse)), name='ckeditor_browse', ), - url(r'^(?P[\w:-]+)/edit/$', staff_member_required(views.NoteEditView.as_view(), login_url='login')), - url( + re_path(r'^(?P[\w:-]+)/edit/$', staff_required(views.NoteEditView.as_view())), + re_path( r'^(?P[\w:-]+)/delete/$', - staff_member_required(views.NoteDeleteView.as_view(), login_url='login'), + staff_required(views.NoteDeleteView.as_view()), + ), + re_path(r'^(?P[\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[\w:-]+)/atom$', views.AtomFeed()), + path('feed/atom', views.AtomFeed()), + re_path(r'^tag/(?P[\w:-]+)/$', views.ListOnTagView.as_view()), + path('archives/', views.ArchivesView.as_view()), + re_path( + r'^(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P[\w:-]+)/$', views.NoteView.as_view() ), - url(r'^(?P[\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[\w:-]+)/atom$', views.AtomFeed()), - url(r'^feed/atom$', views.AtomFeed()), - url(r'^tag/(?P[\w:-]+)/$', views.ListOnTagView.as_view()), - url(r'^archives/$', views.ArchivesView.as_view()), - url(r'^(?P\d{4})/(?P\d{2})/(?P\d{2})/(?P[\w:-]+)/$', views.NoteView.as_view()), - url(r'^(?P[\w:-]+)/$', views.NoteView.as_view()), - url(r'^$', views.HomeView.as_view()), + re_path(r'^(?P[\w:-]+)/$', views.NoteView.as_view()), + path('', views.HomeView.as_view()), ] diff --git a/chloro/phyll/views.py b/chloro/phyll/views.py index 2e77fc9..a7f45e7 100644 --- a/chloro/phyll/views.py +++ b/chloro/phyll/views.py @@ -18,15 +18,16 @@ import os import urllib.parse from django.conf import settings +from django.contrib.postgres.search import SearchQuery, SearchRank, SearchVector from django.contrib.syndication.views import Feed from django.core.exceptions import PermissionDenied from django.core.files.storage import default_storage -from django.http import HttpResponse, Http404, JsonResponse +from django.http import Http404, HttpResponse, JsonResponse from django.utils.feedgenerator import Atom1Feed +from django.utils.text import slugify from django.views import View from django.views.decorators.csrf import csrf_exempt -from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView, TemplateView - +from django.views.generic import CreateView, DeleteView, DetailView, ListView, TemplateView, UpdateView from sorl.thumbnail.shortcuts import get_thumbnail from .models import Note @@ -48,12 +49,12 @@ class NoteView(DetailView): raise Http404() if not note.published and not request.user.is_staff: raise PermissionDenied() - return super(NoteView, self).get(request, *args, **kwargs) + return super().get(request, *args, **kwargs) class NoteEditView(UpdateView): model = Note - fields = ['title', 'slug', 'text', 'tags', 'published'] + fields = ['title', 'slug', 'text', 'tags', 'published', 'included_in_feed'] class NoteApiSaveView(View): @@ -68,7 +69,7 @@ class NoteApiSaveView(View): class NoteAddView(CreateView): model = Note - fields = ['title', 'slug', 'text', 'tags', 'published'] + fields = ['title', 'slug', 'text', 'tags', 'published', 'included_in_feed'] class NoteDeleteView(DeleteView): @@ -82,8 +83,14 @@ class HomeView(TemplateView): 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 @@ -91,9 +98,9 @@ class ArchivesView(ListView): model = Note def get_queryset(self): - qs = super(ArchivesView, self).get_queryset() + qs = super().get_queryset() if not self.request.user.is_staff: - qs = qs.filter(published=True) + qs = qs.filter(published=True, included_in_feed=True) return qs @@ -109,7 +116,7 @@ class ListOnTagView(ListView): 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 @@ -123,10 +130,10 @@ class AtomFeed(Feed): def get_object(self, request, *args, **kwargs): self.sub = kwargs.get('sub', 'default') - return super(AtomFeed, self).get_object(request, *args, **kwargs) + return super().get_object(request, *args, **kwargs) def items(self): - qs = Note.objects.filter(published=True) + qs = Note.objects.filter(published=True, included_in_feed=True) if self.sub == 'default': pass elif self.sub == 'gnome-en': @@ -168,3 +175,33 @@ def ajax_upload(request, *args, **kwargs): 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 = '

    ...

    ' + 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 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 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 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 index 0000000..dd7beb8 --- /dev/null +++ b/chloro/rdio/static/css/style.scss @@ -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 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 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 index 0000000..26522b3 --- /dev/null +++ b/chloro/rdio/templates/phyll/base.html @@ -0,0 +1,43 @@ +{% load gadjo i18n %} + + + + + {% block page-title %}rdio.space - panikdb - gestion radio{% endblock %} + + + + + + {% block bottom-head %} + {% endblock %} + + +
    + PanikDB - gestion radio +
    +
    + + {% block body %} + {% endblock %} +
    + + + diff --git a/chloro/rdio/templates/phyll/home.html b/chloro/rdio/templates/phyll/home.html new file mode 100644 index 0000000..cb49240 --- /dev/null +++ b/chloro/rdio/templates/phyll/home.html @@ -0,0 +1,27 @@ +{% extends "phyll/base.html" %} + +{% block content-class %}home{% endblock %} + +{% block body %} + +
    +

    {{ index.title }}

    + {{ index.text|safe }} +
    + + {% if request.user.is_staff %}

    (page éditable)

    {% endif %} + +
    +

    Dernières pages modifiées

    +
      + {% for page in recent_changes|slice:":10" %} +
    • {{ page.title }} + le {{ page.last_update_timestamp|date:"j E Y, H:i"|lower }}
    • + {% endfor %} +
    +
    + +{% 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 index 0000000..c4d7967 --- /dev/null +++ b/chloro/rdio/templates/phyll/note_detail.html @@ -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 %} +
    +

    {{ object.title }}

    +
    {{ object.text|safe }}
    + +
    Dernière modification : {{ object.last_update_timestamp|date:"j E Y, H:i"|lower }}
    +
    + + {% if object.linking_note.all.exists %} + + {% endif %} + + {% if request.user.is_staff %} + {% csrf_token %} +
    + +
    + {% endif %} + +{% endblock %} + +{% block bottom-actions %} + - + {% trans "Edit" %} + - + {% trans "Delete" %} +{% endblock %} diff --git a/chloro/settings.py b/chloro/settings.py index 143662f..11c840f 100644 --- a/chloro/settings.py +++ b/chloro/settings.py @@ -33,6 +33,8 @@ ALLOWED_HOSTS = [] # Application definition +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' + INSTALLED_APPS = [ 'django.contrib.auth', 'django.contrib.contenttypes', @@ -82,7 +84,7 @@ WSGI_APPLICATION = 'chloro.wsgi.application' DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'chloro', }, } @@ -122,6 +124,7 @@ USE_L10N = 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/ @@ -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') diff --git a/chloro/urls.py b/chloro/urls.py index d84a5e5..3e1cf86 100644 --- a/chloro/urls.py +++ b/chloro/urls.py @@ -15,18 +15,18 @@ # along with this program. If not, see . from django.conf import settings -from django.conf.urls import include, url from django.conf.urls.static import static from django.contrib.auth import views as auth_views from django.contrib.staticfiles.urls import staticfiles_urlpatterns +from django.urls import include, path urlpatterns = [ - url(r'^accounts/login/$', auth_views.LoginView.as_view(), name='login'), - url(r'^accounts/logout/$', auth_views.LogoutView.as_view(), name='logout'), + path('accounts/login/', auth_views.LoginView.as_view(), name='login'), + path('accounts/logout/', auth_views.LogoutView.as_view(), name='logout'), ] # static and media files urlpatterns += staticfiles_urlpatterns() urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) -urlpatterns.append(url(r'', include('chloro.phyll.urls'))) +urlpatterns.append(path('', include('chloro.phyll.urls'))) diff --git a/chloro/wsgi.py b/chloro/wsgi.py index b3688bf..8dd4132 100644 --- a/chloro/wsgi.py +++ b/chloro/wsgi.py @@ -11,6 +11,6 @@ import os 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() diff --git a/debian/control b/debian/control index e682cdd..848912f 100644 --- a/debian/control +++ b/debian/control @@ -3,15 +3,15 @@ Section: python Priority: optional Maintainer: Frederic Peters Build-Depends: debhelper-compat (= 12), - sassc, + dh-python, python3-all, python3-django, python3-setuptools, - dh-python + sassc, Standards-Version: 3.9.1 Package: chloro Architecture: all -Depends: ${python3:Depends} +Depends: ${python3:Depends}, Description: Content Manager Code to run 0d.be. diff --git a/manage.py b/manage.py index bdca6e1..8e309ba 100644 --- a/manage.py +++ b/manage.py @@ -2,8 +2,8 @@ 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: @@ -15,8 +15,8 @@ if __name__ == "__main__": 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) diff --git a/setup.py b/setup.py index c11cbb8..01bf6b6 100644 --- a/setup.py +++ b/setup.py @@ -1,17 +1,16 @@ #! /usr/bin/env python3 -# -*- coding: utf-8 -*- import os import re import subprocess import sys - -from setuptools.command.install_lib import install_lib as _install_lib +from distutils.cmd import Command from distutils.command.build import build as _build from distutils.command.sdist import sdist -from distutils.cmd import Command from distutils.spawn import find_executable -from setuptools import setup, find_packages + +from setuptools import find_packages, setup +from setuptools.command.install_lib import install_lib as _install_lib class eo_sdist(sdist): @@ -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'): - with open('VERSION', 'r') as v: + with open('VERSION') as v: return v.read() if os.path.exists('.git'): p = subprocess.Popen(