diff --git a/README.md b/README.md index cfe330a..50b98ce 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,10 @@ Otherwise, you have to configure the following settings : - `JFME_DOMAIN` : the domain name of your website, for instance `"https://www.example.com"` (used in sitemap file) - `JFME_CONTENT_DIRS` : a list of directories where to look for the site content +Other useful settings : +- Default metadata : `JFME_DEFAULT_METADATA_DICT` and `JFME_DEFAULT_METADATA_FILEPATH` allow to set default metadata for pages and posts. The first one is a python dictionary and the second one is a Path to a file having the same format as metadata section in pages. +The order, from less to most priority is : `JFME_DEFAULT_METADATA_DICT` then `JFME_DEFAULT_METADATA_FILEPATH` then page matadata. + ### `Dockerfile` : - In the `# Copy source dir` section, add `COPY / /` for each content directory in `JFME_CONTENT_DIRS` @@ -123,7 +127,9 @@ For each command, the option `-h` give u some help. `./manage.py distill-local` to make the static website, see [Prod](#prod) for usage - `./manage.py list-widgets` to list all widgets found in content directories + `./manage.py list-widgets` to list all widgets found in content directories. See an example in `EXAMPLES.md`. + + `./manage.py format-html ` to minify or beautify the html content (`` being `minify` or `beautify`) ## Others diff --git a/content/pages/a_page.md b/content/pages/a_page.md index f37368f..8dcc350 100644 --- a/content/pages/a_page.md +++ b/content/pages/a_page.md @@ -2,13 +2,36 @@ title Page1 slug page1 template_engine jinja2 +description --- { } --- +{% from "widgets/generic.html" import page_header %} +

Page 1

-

Go to page 2

\ No newline at end of file +

Go to page 2

