depuis la création du compte
Confiez votre projet à Gabriel
Faites appel à l'expertise de Gabriel pour faire avancer votre projet, ou découvrez d'autres freelances pour trouver celui qui correspondra parfaitement à vos besoins.
Développeur Python freelance spécialisé Django, j'interviens sur la conception, la reprise et l'évolution d'applications web métier.
Backend Django, API REST avec Django REST Framework, sites éditoriaux et back-offices sur Wagtail. J'intègre aussi l'IA dans des applications existantes : assistants documentaires, traitement et classification automatique de contenus, suppression de saisies manuelles.
L'IA fait également partie de mon processus de développement au quotidien, pour gagner en rapidité tout en gardant la maîtrise et la relecture du code.
Quelques réalisations :
Refonte complète d'un e-commerce, migration de Django 1.3 vers 5.2 avec reprise intégrale des données historiques, sans coupure de service
Site officiel d'une commune, avec back-office autonome pour les équipes municipales
SaaS éducatif en production, incluant un assistant pédagogique
Application de sondages géolocalisés avec cartographie interactive
Je suis mentor sur Docstring.fr où j'accompagne des développeurs Python, co-animateur d'un workshop à la PyCon FR, speaker au Django Meetup Paris et membre individuel de la Django Software Foundation. Certifié TOSA Python (955/1000).
Je travaille en direct avec des PME et des collectivités, ainsi qu'en sous-traitance pour des agences. Portfolio complet : pygab.dev
Le marché francophone de l'actualité IA est saturé de newsletters qui promettent toutes de filtrer le bruit. Carnetia tranche : chaque outil reçoit un verdict, chaque guide est daté et maintenu.
Le modèle est freemium.
Technologies utilisées
Backend : Django 6 + Django REST Framework (Python 3.13)
Frontend : Nuxt 4 (Vue 3, TypeScript), SSR sur les pages publiques, rendu client sur l'espace membre
Base de données : PostgreSQL
Authentification : django-allauth en mode headless, JWT (access court + refresh rotatif), vérification d'adresse obligatoire
UI : Nuxt UI v4 (Tailwind v4), kit de marque en tokens CSS, thème sombre par défaut
État : Pinia
Markdown : Martor côté admin (édition + upload d'images), @nuxtjs/mdc côté front (rendu SSR)
SEO : @nuxtjs/seo
Paiement : Stripe (Checkout, Billing Portal, webhooks)
E-mail : Brevo (SMTP transactionnel + campagnes)
Communauté : Discord (OAuth2 + rôle attribué selon l'adhésion)
Anti-bot : Cloudflare Turnstile
Observabilité : Sentry (back et front)
Tests : pytest (back), Vitest ****@****/test-utils (front)
Outillage : uv, ruff, vue-tsc, GitHub Actions
Déploiement : VPS, Nginx, Gunicorn, systemd
Architecture
Deux dépôts indépendants (API DRF / front Nuxt), le front consomme l'API sur une origine distincte
Rendu piloté par route : SSR là où le SEO compte (carnets, Radar, éditions, glossaire), rendu client sur l'espace membre, pages privées exclues du sitemap et de l'indexation
Statut de publication porté par un mixin commun : le filtrage brouillon / publié se fait au queryset, en amont de la sérialisation ; un brouillon n'existe pas pour le public
Back-office = admin Django : éditeur markdown, upload d'images réservé au staff, stockage local avec conversion WebP à l'enregistrement, aucun stockage objet
Front : logique métier dans des composables et des stores, un client API unique ; tests sur la logique, jamais sur le rendu, typecheck + build en filet
Authentification
allauth fabrique le JWT, DRF le vérifie : les vues métier ne dépendent d'aucun fournisseur de tokens
Vérification d'adresse obligatoire par lien : l'inscription ne délivre aucun token tant que l'adresse n'est pas confirmée ; anti-énumération à l'inscription
Client API unique : header Authorization, refresh puis rejeu unique de la requête sur 401
Rate limiting sur inscription et connexion, Turnstile vérifié côté serveur
Fonctionnalités principales
Accueil en flux : verdicts récents, carnets mis à jour, palier en cours et places restantes, compteur du cercle, teaser de la dernière édition
Radar : outils groupés par verdict, phrase publique et date du verdict, filtre par thème
Carnets : guides markdown avec images inline, niveau, date de dernière révision, gratuit ou premium
Éditions : article hebdomadaire daté, intro publique + corps premium
Glossaire : fiches A–Z publiques avec balisage JSON-LD
Embeds médias dans le corps (YouTube avec bornes d'extrait, X), citation rendue en SSR
Tarif : grille de paliers, palier en cours, places restantes, redirection Stripe Checkout
Espace membre : statut, palier et prix verrouillé, portail de facturation, liaison du compte Discord, préférences e-mail (promo en opt-in, newsletter en opt-out)
Mur d'abonnement inline pour un compte gratuit connecté
Paliers, paiement et contrôle d'accès
Le cœur du projet est la boucle adhésion → palier → accès premium, conçue pour qu'un prix promis soit tenu à vie et qu'un corps réservé ne fuite jamais.
Grille : cohortes de membres à prix croissant, statut à venir / en cours / complet, un prix Stripe distinct par palier
Attribution transactionnelle : à la confirmation du paiement, l'adhésion est créée sous transaction avec verrouillage des paliers ; le numéro de membre détermine le palier ; à la borne haute, le palier passe complet et le suivant s'ouvre
Verrouillage à vie : l'abonnement Stripe reste sur le prix de son palier d'origine, indépendamment de la grille publique
Idempotence des webhooks : rejouer un événement Stripe n'a aucun effet
Webhooks de facturation → statut actif / impayé / résilié, fin de période, annulation programmée
Propagation : tout changement de statut est reflété sur le contact e-mail et sur le rôle Discord
Gating côté API : le drapeau locked est calculé à la sérialisation selon le lecteur ; le corps premium n'est jamais sérialisé pour un non-membre ; l'extrait est toujours renvoyé, jamais de 403, pour garder le teaser indexable
Deux granularités : un carnet est premium en bloc, une édition est toujours scindée intro / corps
Seconde barrière e-mail : envoi segmenté aux seuls membres, avec un bloc conditionnel dans le template Brevo
[URL MASQUÉE]
L'entreprise entretient des équipements biomédicaux dans des établissements de santé. Jusqu'ici, chaque intervention donnait lieu à un rapport papier signé sur place. L'application remplace ce flux de bout en bout : le technicien saisit l'intervention sur téléphone ou tablette, la clôture est validée électroniquement, le rapport PDF est généré automatiquement et l'établissement le retrouve dans son espace client.
Technologies utilisées
Backend : Django 5.2 LTS + Django REST Framework 3.18 (Python 3.13)
Frontend : Nuxt 4 (Vue 3, TypeScript strict), full CSR, servi en statique par Nginx
Base de données : PostgreSQL (psycopg 3), en développement comme en production
Authentification : JWT (djangorestframework-simplejwt) access token en mémoire, refresh token en cookie httpOnly, blacklist
Contrat API : schéma OpenAPI (drf-spectacular) → types TypeScript générés (openapi-typescript) + client typé (openapi-fetch)
CSS : Tailwind v4
État : Pinia
PDF : WeasyPrint
QR codes : segno
Exports : CSV (stdlib) + Excel (openpyxl)
Tests : pytest + factory_boy (back), Vitest ****@****/test-utils et Playwright (front)
Outillage : uv, ruff, pre-commit, ESLint (@nuxt/eslint)
Architecture
Deux repos indépendants (API Django / SPA Nuxt) reliés par le schéma OpenAPI : le back décrit chaque endpoint, le front génère ses types, aucune interface API écrite à la main.
Vues fines, services épais : les ViewSets ne portent que la couche HTTP ; la logique métier (clôture, numérotation, gel du contexte, PDF) vit dans des services typés, testables sans HTTP
Trois rôles (admin, technicien, client) portés par le User custom ; permissions DRF explicites et filtrage des querysets par rôle et par client. Un objet d'un autre client n'existe pas : 404, jamais 403
Aucune page publique : la SPA est générée en statique (ssr: false), aucun process Node en production
Fichiers générés privés : les PDF sont stockés hors racine web et servis via X-Accel-Redirect après contrôle d'accès ; les QR codes sont rendus en mémoire à la demande, rien n'est écrit sur disque
Authentification JWT
Access token court (15 min) gardé en mémoire côté front, jamais en localStorage
Refresh token (7 jours) en cookie httpOnly, SameSite=Strict, Path limité aux endpoints d'authentification
Refresh silencieux au démarrage : un QR code scanné ouvre une page à froid et retrouve sa session sans ressaisir le mot de passe
Composable API unique : header Authorization, rejeu automatique de la requête après refresh sur 401, requêtes concurrentes mises en file derrière un seul refresh
Blacklist activée : la désactivation d'un compte prend effet immédiatement
Throttling sur login et refresh, messages d'erreur non énumérants
Fonctionnalités principales
Accueil : compteurs (interventions à venir, machines, clients, derniers rapports) et agenda trié par date planifiée
Clients : fiches (coordonnées, contact), recherche, archivage logique, jamais de suppression physique
Machines : fiches rattachées à un client (n° de série, modèle, mise en service, garantie), rattachement figé après création pour éviter toute fuite d'historique entre clients
QR codes : un QR par machine, SVG généré à la demande, encodant l'URL de sa page d'historique ; étiquette imprimable depuis la fiche
Interventions : formulaire par étapes pensé mobile (contexte et machines → travaux et résultat par machine → temps, kilomètres et pièces → récapitulatif et clôture), plusieurs machines par dossier, brouillon enregistré à chaque étape
Cycle de vie : brouillon → planifiée → clôturée (+ annulée avec motif), transitions contrôlées côté serveur
Espace client : chaque établissement consulte ses machines, son historique (dossiers clôturés uniquement), ses prochains entretiens et télécharge ses rapports, en lecture seule
Exports CSV/Excel des clients, machines et interventions (réversibilité contractuelle), qui respectent les filtres de la liste
Création de techniciens et d'accès espace client depuis l'application, mot de passe initial défini par l'admin
Clôture d'intervention et génération de rapport PDF verrouillé
Clôture et rapport PDF
Le cœur du projet est le cycle intervention → clôture → rapport, conçu pour qu'un rapport émis ne puisse jamais être altéré silencieusement.
Validation électronique : technicien identifié par sa session, nom du signataire client saisi au clavier (obligatoire), date horodatée
Numérotation transactionnelle : le n° de rapport est attribué à la clôture via select_for_update sur une ligne compteur ; brouillons et annulés n'en consomment pas, la séquence des rapports émis est sans trou. Une contrainte en base garantit « clôturée ⟺ numérotée »
Contexte gelé : à la clôture, toutes les données imprimées (client, contact, machines, pièces) sont figées dans un instantané JSON ; le PDF est toujours rendu depuis cet instantané, jamais depuis les fiches vivantes
Génération WeasyPrint dans la transaction de clôture : un échec annule la clôture et rend le numéro au compteur
Verrouillage : toute écriture ultérieure est refusée ; déverrouillage réservé à l'admin, re-clôture qui conserve la validation d'origine et régénère le PDF avec la mention « corrigé le »
Garde de concurrence : version transmise à chaque enregistrement, conflit signalé (409) plutôt qu'écrasement silencieux
Technologies utilisées
Framework : Django 5.2 (Python 3.13)
Base de données : PostgreSQL
Frontend : Django Templates, Bootstrap 5, HTMX
Tâches asynchrones : Celery + Redis (emails, notifications admin)
Paiement : Stripe + PayPal
Email : Brevo
Médias : ~160 000 fichiers migrés depuis l'ancien serveur
Déploiement : Gunicorn (socket) + Nginx sur VPS Hostinger
SSL : Certbot
Formulaires : django-crispy-forms + django-recaptcha (reCAPTCHA v3)
Export données : django-import-export
Migration legacy
Réécriture complète depuis Django 1.3 / Python 2.7
Export, nettoyage et transformation manuelle des données (utilisateurs, capsules, paniers en cours)
Migration des mots de passe : implémentation d'un hasher de compatibilité SHA1 permettant aux utilisateurs existants de se connecter sans réinitialisation, avec upgrade automatique vers PBKDF2 à la connexion suivante
Transfer de ~160 000 fichiers médias via rsync
Espace d'administration
L'application dispose d'un espace d'administration Django personnalisé permettant :
Gestion du catalogue : capsules unitaires et génériques, thèmes, vignerons
Gestion des commandes : suivi des statuts, détail des lignes, historique
Gestion des membres : profils utilisateurs en inline
Planification : tâches Celery Beat gérées via django-celery-beat
Fonctionnalités principales
Catalogue : navigation par thème, vigneron, recherche, nouveautés, ajouts récents
Panier : gestion session (anonyme) avec transfert en base à la connexion
Tunnel de commande : intégration Stripe Checkout et PayPal, confirmation par email
Authentification : connexion en deux étapes (email puis mot de passe) via HTMX, inscription avec reCAPTCHA v3, réinitialisation de mot de passe
Redirections SEO : règles Nginx pour préserver le référencement de l'ancienne structure d'URLs
Multi-domaines : [URL MASQUÉE] et [URL MASQUÉE] (redirection 301)
Emails : confirmation de commande, réinitialisation de mot de passe, alertes admin (tentatives de connexion suspectes)
Moteur de recherche avancé maison
[URL MASQUÉE]