Runique vs Django
Sommaire
CLI
Commandes réelles du binaire runique (voir runique/src/bin/runique.rs) : new, create-superuser, test, makemigrations, migration up | down | status. Pas de runique start — l'admin est du Rust plain, cargo run suffit.
| Commande | Django | Runique |
|---|---|---|
| Créer un projet | django-admin startproject nom | runique new nom |
| Créer une app | python manage.py startapp nom | — |
| Migrations (générer) | python manage.py makemigrations | runique makemigrations |
| Migrations (appliquer) | python manage.py migrate | runique migration up |
| Migrations (annuler) | python manage.py migrate app 0001 | runique migration down -n 1 |
| Statut migrations | python manage.py showmigrations | runique migration status |
| Créer superuser | python manage.py createsuperuser | runique create-superuser |
| Tests | python manage.py test | runique test |
| Démarrer | python manage.py runserver | cargo run |
Routing
| Fonctionnalité | Django | Runique |
|---|---|---|
| Déclaration des routes | urls.py avec path() | url.rs avec macro urlpatterns!{} |
| Routes dynamiques | path('users/<int:id>/', view) | "/users/{id}" dans urlpatterns! |
| Namespaces | app_name + include() | Router::new().nest("/prefix", ...) |
| Reverse URL | {% url "nom_vue" %} natif | {% link "nom_vue" %} → fonction Tera custom |
| Paramètre de chemin typé | <int:id> dans path(), converti en int | request.get_path_as::<i32>("id") |
| Paramètre de chemin brut | <str:id> | request.get_path("id") |
| Query param unique | request.GET.get('key') | request.get_query("key") |
| Query string complète | request.GET | request.query::<MyStruct>()? (désérialise vers un struct Deserialize, 400 si la query ne correspond pas) |
| Headers HTTP | request.META['HTTP_X_FOO'] | request.headers.get("x-foo") |
Vues / Handlers
| Fonctionnalité | Django | Runique |
|---|---|---|
| Vue fonction | def ma_vue(request) | async fn ma_vue(...) |
| Vue classe | class MaVue(View) | — |
| Accès session | request.session | request.session via context::template::Request |
| Accès DB | Model.objects.get(...) | Model::objects.get(...) ou builders SeaORM |
| Rendu template | render(request, "template.html", ctx) | request.render("template.html") |
| Redirect | redirect("nom_url") | Redirect::to("/url") ou reverse(&engine, "nom") |
| Messages flash | messages.success(request, "...") | macros success!, error!, info!, warning! |
| Connexions secondaires | DATABASES['mongo'] | engine.extension::<mongodb::Client>() (TypeMap multi-type) |
Formulaires
| Fonctionnalité | Django | Runique |
|---|---|---|
| Définition | class MonForm(ModelForm) | #[form(schema = blog, fields = [...])], lié à son entité par le schéma, ou RuniqueForm manuel |
| Validation | form.is_valid() | ValidationForm::try_form(form, &request).await |
| Champs disponibles | CharField, EmailField, FileField, etc. | TextField (+ constructeurs ::email/::password/::url/::phone → bon <input type> + validation), NumericField, BooleanField, CheckboxField, RadioField, ChoiceField, FileField, DateField, TimeField, DateTimeField, DurationField, ColorField, SlugField, UUIDField, IPAddressField, JSONField, HiddenField, MediaField, BinaryField |
| Rendu HTML | {{ form.as_p }} | {% form.nom_form %} (entier) ou {% form.nom_form.champ %} |
| CSRF intégré | automatique | automatique — injecté avant le premier champ |
| Sauvegarde | form.save() | validated.save(&db).await, sur le formulaire validé, pour tout formulaire qui porte son entité (FormEntity) : #[form(schema = …)] ou impl FormEntity |
| Accès aux données | form.cleaned_data['clé'] | validated.cleaned::<String>("clé"), validated.cleaned::<i32>(...), etc. |
| Validation async | non | oui (accès DB dans clean()) |
| Validation croisée | clean() | clean() async |
| Fichiers | FileField (pas de validation de contenu par défaut) | FileField / MediaField multipart natif — validation type/taille, signature du contenu pour les images, l'audio et la vidéo, extensions actives (.svg, .html, .js…) rejetées, écriture en staging seulement après le jeton CSRF, puis commit après validation (nom UUID, pas de path traversal) |
| Sanitisation HTML | non (à la main) | sanitize_rich / sanitize_strict appliquées aux champs richtext |
Templates
| Fonctionnalité | Django | Runique |
|---|---|---|
| Moteur | Django Template Language | Tera (syntaxe Jinja2 / Django-like) |
| Héritage | {% extends %} / {% block %} | idem |
| Fichiers statiques | {% load static %} + {% static "file" %} | {% static "file" %} natif |
| Fichiers media | {{ MEDIA_URL }}file | {% media "file" %} natif (variables supportées) |
| URL reverse | {% url "nom" %} | {% link "nom" %} |
| CSRF | {% csrf_token %} | {% csrf %} |
| Messages | {% for m in messages %} | {% messages %} |
| Internationalisation | {% trans "..." %} | — (t() et tf() côté Rust) |
ORM / Base de données
| Fonctionnalité | Django | Runique |
|---|---|---|
| ORM | Django ORM natif | SeaORM (Rust async) |
| Définition modèle | class User(models.Model) | macro model!{} (types sémantiques) |
| Migrations auto | oui (détection changements) | runique makemigrations |
| QuerySet chaînable | User.objects.filter(...).order_by(...) | User::objects.filter(...).order_by_asc(Col) |
.get() strict | lève MultipleObjectsReturned | .one() — retourne Err si plusieurs lignes |
| Tri aléatoire | order_by('?') | .order_by_random() |
| Tri par expression | order_by(Lower("titre").desc()) | .order_by_expr(expr, order) |
| Relations | ForeignKey, ManyToMany, OneToOne | Relations SeaORM standard |
| Transactions | with transaction.atomic() | request.db_atomic() ou transaction SeaORM |
| Multi-moteur SQL | oui | PostgreSQL, MySQL/MariaDB, SQLite (3 backends SeaORM ; MariaDB passe par le driver MySQL avec son propre dialecte de migration) |
| Connexions secondaires | DATABASES multi-entrées | .with_custom_db(client), un client par type (TypeMap) |
| Extension table framework | — | extend!{} — ALTER TABLE ADD COLUMN sur tables eihwaz_* |
Authentification
| Fonctionnalité | Django | Runique |
|---|---|---|
| Login / Logout | authenticate() + login() | authenticate_user() + login(), logout() |
| Vérif authentification | request.user.is_authenticated | is_authenticated(&session).await |
| Utilisateur courant | request.user | request.user |
| Protection route | @login_required | .login_required("/profil", "profil", view!(profil), "/login") |
| Sessions | natif | tower-sessions (MemoryStore + DB fallback) |
| Protection brute force | django-axes (tiers) | LoginGuard natif (lockout auto) |
| Hashage mot de passe | PBKDF2 par défaut, Argon2 en option | Argon2 par défaut |
| Reset password | token signé lié au hash du mot de passe (stateless) | tokens persistés en DB, hashés, single-use (durcis IDOR — clé user_id), TTL configurable via .token_ttl(...), template email personnalisable |
| Déconnexion forcée | changer le mot de passe ferme ses sessions | changer le mot de passe aussi ; invalidate_all(user_id) sur RuniqueSessionStore |
Sécurité
| Fonctionnalité | Django | Runique |
|---|---|---|
| CSRF | natif | natif (comparaison constant-time) |
| CSP | natif depuis Django 6.0 (ContentSecurityPolicyMiddleware), à activer | natif (nonce par requête toujours actif) |
| HSTS | SECURE_HSTS_SECONDS | natif — émis uniquement sur HTTPS réel (enforce_https/ACME), max-age/includeSubDomains/preload configurables (preload opt-in) |
| SameSite cookies | configurable | Strict par défaut |
| HttpOnly cookies | par défaut | toujours true |
| Rate limiting | django-ratelimit (tiers) | RateLimiter natif, déclaré par route — clé = IP cliente résolue par les proxys de confiance, sinon le pair ConnectInfo ; jamais un en-tête non validé |
| Open Redirect | — | natif — Location des réponses 3xx et en-tête Refresh de toute réponse, cible lue par l'analyseur d'URL WHATWG (le même que les navigateurs), refus en cas de doute |
| CORS | django-cors-headers (tiers) | natif via .with_cors(...) |
| Permissions-Policy | — | natif — preset sécurisé par défaut (~20 features refusées) |
| Trusted Proxies / XFF | SECURE_PROXY_SSL_HEADER (partiel) | natif — validation chaîne XFF complète (droite→gauche), preset RFC 1918, défaut edge-aware : none() auto quand Runique termine le TLS (ACME = pas de proxy) ; une plage /0 (faire confiance à tout le monde) est refusée au démarrage |
| Secret key | manuel | générée auto à runique new — refus de boot en prod si absente / valeur par défaut / < 32 caractères |
Panel Admin
| Fonctionnalité | Django | Runique |
|---|---|---|
| Activation | admin.site.register(Model) | builder typé ModelAdmin + AdminSite.register(...) |
| CRUD complet | natif | natif |
| Pagination liste | natif | .page_size(n) au site + .per_page(n) par modèle |
| Colonnes affichées | list_display | .list(|l| l.display([Title, Status])) — colonnes SeaORM typées |
| Recherche / filtres | natif | .search([...]) + .filter([...]) — colonnes directes uniquement, filtres cumulables |
| Actions de groupe | actions | .action(Col, value, "Libellé") — valeur fixée dans le code, l'UI envoie l'index |
| Édition en masse | list_editable (plusieurs lignes depuis la liste) | édition en lot sur une sélection |
| Suppression en masse | natif | natif si le compte peut supprimer |
| Relations M2M / listes | filter_horizontal / ManyRelatedField | champs liste du modèle (multichoice / checkbox) |
| Routes admin custom | get_urls() | admin "…" => … dans urlpatterns! |
| Templates custom | oui | oui (hiérarchie Tera + .templates(...) par modèle) |
| Permissions | par ressource | Rangs, droits des admins par compte (table et colonne) |
| Comptes superutilisateur | un staff ayant le droit de modifier les utilisateurs peut se rendre superutilisateur (la doc de Django le signale) | is_superuser jamais modifiable depuis l'admin, et le compte d'un superutilisateur (email, réinitialisation, suppression) n'est modifiable que par un superutilisateur |
| Historique modifications | natif (LogEntry, lien « History ») ; diffs de champs avec django-simple-history (tiers) | historique natif (créé/modifié/supprimé) avec diff de champs, vue timeline et filtres (ressource/action/utilisateur) |
| Comptes étendus | AUTH_USER_MODEL (modèle utilisateur personnalisé) | extend!{} + AdminSite::accounts_users(ModelAdmin) |
| Verrouillage optimiste | — | natif — détecte les éditions concurrentes (__original_updated_at) |
| Protection brute-force login | django-axes (tiers) | natif — LoginGuard + rate limiter sur le login admin |
| Email création compte | plugin tiers | natif — lien « choisir mon mot de passe » envoyé à l'adresse du compte, activé à la première définition |
| Rafraîchissement liste partiel | — | natif — list_partial HTMX sans rechargement page |
| Champs lecture seule / create-only | readonly_fields | .readonly([...]) / .create_only([...]) sur le builder form |
| Fieldsets | fieldsets | — |
| Navigation par date | date_hierarchy | — |
| Export | tiers (django-import-export) | — |
| Boutons save avancés | "Save and continue" / "Save and add" | — |
| Fonctionnalité | Django | Runique |
|---|---|---|
| Envoi email | send_mail() natif | Email::new().send() natif |
| Templates email | natif | templates Tera via .template(&tera, "emails/mon.html", ctx.into()) |
| Backend SMTP | configurable | configuration via .env |
| Backend dev (console) | EMAIL_BACKEND = 'console' | EMAIL_BACKEND=console dans .env |
Observabilité / Logging
| Fonctionnalité | Django | Runique |
|---|---|---|
| Config logging | dict LOGGING (logging Python) | arbre RuniqueLog par domaine (forms, middleware, session, auth, admin, db, mailer, migration, templates, errors, builder) — chaque feuille un Option<LogLevel> |
| Tracing structuré | tiers (structlog) | natif (tracing) ; les Result avalés loggent leur file:line via TraceResult::trace / trace_or |
| Sorties | handlers (console / fichier / …) | .output(...) : stdout couleurs, fichier roulant (JSON ou texte, non bloquant), LogSink custom (DB / HTTP / file) |
| Override fichier runtime | — | variable d'env RUNIQUE_LOG_FILE |
| Subscriber côté app | le logging appartient à l'app | .external() — Runique n'installe rien, émet quand même vers la façade tracing |
Internationalisation
| Fonctionnalité | Django | Runique |
|---|---|---|
| Langues supportées | illimitées | 9 langues par défaut (JSON compilé) |
| Fallback | oui | oui (Lang::En) |
| Traduction | _("...") | t("section.clé") / tf("...", &[var]) en Rust, textes du framework |
Performance & Déploiement
| Aspect | Django | Runique |
|---|---|---|
| Runtime | CPython (interprété) | Tokio async Rust (compilé) |
| Compilation | — | binaire unique |
| ACME / TLS auto | certbot (externe) | natif via feature acme |
Ce qu'il manque encore (comparé à Django)
- Redimensionnement automatique d'images : resize/cropping côté serveur non natif.
- Vues CRUD publiques génériques : pas d'équivalent aux
DetailView,ListView,CreateViewde Django pour les vues publiques — envisagé viacrud!{}(feuille de route, sans engagement). Le panel admin couvre le CRUD backoffice viaModelAdmin. - Signals / hooks modèles :
before_save,after_save,before_delete,after_deletene se déclarent pas dansmodel!{}. La structure interneHooksDefexiste, sans lien avec le DSL ; le comportement généré parmodel!{}ne remplit que les colonnesauto_now. Prévu à la feuille de route, sans engagement. - Management commands : pas d'équivalent à
manage.py custom_command— les opérations one-shot passent parsrc/bin/. - Test client intégré : pas de client HTTP de test natif.
runique testetrunique_testcouvrent le lancement et l'isolation de la base ; les requêtes passent parreqwestoutower::ServiceExt::oneshot. - Fixtures : pas de
loaddata/dumpdata— les seeds sont des fonctions Rust. - Admin inline : pas d'édition d'objets liés directement dans le formulaire parent.
- Admin actions de groupe custom :
.action(...)ne supporte que des mises à jour de colonnes à valeur fixe — aucune logique Rust arbitraire possible sur la sélection (équivalent Django :actionsavec une fonction quelconque). - Admin filtre FK :
.filter([...])ne supporte que les colonnes directes — pas de traversée de relation (article__auteur__nom). - Admin fieldsets : pas de groupement de champs par section dans les formulaires admin (
fieldsetsDjango). - Admin navigation par date : pas de
date_hierarchypour filtrer la liste par année/mois/jour. - Admin export : pas d'export CSV/JSON natif depuis la liste.
- Admin boutons save avancés : pas de "Enregistrer et continuer" / "Enregistrer et ajouter un autre".
- i18n complète :
t()ettf()sont des fonctions Rust pour les textes du framework. Pas de fonction de traduction dans les templates Tera, pas de pluralisation, et un projet ne peut pas encore ajouter ses propres traductions. - Tâches de fond : pas de framework de tâches ; Django en a un depuis la 6.0 (
@task, avec un worker externe). - Sitemap / RSS : pas de génération native.
.sitemap(url)ajoute seulement la directiveSitemap:au/robots.txtgénéré. - Authentification tiers : le mode
Delegatednomme des fournisseurs (Google, Microsoft, Apple, LDAP, SAML, personnalisé), mais aucun flow n'est implémenté. JWT et clés d'API absents. - Observabilité sécurité : CSP
report-uri/report-toabsent, les violations ne sont pas collectées. Les connexions (réussies, échouées, verrouillages) passent par les logs (RuniqueLog) et ne sont pas enregistrées en base ;eihwaz_historytrace les écritures de l'admin, les droits et les resets.