+ +
+{% markdown %} +``` +This is markdown : +``` +test +**test** +`test` +{% endmarkdown %} +
+ + +{{ page_header( + markdown(' +test +**test** +`test` + ') +) }} diff --git a/content/posts/ia.md b/content/posts/ia.md new file mode 100644 index 0000000..50b4668 --- /dev/null +++ b/content/posts/ia.md @@ -0,0 +1,22 @@ +--- +title L'IA 🧠 va-t-elle remplacer la recherche web classique ? +date 2024-05-20T15:12:42+00:00 +category Technologie +abstract L'intelligence artificielle est partout. Tout le monde en parle ... Cet article vous propose de découvrir un cas intéressant et très puissant. +author Damien ACCORSI +--- +{ + +} +--- + +{%markdown%} +La question se pose ... + +Quelques mots-clé : #veilletechno #googlesearch #ia #notopensource + +Cette publication n'a pas vocation a approfondir et à répondre à la question mais plutôt à partager l'existence d'une solution intéressante et qui a été présentée à l'un des suricates comme le futur remplaçant de google search. + +![Perplexity va-t-il remplacer Google Search ?](/actualites/images/800x600/image-14219ee739c6a17b76d59f1d55b73776ea8b5e0e.png) + +{%endmarkdown%} \ No newline at end of file diff --git a/content/posts/manipuler-pdf.md b/content/posts/manipuler-pdf.md new file mode 100644 index 0000000..be3f3af --- /dev/null +++ b/content/posts/manipuler-pdf.md @@ -0,0 +1,69 @@ +--- +title Quels logiciels libres pour manipuler des fichiers PDF ? +date 2024-05-03T08:43:47+00:00 +category univers du libre +abstract Manipuler des fichiers PDF est chose courante ; il existe une série de logiciels libres qui permet de procéder à la majorité des manipulations que l'on est en droit d'attendre de tels outils. +author Damien ACCORSI +--- +{ + +} +--- + +{%markdown%} +Nous sommes tous amenés, à un moment ou à un autre, dans notre vie personnelle ou professionnelle, à manipuler des PDF. + +Tout le monde connait Acrobat Reader et autres services en ligne tels que Ilovepdf. + +Mais existe-t-il des logiciels libres pour procéder aux même opérations ? Si oui quels sont ces outils ? + + +## Consulter des PDF + +**Evince** est un visionneur de documents pour GNOME. Il permet de visualiser des PDFs et de les imprimer + +## Modifier et produire des PDF + +**Libreoffice** permet d'exporter les documents que l'on rédige en PDF. + +Moins connu, **Libreoffice Draw** permet quant à lui d'ouvrir des PDF existant et d'en éditer le contenu. + +## Découper, extraire, fusionner des fichiers PDF en masse (en ligne de commande) + +PDFtk permet facilement de découper un fichier PDF via la commande `pdftk file.pdf burst` + +Il est également possible d'extraire un lot de pages `pdftk file.pdf cat 3-5 output pages-3-to-5.pdf` + +Enfin, il est possible de concaténer des fichiers PDF, exemple : `pdftk file1.pdf file2.pdf cat output file1-file2-merged.pdf` + +D'autres opérations sont possibles. + +Pour un équivalent graphique, je vous renvoie vers PDF Chain qui m'a permis, par exemple, de découvrir qu'on pouvait intégrer des pièces jointes quelconques dans un PDF. + + +![](/actualites/images/500x500/image-b0158969619b6e2215ff928a68a0cd882228d06a.png) + +## Découper, extraire, fusionner des PDFs naturellement + +Pour un usage plus grand public, je ne peux que vous conseiller de vous tourner vers PDF Arranger. + +Il permet de procéder aux même opérations que les outils précédemment cités, avec l'avantage de travailler sur des éléments visuels car l'interface intègre une prévisualisation des pages, une sélection à la souris et des menus contextuels - bref un ensemble plus intuitif pour les utilisations qui ne sont pas familiers avec la ligne de commande. + +![](/actualites/images/500x500/image-3a7f8c0e10a091e5b7d8d5ac1d454ff27f1edf6d.png) + +---- + +Voilà, vous savez désormais quoi utiliser pour : + + +- 🖨 Imprimer un document PDF +- ⤵ tourner les pages d'un document PDF +- ✂ découper un document PDF page par page +- 🖇 fusionner plusieurs documents PDF +- 🔎 extraire des sections de pages d'un document PDF + + +Vous n'avez plus aucune excuse pour diffuser des données confidentielles sur des services en ligne "gratuits" ;) + + +{%endmarkdown%} \ No newline at end of file diff --git a/content/posts/post1.md b/content/posts/post1.md new file mode 100644 index 0000000..e4e76bd --- /dev/null +++ b/content/posts/post1.md @@ -0,0 +1,71 @@ +--- +title R&D et Veille techno - Petit point sur nos projets internes +date 2024-07-11T08:33:06+00:00 +category Technologie +abstract Un petit tour d'horizon des projets sur lesquels nos équipes techniques travaillent actuellement ... +author Damien ACCORSI +--- +{ + +} +--- + +{%markdown%} +L'été est là, le rythme ralentit, c'est l'occasion de parler un peu R&D + +Petit tour d'horizon des projets sur lesquels nous travaillons actuellement : + +## Tracim + +La version 4.10 est sortie début juillet et intègre un paramétrage plus fin des notifications web. C'est la première étape vers une stratégie de notifications optimisée pour être mieux informé tout en étant moins sollicité. Une vidéo de présentation de cette nouvelle version va arriver courant juillet. + +## Galae + +Une grosse mise à jour a été faite dans la stack technique, la mise en prod arrive début août. On progresse également sur la gestion des migrations depuis les outils Microsoft pour récupérer carnets d'adresses, agendas, etc (qui ne sont pas accessibles via les classiques CardDAV, CalDAV, sinon je n'en parlerais pas ;) + +## JSSG & JFME + +Clément nous a rejoint le temps d'un stage pour travailler avec Damien sur la future version statique de l'ensemble de nos sites web. + +On s'appuie sur le projet libre JSSG qu'on a amicalement forké et qui est en train de devenir JFME (JSSG For Meerkats Engine) et qui permet de gérer/générer des sites statiques avec une stratégie de construction de pages par blocs et factorisation des designs. + +## Whatelse ? + +Les 2 outils principaux (hors usines logicielles) qu'on utilise en interne sont Dolibarr et Tracim. Une partie de périmètre fonctionnel n'est adressé ni par l'un, ni par l'autre. Du coup on développe un outil interne qui permet d'implémenter toutes les fonctionnalités manquantes : tableaux de bord, préparation de fichiers bancaires, préparation de documents pour les déclarations de TVA, etc. + +Lorenzo et Damien travaillent à l'implémentation de nouvelles fonctionnalités + +## Connecteur Dolibarr en Python + +On utilise dolibarr en interne et on s'y interconnecte via plusieurs applicatifs python. On va probablement transformer le code client de l'API en un module qu'on publiera sur pypi. + +## Superset pour construire des tableaux de bord visuel + +On teste et expérimente l'utilisation de [superset](https://superset.apache.org/), un outil de la fondation Apache, pour construire des tableaux de bord graphiques. + +## Grist pour gérer des données et les enrichir par formulaire + +On teste et expériment [grist](https://www.getgrist.com/) pour la gestion de données structurées et la création de formulaires associés. + +## Suritech, l'événement veilletechno alpin libriste + +La R&D et veilletechno sont des activitées fondamentales pour perdurer. + +On a d'ailleurs organisé l'événement hashtag#suritech semaine dernière qui a été un franc succès 🎉 On réfléchit à la manière d'en faire la prochaine itération. + +On publiera l'enregistrement des conférences durant l'été. + +## Recrutement d'une ingénieure ou d'un ingénieur fullstack + +Après moultes rebondissements, on réfléchit à recruter une ingénieure ou un ingénieur fullstack geek & dynamique sur Grenoble pour contribuer à notre projet d'entreprise et nos produits tracim et galae. + +Si cela vous intéresse, envoyez-nous votre candidature (CV + quelques lignes qui expliquent pourquoi on devrait miser sur vous / pourquoi cela vous intéresse) + +~~~~ + +On ne s'arrête pas pendant l'été mais on réduit drastiquement notre présence en ligne. + +Passez toutes et tous de bonnes vacances 🌅 + +Rendez-vous début septembre pour de nouvelles aventures libres et souveraines ! 🚀 +{%endmarkdown%} \ No newline at end of file diff --git a/content/posts/post2.md b/content/posts/post2.md new file mode 100644 index 0000000..f27f03a --- /dev/null +++ b/content/posts/post2.md @@ -0,0 +1,42 @@ +--- +title Microsoft deviendrait-il un spyware avec la fonctionnalité Recall ? +date 2024-05-22T12:36:09+00:00 +category Technologie +abstract Le 20 mai 2024, Microsoft annonce la fonctionnalité Recall qui va "aider les utilisateurs à trouver leurs informations" en s'appuyant sur l'IA ... +author Damien ACCORSI +--- +{ + +} +--- +{% markdown %} +Le 20 mai dernier, Microsoft a annoncé sa nouvelle fonctionnalité #recall, basée sur l'#IA . Son objectif ? Vous aider à retrouver tout ce que vous avez déjà fait sur votre ordinateur. 🤗 Le moyen ? Enregistrer toute votre activité sur votre machine 😳 + +Tout ce que je tape au clavier ⌨ ? Oui, comme un Spyware 😨 + +Mais pas seulement ! 😰 + +Contrairement à ce qu'on imagine, il ne s'agit pas simplement d'enregistrer les frappes au clavier : il est également question de capturer *en permanence* ce qui est affiché à l'écran. Bien sûr, aucune donnée ne sortira de la machine et n'ira sur les serveurs Microsoft. Ouf. De toute façon, les données sont chiffrées et l'histoire est là pour vous rassurer : les clés de chiffrement ne fuitent jamais. 🔐 + +---- + +Ce que Wikipedia nous dit d'un spyware : un logiciel espion, un mouchard ou un espiogiciel (de l'anglais spyware) est un logiciel malveillant qui s'installe dans un ordinateur ou autre appareil mobile, dans le but de collecter et transférer des informations sur l'environnement dans lequel il s'est installé, très souvent sans que l'utilisateur en ait connaissance. L'essor de ce type de logiciel est associé à celui d'Internet qui lui sert de moyen de transmission de données. + +Je note un point important : *sans que l'utilisateur en ait connaissance*. + +France TV Info a publié un article sur l'annonce de la fonctionnalité Recall ; vous trouverez le lien ci-dessous. Maintenant que vous êtes au courant il ne s'agit plus d'un #spyware puisque vous êtes informé(e)s 🎁 + +---- + +Si vous préférez ne pas être confronté à ce genre de problématique, utilisez des logiciels libres et souverains comme ceux qu'on déploie, exploite et développe à #algoo : + +- 🐧 des poste de travail exploitant une distribution linux renommée telle que Debian, Ubuntu ou Zorin, +- 🏠 des outils auto-hébergés tels que Gitlab, mailtrain, dolibarr, etc, +- 💡 une plateforme de collaboration #tracim déployée en Saas en France sans exploitation des données ou déployée on premise, sur votre infrastructure y compris (et ça fonctionne même sans aucune connexion internet contrairement à d'autres) +- 📨 un service e-mail #galae exclusivement basé sur des logiciels libres et auto-hébergeable, conforme aux standards +- ... + +Bref des données dont vous maîtrisez l'hebergement, la localisation ... et l'existence. + +Vos données sont VOS données comme dirait l'autre. +{% endmarkdown %} diff --git a/content/posts/post3.md b/content/posts/post3.md new file mode 100644 index 0000000..2866605 --- /dev/null +++ b/content/posts/post3.md @@ -0,0 +1,58 @@ +--- +title Venez échanger sur les sujets de collaboration d'équipe et e-mail aux JDLL 2024 ! +date 2024-05-20T16:20:03+00:00 +category Univers du libre +abstract Algoo sera présente et très active aux JDLL 2024 : stand, ateliers et conférence ! +author Damien ACCORSI +--- +{ + +} +--- +{%markdown%} +Les 25 et 26 mai 2024 se tiennent les traditionnelles JDLL. Changement de lieu : ça se déroule toujours à Lyon, mais cette année ce sera [sur le site Descartes de l'ENS Lyon](https://jdll.org/contact-and-informations). + +## Le stand animé par les suricates Algoo + +Comme chaque année, nous serons présents, pour échanger, discuter, démontrer et animer autour de nos 2 thématiques principales : + +### 👉 Collaboration d'équipe en mode projet + +Venez découvrir la puissance de notre [logiciel Tracim, pensé pour la collaboration en mode projet et la collaboration asynchrone](https://www.tracim.fr) + + +Nous pourrons vous montrer ses fonctionnalités, mais aussi sa philosophie qui assure le meilleur embarquement des collaobrateurs. + +### 👉 Messagerie e-mail libre, éthique, collaborative + +Venez découvrir la flexibilité et la puissance de notre [service e-mail galae](https://www.galae.net) à travers les briques qu'il intègrent et une philosophie unique : vous laisser la main sur le paramétrage fin de votre messagerie email. + + +## Nos conférences et ateliers + +Comme chaque année nous participons activement à l'événement. Nous avons proposé une série de conférences et ateliers, tous ont été retenus ! \o/ + +### 🛠️ Comment collaborer efficacement en mode projet avec Tracim - Samedi 25 mai à 13h (atelier) + + +Damien animera l'atelier « [Découvrez comment collaborer efficacement en mode projet avec Tracim](https://pretalx.jdll.org/jdll2024/talk/CJPDZZ/) » + +### 🛠️ Dotez votre association d'un outil de collaboration auto-hébergée facile à maintenir. - Samedi 25 mai à 15h (atelier) + +Philippe animera l'atelier « [Dotez votre association d'un outil de collaboration auto-hébergée facile à maintenir.](https://pretalx.jdll.org/jdll2024/talk/MPGBWT/) » + +### 🎤 From zero to Galae : vivez de l'intérieur le lancement de la plateforme e-mail galae - Dimanche 26 mai à 16h (conférence) + +Sarah, Philippe et Damien vous relaterons l'aventure (et les mésaventures) du lancement du service e-mail Galae durant la conférence « [From zero to Galae : vivez de l'intérieur le lancement de la plateforme e-mail galae.](https://pretalx.jdll.org/jdll2024/talk/UCQJDZ/) » + +## Bonne ambiance, convivialité + +Comme d'habitude, cet événement est l'occasion d'échanger, discuter, boire un café ou une bière ... et de venir chiper sur notre stand les classiques stickers #suricate et désormais également les stickers galae :) + +Mais l'intérêt de venir aux JDLL est aussi (et surtout) de profiter de l'ensemble des conférences et stands ... + +Retrouvez ci-dessous le lien vers le planning complet des conférences et la liste exhaustive des stands et découvertes que vous pourrez faire durant les Journées Du Logiciel Libre ! + + +À très vite pour se rencontrer en chair et en os ! +{%endmarkdown%} \ No newline at end of file diff --git a/content/posts/19700101_hello.md b/content/posts/test/19700101_hello.md similarity index 53% rename from content/posts/19700101_hello.md rename to content/posts/test/19700101_hello.md index ec8ec82..66ed934 100644 --- a/content/posts/19700101_hello.md +++ b/content/posts/test/19700101_hello.md @@ -1,6 +1,12 @@ --- title Hello, world date 1970-01-01T00:00+00:00 +category test +abstract +author Bob --- +{ -My first post! +} +--- +Hello world \ No newline at end of file diff --git a/content/posts/xs-compromise.md b/content/posts/xs-compromise.md new file mode 100644 index 0000000..ce16595 --- /dev/null +++ b/content/posts/xs-compromise.md @@ -0,0 +1,25 @@ +--- +title La lib de compression Xz compromise ... +date 2024-04-03T13:13:26+00:00 +category Technologie +abstract Une porte dérobée introduite dans la lib de compression Xz a été fortuitement découverte par un développeur PostgreSQL ... +author Damien ACCORSI +--- +{ + +} +--- + +{%markdown%} +La bibliothèque partagée de compression Xz (liblzma) a été compromise par l'un de ses 2 principaux développeurs. Une porte dérobée a été introduite et fortuitement découverte par un développeur du projet PostgreSQL qui [évoque humblement sa découverte sur Mastodon](https://mastodon.social/@AndresFreundTec/112180083704606941) : « I accidentally found a security issue while benchmarking postgres changes » + +L'ingéniosité de l'attaque est de passer par un contributeur au long court et par une démarche très ingénieuse ; [Ytterbium décrit en détail l'attaque dans un long journal publié sur LinuxFR](https://linuxfr.org/users/ytterbium/journaux/xz-liblzma-compromis). + +Une fois n'est pas coutume : le problème des dépendances "invisibles" (et pour autant stratégiques) se pose ... + +![Dépendances](https://imgs.xkcd.com/comics/dependency.png) + +(source: https://xkcd.com/2347/ ) + + +{%endmarkdown%} \ No newline at end of file diff --git a/content/templates/jinja2/base.html b/content/templates/jinja2/base.html index 0130958..32adca0 100644 --- a/content/templates/jinja2/base.html +++ b/content/templates/jinja2/base.html @@ -3,7 +3,7 @@ - + @@ -13,7 +13,7 @@ {% for key, value in object.metadata.items()|filter_opengraph_metadata %} {% endfor %} - {{ object.title }} + {{ object.title|default("") }} diff --git a/content/templates/jinja2/post-list.html b/content/templates/jinja2/post-list.html new file mode 100644 index 0000000..f7256f1 --- /dev/null +++ b/content/templates/jinja2/post-list.html @@ -0,0 +1,86 @@ +{% extends "base.html"%} +{% block content %} + +
+ +
+ +
+{% for post in object.posts %} +
+
+
+
+ +

{{ post.title }}

+
+ +
+

{{ post.metadata.abstract }}

+
+ +
+
+
+
+
{{post.metadata.author}}
+
+ +
+
+
{{ post.timestamp.strftime("%d/%m/%Y") }}
+
+
+ +
+ + Lire la suite ... + +
+
+ +
+
+
+
+{% endfor %} +
+
+{% if object.page > 1 %} + {% if object.category != "" %} + Page précédente + {% else %} + Page précédente + {% endif %} +{% endif %} + +{% if object.page < object.nb_pages %} + {% if object.category != "" %} + Page suivante + {% else %} + Page suivante + {% endif %} +{% endif %} +
+
+Pages : +{% for page in range(1, object.nb_pages+1) %} + {% if object.category != "" %} + {{page}} + {% else %} + {{page}} + {% endif %} +{% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/content/templates/jinja2/post.html b/content/templates/jinja2/post.html index 804119a..25a1e33 100644 --- a/content/templates/jinja2/post.html +++ b/content/templates/jinja2/post.html @@ -1,8 +1,11 @@ {% extends "base.html" %} {% block content %} -Publié le {{ object.timestamp.strftime("%Y-%m-%d") }}. +Publié le {{ object.timestamp.strftime("%d/%m/%Y") }} à {{ object.timestamp.strftime("%Hh%M") }}.

{{ object.title }}

{{ object.content|safe }} +
+Tous les posts +
{% endblock %} diff --git a/jssg/default_metadata.txt b/jssg/default_metadata.txt new file mode 100644 index 0000000..e69de29 diff --git a/jssg/jinja2.py b/jssg/jinja2.py index dcbbf73..8c87f1b 100644 --- a/jssg/jinja2.py +++ b/jssg/jinja2.py @@ -1,6 +1,6 @@ from django.templatetags.static import static from django.urls import reverse - +from django_jinja_markdown.templatetags.md import markdown from jinja2 import Environment from jssg.templatetags.filter_opengraph_metadata import filter_opengraph_metadata @@ -91,6 +91,7 @@ def environment(**options): { "static": static, "url": reverse, + "markdown": markdown, "url_for_slug": url_for_slug, "url_for_slug_path" : url_for_slug_path } diff --git a/jssg/management/commands/check-metadata.py b/jssg/management/commands/check-metadata.py new file mode 100644 index 0000000..be037f8 --- /dev/null +++ b/jssg/management/commands/check-metadata.py @@ -0,0 +1,63 @@ +from django.core.management.base import BaseCommand +from django.conf import settings +from jssg.models import Page +from pathlib import Path + +class MetadataStatus : + + @classmethod + def get_metadata_status_for(cls, page) : + metadata_status = MetadataStatus() + metadata_status.missing = [] + metadata_status.empty = [] + for required_metadata in settings.JFME_CONTENT_REQUIRED_METADATA : + if required_metadata not in page.metadata : + metadata_status.missing.append(required_metadata) + elif page.metadata[required_metadata] == "" : + metadata_status.empty.append(required_metadata) + metadata_status.complete = (metadata_status.missing == []) and (metadata_status.empty == []) + if len(settings.JFME_CONTENT_REQUIRED_METADATA) > 0 : + metadata_status.progression = (len(settings.JFME_CONTENT_REQUIRED_METADATA) - len(metadata_status.missing) - len(metadata_status.empty)) * 100 / len(settings.JFME_CONTENT_REQUIRED_METADATA) + else : + metadata_status.progression = 100 + return metadata_status + +class Command(BaseCommand): + help = "Check if metadata in JFME_CONTENT_REQUIRED_METADATA setting are specified in pages." + + def add_arguments(self, parser): + parser.add_argument( + "--verbose", + action = "store_true", + help="Show missing or empty metadata in each page." + ) + parser.add_argument( + "content path", + nargs = "*", + type=str, + default=settings.JFME_PAGES_DIRS, + help="The paths where search the pages. Set to JFME_PAGES_DIRS by default." + ) + + def handle(self, *args, **options) : + + if settings.JFME_CONTENT_REQUIRED_METADATA == [] : + self.stdout.write(self.style.WARNING( + "Warning : no metadata specified in JFME_CONTENT_REQUIRED_METADATA setting." + )) + + for page in Page.load_glob(path = list(map(lambda p : Path(p).absolute(), options["content path"])), all = True) : + + metadata_status = MetadataStatus.get_metadata_status_for(page) + + self.stdout.write("{:3.0f}% : {}".format( + metadata_status.progression, + page.path.relative_to(page.content_page_dir)) + ) + + if options["verbosity"] > 1 or options["verbose"] : + if not metadata_status.complete : + for missing in metadata_status.missing : + self.stdout.write("\t- '%s' is missing" % missing) + for empty in metadata_status.empty : + self.stdout.write("\t- '%s' is empty" % empty) \ No newline at end of file diff --git a/jssg/management/commands/format-html.py b/jssg/management/commands/format-html.py new file mode 100644 index 0000000..28bbe71 --- /dev/null +++ b/jssg/management/commands/format-html.py @@ -0,0 +1,33 @@ +from django.core.management.base import BaseCommand +from pathlib import Path +from bs4 import BeautifulSoup +from django.conf import settings + +class Command(BaseCommand): + help = "Format (beautify or minify) the html files in dist content" + + def add_arguments(self, parser): + parser.add_argument( + "mode", + choices=["beautify", "minify"], + type=str, + help="Beautify or minify the html files" + ), + parser.add_argument( + "distpath", + nargs='?', + type=str, + default=str(settings.DIST_DIR), + help="To specify a particular dist path. Default is: " + str(settings.DIST_DIR) + ) + + def handle(self, *args, **options) : + for path in Path(options["distpath"]).rglob("*.html") : + with open(path, "r+") as file : + soup = BeautifulSoup(file.read(), 'html.parser') + file.seek(0) + if options["mode"] == "minify" : + file.write(str(soup).replace('\n', '')) + else : + file.write(soup.prettify()) + file.truncate() \ No newline at end of file diff --git a/jssg/models.py b/jssg/models.py index c93d4dc..26c96de 100644 --- a/jssg/models.py +++ b/jssg/models.py @@ -21,12 +21,22 @@ from typing import Iterator, Mapping, Optional, List import markdown2 +import re from django.conf import settings from django.template import Context, Template, engines from django.utils.text import slugify from django.core.management.commands.runserver import Command as runserver +from math import ceil + + + +class EmptyLine(Exception) : + pass +class CommentLine(Exception) : + pass + class Document: """A document. @@ -58,7 +68,6 @@ def content(self) -> str: :return: the rendered document """ - import re # INFO DA 2024-02-18 - Replace "{{{ }}}" pattern into one-line pattern # this is usefull in order to exploit multi-line includes # {{{ include "block.html" with @@ -118,11 +127,22 @@ def load(cls, path: Path) -> "Document": :return: The loaded document """ _path = path - metadata = {} + metadata = settings.JFME_DEFAULT_METADATA_DICT.copy() data = {} json_data = "" content = StringIO() + with settings.JFME_DEFAULT_METADATA_FILEPATH.open() as f: + for line in f : + try : + # Parse a metadata key value pair + key, value = cls.parse_metadata_line(line) + metadata[key] = value + except EmptyLine : # ignore empty lines + continue + except CommentLine : # ignore comment lines + continue + with path.open() as f: # States: # 0: search the metadata start block @@ -145,17 +165,14 @@ def load(cls, path: Path) -> "Document": # Metadata end block found state = 2 else: - if line.strip() == "": # ignore empty lines + try : + # Parse a metadata key value pair + key, value = cls.parse_metadata_line(line) + metadata[key] = value + except EmptyLine : # ignore empty lines continue - if line.startswith("#"): # ignore comment lines + except CommentLine : # ignore comment lines continue - - # Parse a metadata key value pair - # key, value = map(str.strip, line.split("", maxsplit=1)) - import re - key, value = map(str.strip, re.split("[\s]", line, maxsplit=1)) - # FIXME print("KEY {} : {} (line is: {})".format(key, value, line)) - metadata[key] = value elif state == 2: if line.rstrip().startswith("---"): # data end block found @@ -229,6 +246,14 @@ def make_imports(cls) : import_str += "{% " + "import '{}' as {}".format(widget_file.relative_to(template_dir / "jinja2"), widget_file.stem) + " %}\n" return import_str + @classmethod + def parse_metadata_line(cls, line) : + if line.strip() == "": # ignore empty lines + raise EmptyLine() + if line.startswith("#"): # ignore comment lines + raise CommentLine(line) + # key, value = map(str.strip, line.split("", maxsplit=1)) + return map(str.strip, re.split("[\s]", line, maxsplit=1)) class Page(Document): """A webpage, with a title and some content.""" @@ -286,6 +311,10 @@ def __init__(self, content: str, **metadata) -> None: """ super().__init__(content, **metadata) self.timestamp = datetime.datetime.fromisoformat(metadata["date"]) + if "category" in self.metadata : + self.metadata["category"] = slugify(self.metadata["category"]) + else : + self.metadata["category"] = "" @classmethod def load_glob( @@ -303,3 +332,49 @@ class Sitemap : domain = settings.JFME_DOMAIN pages_slugs = [p["slug"] for p in Page.get_pages()] posts_slugs = [p["slug"] for p in Post.get_posts()] + +class PostList : + metadata = {"page_header_h1":"Posts"} + category = "" + + def __init__(self, category = "", page = 1) -> None: + self.category = category + self.page = page + self.posts_by_page = settings.JFME_NUMBER_OF_POSTS_BY_PAGE + if self.category == "" : + self.nb_pages = ceil(len(list(Post.load_glob(all=True))) / self.posts_by_page) # number of posts / number of posts by page + else : + self.nb_pages = ceil(len(list(filter(lambda p: p.metadata["category"] == self.category, Post.load_glob(all=True)))) / self.posts_by_page) # number of posts of the category / number of posts by page + + @classmethod + def load_post_list_with_category(cls, category, page) : + return cls(category, page) + + @property + def categories(self) : + cat = set() + for post in Post.load_glob(all = True) : + if post.metadata["category"] != "" : + cat.add(post.metadata["category"]) + return sorted(cat) + + @classmethod + def get_categories_and_pages(cls) : + t = [] + for category in cls().categories : + t += [{"category": category, "page":page} for page in range(1, cls(category).nb_pages + 1)] + return t + + + @classmethod + def get_pages(cls) : + return [{"page": page} for page in range(1, cls().nb_pages+1)] + + @property + def posts(self) : + posts = sorted(Post.load_glob(all=True), key=lambda p: p.timestamp, reverse=True) + if self.category == "" : + return posts[self.posts_by_page*(self.page-1):self.posts_by_page*(self.page)] + else : + return list(filter(lambda p: p.metadata["category"] == self.category, posts))[self.posts_by_page*(self.page-1):self.posts_by_page*(self.page)] + \ No newline at end of file diff --git a/jssg/settings.py b/jssg/settings.py index a0ce03b..43b2363 100644 --- a/jssg/settings.py +++ b/jssg/settings.py @@ -52,13 +52,17 @@ JFME_POSTS_DIRS = [path / "posts" for path in JFME_CONTENT_DIRS] JFME_TEMPLATES_DIRS = [path / "templates" for path in JFME_CONTENT_DIRS] JFME_STATIC_DIRS = [path / "static" for path in JFME_CONTENT_DIRS] - +JFME_DEFAULT_METADATA_DICT = {"slug": "index", } # The order of include is : JFME_DEFAULT_METADATA_DICT then JFME_DEFAULT_METADATA_FILEPATH then page metadata +JFME_DEFAULT_METADATA_FILEPATH = BASE_DIR / "jssg" / "default_metadata.txt" # If a metadata is specified more than once, the last included is retained +JFME_NUMBER_OF_POSTS_BY_PAGE = 3 +JFME_CONTENT_REQUIRED_METADATA = ["title", "slug", "lang", "description"] # Application definition INSTALLED_APPS = [ "jssg", + "django_jinja_markdown", "django.contrib.contenttypes", "whitenoise.runserver_nostatic", "django.contrib.staticfiles", @@ -80,7 +84,8 @@ "DIRS": [path / "jinja2" for path in JFME_TEMPLATES_DIRS], "APP_DIRS": True, "OPTIONS": { - "environment": "jssg.jinja2.environment" + "environment": "jssg.jinja2.environment", + "extensions": ["django_jinja_markdown.extensions.MarkdownExtension"] }, }, { diff --git a/jssg/urls.py b/jssg/urls.py index 513dc1a..736bb23 100644 --- a/jssg/urls.py +++ b/jssg/urls.py @@ -15,17 +15,17 @@ from django_distill import distill_path, distill_re_path from jssg import views -from jssg.models import Page, Post +from jssg.models import Page, Post, PostList from jssg import settings # print([p for p in Page.get_pages()]) +# print([p for p in PostList.get_categories_and_pages()]) urlpatterns = [ distill_path( "", views.IndexView.as_view(), name="index", distill_file="index.html" ), - distill_path("atom.xml", views.PostFeedsView(), name="atom_feed"), distill_re_path( r'^(?!posts/)(?P[a-zA-Z0-9-]+).html$', views.PageView.as_view(), @@ -38,19 +38,33 @@ name="page", distill_func=Page.get_pages, ), + distill_path("atom.xml", views.PostFeedsView(), name="atom_feed"), distill_path( - "posts/.html", + "posts/page.html", + views.PostListView.as_view(), + name = "post-index", + distill_func = PostList.get_pages + ), + distill_path( + "posts/category//page.html", + views.PostListView.as_view(), + name = "post-category", + distill_func = PostList.get_categories_and_pages + ), + distill_path( + "posts/articles/.html", views.PostView.as_view(), name="post", distill_func=Post.get_posts, ), distill_path( - "posts//.html", + "posts/articles//.html", views.PostView.as_view(), name="post", distill_func=Post.get_posts, ), + distill_path( "sitemap.xml", views.SitemapView.as_view(), diff --git a/jssg/views.py b/jssg/views.py index 2aa9004..3145513 100644 --- a/jssg/views.py +++ b/jssg/views.py @@ -22,7 +22,7 @@ from django.utils.feedgenerator import Atom1Feed from django.views.generic import DetailView -from jssg.models import Page, Post, Sitemap +from jssg.models import Page, Post, Sitemap, PostList class PostFeedsView(Feed): @@ -78,4 +78,13 @@ class SitemapView(DetailView) : template_name = "sitemap.html" def get_object(self, queryset=None) -> Model: - return self.model() \ No newline at end of file + return self.model() + +class PostListView(DetailView): + template_name = "post-list.html" + + def get_object(self, queryset=None) -> Model: + if "category" not in self.kwargs.keys() : + self.kwargs["category"] = "" + print(self.kwargs["category"]) + return PostList.load_post_list_with_category(self.kwargs["category"], self.kwargs["page"]) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c196e9e..11a21bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,8 @@ -Django==5.0.6 +Django==4.2.9 django-distill django_vite_plugin==3.0.0 markdown2[all]==2.4.13 whitenoise==6.7.0 Jinja2==3.1.4 +beautifulsoup4==4.12.3 +django-jinja-markdown \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 984c649..b534021 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,30 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "paths": { + "@/*": [ + "./*" + ], + "@s:jssg/*": [ + "./jssg/static/jssg/*" + ], + "@t:jssg/*": [ + "./jssg/templates/jssg/*" + ], + "@s:django_jinja_markdown/*": [ + "./env/lib/python3.9/site-packages/django_jinja_markdown/static/django_jinja_markdown/*" + ], + "@t:django_jinja_markdown/*": [ + "./env/lib/python3.9/site-packages/django_jinja_markdown/templates/django_jinja_markdown/*" + ], + "@s:django_distill/*": [ + "./env/lib/python3.9/site-packages/django_distill/static/django_distill/*" + ], + "@t:django_distill/*": [ + "./env/lib/python3.9/site-packages/django_distill/templates/django_distill/*" + ] + } }, "include": [ "content/front/"