Comparatif
Runique face à Loco, Cot et Django, ses limites connues et ce qui arrive.
Sommaire
Runique / Loco
CLI
| Commande | Loco | Runique |
|---|---|---|
| Installer | cargo install loco | cargo install runique --features sqlite --locked |
| Créer un projet | loco new (assistant : type d'application, base, worker) | runique new nom |
| Migrations (générer) | cargo loco generate model / generate migration | runique makemigrations |
| Migrations (appliquer) | cargo loco db migrate | runique migration up |
| Migrations (annuler) | cargo loco db down | runique migration down -n 1 |
| Statut migrations | cargo loco db status | runique migration status |
| Régénérer les entités | cargo loco db entities | — (model!{} les génère à la compilation) |
| Générateurs de code | modèle, contrôleur, scaffold, worker, mailer, tâche, planificateur, déploiement | — |
| Lister les routes | cargo loco routes | — |
| Lister les middlewares | cargo loco middleware | — |
| Diagnostic | cargo loco doctor | — |
| Créer un superutilisateur | — | runique create-superuser |
| Démarrer | cargo loco start | cargo run |
| Rechargement du code | cargo loco watch (avec cargo-watch) | — |
Routing
| Fonctionnalité | Loco | Runique |
|---|---|---|
| Déclaration des routes | Routes::new().prefix("notes").add("/", get(list)) | url.rs avec macro urlpatterns!{} |
| Routes dynamiques | .add("/{id}", get(get_one)) | "/users/{id}" dans urlpatterns! |
| Reverse URL | — | {% link "nom_vue" %} |
| Paramètre de chemin | extracteur Axum Path | Path Axum ou request.get_path_as::<i32>("id") |
| Query string | QueryValidate<T> ou extracteur Axum | request.query::<MyStruct>()? |
| Préfixe d'application | AppRoutes | Router::new().nest("/prefix", ...) |
Vues / Handlers
| Fonctionnalité | Loco | Runique |
|---|---|---|
| Handler | async fn hello() -> Result<Response> | async fn ma_vue(mut request: Request) -> AppResult<Response> |
| Accès DB | State(ctx): State<AppContext>, puis ctx.db | request.engine.db |
| Rendu | format::view(&v, "home/hello.html", data!({...})) | request.render("template.html") |
| Réponse JSON | format::json(res) | Json d'Axum |
| Redirection | format::redirect("/url") | Redirect::to("/url") |
| Messages flash | — | macros success!, error!, info!, warning! |
| Connexions secondaires | MultiDbInitializer, connexions nommées | .with_custom_db::<T>() |
Formulaires
| Fonctionnalité | Loco | Runique |
|---|---|---|
| Définition | struct #[derive(Deserialize, Validate)] (crate validator) | #[form(schema = blog, fields = [...])] ou RuniqueForm manuel |
| Lecture et validation | extracteurs FormValidate<T>, JsonValidate<T>, QueryValidate<T> | ValidationForm::try_form(form, &request).await |
| Rendu HTML | — | {% form.nom_form %} ou {% form.nom_form.champ %} |
| CSRF intégré | — | automatique, injecté avant le premier champ |
| Sauvegarde | code du modèle (SeaORM) | validated.save(&db).await, le formulaire porte son entité (FormEntity) |
| Validation croisée | ValidatorTrait | clean() async, avec accès à la base |
| Fichiers | Multipart d'Axum, stockage configurable | FileField et MediaField : type, taille, magic bytes pour les images |
| Stockage | disque, mémoire, S3, GCP, Azure | dossier MEDIA_ROOT |
Templates
| Fonctionnalité | Loco | Runique |
|---|---|---|
| Moteur | Tera, moteur remplaçable (ViewRenderer) | Tera |
| Emplacement | assets/views/ | templates/ |
| Fichiers statiques | middleware static (assets/static) | {% static "file" %}, avec SRI en production |
| Fichiers media | — | {% media "file" %} |
| URL reverse | — | {% link "nom" %} |
| CSRF | — | {% csrf %} |
| Messages | — | {% messages %} |
| Binaire autonome | feature embedded_assets | — |
ORM / Base de données
| Fonctionnalité | Loco | Runique |
|---|---|---|
| ORM | SeaORM | SeaORM |
| Moteurs | SQLite, PostgreSQL (features par défaut) | SQLite, PostgreSQL, MySQL/MariaDB, un moteur par compilation |
| Définition modèle | entités générées depuis les migrations | macro model!{} |
| Migrations | DSL loco_rs::schema, écrites ou générées | runique makemigrations compare au dernier instantané |
| Requêtes | query::condition() et SeaORM | search! et SeaORM |
| Pagination | query::paginate | .paginate(...) de SeaORM |
| Transactions | SeaORM | request.db_atomic() et SeaORM |
| Multi-tenant | feature multi-tenancy | — |
| Extension table framework | — | extend!{} sur les tables eihwaz_* |
Authentification
| Fonctionnalité | Loco | Runique |
|---|---|---|
| Login / Logout | routes /api/auth/* générées | authenticate_user() + login(), logout() |
| JWT | auth::JWT, auth::JWTWithUser<T> | — |
| Clé d'API | find_by_api_key | — |
| Sessions | cookie signé ou chiffré, stores base et Redis (feature auth_session) | tower-sessions (mémoire + repli en base) |
| OAuth2 / OIDC | feature auth_oauth | — |
| Protection brute force | — | LoginGuard |
| Hashage mot de passe | Argon2id (hash::hash_password) | Argon2 par défaut |
| Reset password | routes forgot et reset générées | jetons hashés en base, à usage unique, avec expiration |
| Droits | — | rangs et droits des admins par compte |
Sécurité
| Fonctionnalité | Loco | Runique |
|---|---|---|
| CSRF | — | toujours actif |
| CSP et en-têtes | secure_headers à activer, présets github et owasp | actifs par défaut, nonce par requête |
| Rate limiting | couche tower choisie par le projet | RateLimiter, déclaré par route |
| CORS | middleware cors | .with_cors(...) |
| Hôtes autorisés | — | .with_allowed_hosts(...) |
| Open Redirect | — | middleware toujours actif |
| IP réelle derrière un proxy | middleware remote_ip | .with_trusted_proxies(...) |
Panel Admin
| Fonctionnalité | Loco | Runique |
|---|---|---|
| Activation | — | builder typé ModelAdmin + AdminSite.register(...) |
| CRUD, recherche, filtres | — | natif |
| Permissions | — | rangs, droits des admins par compte (table et colonne) |
| Historique | — | natif |
Tâches de fond
| Fonctionnalité | Loco | Runique |
|---|---|---|
| Workers | trait Worker, files Redis, PostgreSQL, SQLite ou threads | — |
| Planificateur | cargo loco scheduler, expressions cron | — |
| Tâches ponctuelles | cargo loco task | — |
| Cache | mémoire, Redis, base de données | — |
| Fonctionnalité | Loco | Runique |
|---|---|---|
| Envoi | mailers envoyés par un worker | Email::new().send() |
| Templates | subject.t, html.t, text.t (Tera) | templates Tera via .template("emails/mon.html") |
| Backend SMTP | fichiers config/*.yaml | .env |
| Backend dev (console) | — | EMAIL_BACKEND=console |
Observabilité / Logging
| Fonctionnalité | Loco | Runique |
|---|---|---|
| Config logging | clés logger en YAML | arbre RuniqueLog par domaine |
| Base | tracing | tracing |
| Formats | compact, pretty, json | stdout, fichier JSON ou texte, LogSink |
| Priorité | RUST_LOG | RUST_LOG |
Tests
| Fonctionnalité | Loco | Runique |
|---|---|---|
| Commande | cargo test | runique test |
| Requêtes HTTP | request::<App, _, _>(), TestServer | reqwest ou tower::ServiceExt::oneshot |
| Isolation base | request_with_create_db, #[serial] | runique_test, transaction par test |
Déploiement
| Aspect | Loco | Runique |
|---|---|---|
| Configuration | config/*.yaml, LOCO_ENV | .env, RUNIQUE_ENV |
| Livraison | binaire <package>-cli | binaire |
| Générateurs | Docker, nginx, AWS Lambda | — |
| TLS automatique | — | feature acme |
| Licence | Apache-2.0 | MIT |
Runique / Cot
CLI
| Commande | Cot | Runique |
|---|---|---|
| Installer | cargo install --locked cot-cli | cargo install runique --features sqlite --locked |
| Créer un projet | cot new nom | runique new nom |
| Migrations (générer) | cot migration make | runique makemigrations |
| Migration vide | cot migration new <nom> | — |
| Migrations (appliquer) | automatique au démarrage | runique migration up |
| Migrations (annuler) | cargo run migration rollback <nom> | runique migration down -n 1 |
| Statut migrations | cot migration list | runique migration status |
| Créer un superutilisateur | code de App::init dans l'exemple du guide | runique create-superuser |
| Collecter les statiques | cargo run -- collect-static public/ | — |
| Démarrer | cargo run | cargo run |
| Rechargement du code | bacon (bacon.toml généré) | — |
Routing
| Fonctionnalité | Cot | Runique |
|---|---|---|
| Déclaration des routes | Router::with_urls([Route::with_handler_and_name("/", index, "index")]) | url.rs avec macro urlpatterns!{} |
| Routes dynamiques | "/hello/{name}" | "/users/{id}" dans urlpatterns! |
| Reverse URL | cot::reverse!(urls, "nom") | {% link "nom_vue" %} |
| Paramètre de chemin | extracteur Path(name): Path<String> | Path Axum ou request.get_path_as::<i32>("id") |
Vues / Handlers
| Fonctionnalité | Cot | Runique |
|---|---|---|
| Handler | async fn index() -> cot::Result<Html> | async fn ma_vue(mut request: Request) -> AppResult<Response> |
| Accès DB | extracteur db: Database | request.engine.db |
| Rendu template | IndexTemplate { .. }.render()? | request.render("template.html") |
| Redirection | reverse_redirect!(request, "nom") | Redirect::to("/url") |
| Messages flash | — | macros success!, error!, info!, warning! |
| Pages d'erreur | error_handler(), page de debug avec debug = true | templates 404.html, 500.html, page de debug avec DEBUG=true |
| API | feature openapi, swagger-ui | — |
Formulaires
| Fonctionnalité | Cot | Runique |
|---|---|---|
| Définition | #[derive(Form)] | #[form(schema = blog, fields = [...])] ou RuniqueForm manuel |
| Lecture et validation | ContactForm::from_request(&mut request).await? | ValidationForm::try_form(form, &request).await |
| Validation intégrée | #[form(opts(max_length = 100))] | contraintes du modèle ou du champ |
| Validation personnalisée | context.add_error(FormErrorTarget::Field("title"), ...) | clean() async, avec accès à la base |
| Rendu HTML | {{ form }} ou form.fields() | {% form.nom_form %} ou {% form.nom_form.champ %} |
| CSRF intégré | — | automatique, injecté avant le premier champ |
| Fichiers | form/fields/files.rs | FileField et MediaField : type, taille, magic bytes pour les images |
Templates
| Fonctionnalité | Cot | Runique |
|---|---|---|
| Moteur | Askama, templates liés à une struct | Tera |
| Héritage | {% extends %} / {% block %} | idem |
| Inclusion | {% include %} | idem |
| Fichiers statiques | static_files!(...), extracteur StaticFiles | {% static "file" %}, avec SRI en production |
| Version des statiques | rewrite = "query_param" | ?v= automatique |
| URL reverse | cot::reverse!(urls, "nom") | {% link "nom" %} |
| Internationalisation | — | t() et tf() en Rust, 9 langues pour les textes du framework |
ORM / Base de données
| Fonctionnalité | Cot | Runique |
|---|---|---|
| ORM | ORM propre (sea-query, sqlx) | SeaORM |
| Moteurs | SQLite, PostgreSQL, MySQL, activés ensemble par défaut | SQLite, PostgreSQL, MySQL/MariaDB, un moteur par compilation |
| Définition modèle | #[model] sur une struct | macro model!{} |
| Clé étrangère | ForeignKey<User> | belongs_to, avec action de suppression obligatoire |
| Requêtes | query!(Customer, $id == 5), Customer::objects() | search!, Model::objects |
| Insertion en lot | bulk_insert | SeaORM |
| Transactions | db.begin(), imbrication par savepoints | request.db_atomic() et SeaORM |
| Migration personnalisée | #[migration_op], forwards et backwards | migration SeaORM écrite à la main |
Authentification
| Fonctionnalité | Cot | Runique |
|---|---|---|
| Login / Logout | extracteur Auth : auth.login(...), auth.logout() | authenticate_user() + login(), logout() |
| Utilisateurs | DatabaseUser, DatabaseUserApp | table eihwaz_users, extensible par extend!{} |
| Sessions | stores mémoire, base, fichier, Redis | tower-sessions (mémoire + repli en base), store personnalisable |
| Cookie de session | Secure, HttpOnly, SameSite=Strict par défaut, configurables | HttpOnly, SameSite=Strict, Secure en production |
| Hashage mot de passe | Argon2 | Argon2 par défaut |
| Protection brute force | — | LoginGuard |
| Reset password | — | jetons hashés en base, à usage unique, avec expiration |
Sécurité
| Fonctionnalité | Cot | Runique |
|---|---|---|
| CSRF | — | toujours actif |
| CSP et en-têtes de sécurité | — | actifs par défaut, nonce par requête |
| Rate limiting | — | RateLimiter, déclaré par route |
| CORS | — | .with_cors(...) |
| Hôtes autorisés | — | .with_allowed_hosts(...) |
| Open Redirect | — | middleware toujours actif |
Panel Admin
| Fonctionnalité | Cot | Runique |
|---|---|---|
| Activation | #[derive(AdminModel)] + DefaultAdminModelManager | builder typé ModelAdmin + AdminSite.register(...) |
| CRUD complet | natif | natif |
| Pagination | native | .page_size(n), .per_page(n) |
| Recherche / filtres | — | .search([...]), .filter([...]) |
| Actions de groupe | — | .action(Col, value, "Libellé") |
| Accès | utilisateur connecté | rangs, droits des admins par compte (table et colonne) |
| Historique | — | natif |
Email et cache
| Fonctionnalité | Cot | Runique |
|---|---|---|
| Envoi email | EmailMessage::builder(), feature email | Email::new().send() |
| Backend dev (console) | type = "console" | EMAIL_BACKEND=console |
| Configuration | [email] en TOML | .env |
| Cache | mémoire, Redis, fichier (Cache) | — |
Observabilité / Logging
| Fonctionnalité | Cot | Runique |
|---|---|---|
| Base | tracing | tracing |
| Configuration | — | arbre RuniqueLog par domaine, stdout, fichier, LogSink |
Tests
| Fonctionnalité | Cot | Runique |
|---|---|---|
| Commande | cargo test | runique test |
| Unitaires | TestRequestBuilder | tower::ServiceExt::oneshot |
| Intégration | Client::new(MyProject) | reqwest |
| Base de test | TestDatabase::new_sqlite(), new_postgres, new_mysql | runique_test, transaction par test |
| Bout en bout | #[cot::e2e_test] | — |
Déploiement
| Aspect | Cot | Runique |
|---|---|---|
| Configuration | config/dev.toml, config/prod.toml | .env, RUNIQUE_ENV |
| TLS automatique | — | feature acme |
| Licence | MIT ou Apache-2.0 | MIT |
Runique / Django
CLI
Commandes réelles du binaire runique (voir runique/src/bin/runique.rs) : new, create-superuser, test, makemigrations, migration up. Pas de runique start — l'admin est du Rust plain, cargo run suffit. Annuler / lister les migrations passe par SeaORM, pas par un sous-commande Runique.
| 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 multipart natif — validation type/taille, magic bytes pour les images, SVG rejeté, é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(...) |
.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("emails/mon.html") |
| 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.
Limites connues
Base de données et migrations
| Limite | Détail |
|---|---|
Un moteur par compilation (sqlite, postgres, mysql)SQLite : un seul écrivain à la fois, SQLITE_BUSY sous forte chargeChanger de moteur demande une base neuve : les données ne sont pas transférées | Base de données |
Le changement de type d'une colonne n'est pas migré (--force écrit une migration vide, à compléter à la main)Des snapshots copiés sans leurs migrations font croire que les tables existent Identifiants SQL limités à 64 caractères sous MySQL / MariaDB migration up : une migration n'est atomique que sous Postgres (SQLite, MySQL / MariaDB : une migration qui échoue peut rester à moitié appliquée)Un arrêt brutal pendant makemigrations peut laisser des fichiers à moitié écrits (git pour restaurer) | Migrations, Makemigrations |
search! : in (vec) avec un vec vide, pas de filtre conditionnel inline | Requêtes |
Formulaires et messages
| Limite | Détail |
|---|---|
| Un échec d'écriture en session perd le message flash sans erreur | Macros flash |
| Un fichier publié puis une écriture en base qui échoue brutalement (coupure) peuvent laisser un fichier orphelin | Champs |
Sécurité, sessions, authentification
| Limite | Détail |
|---|---|
RateLimiter et LoginGuard n'ont pas de plafond mémoireRateLimiter et LoginGuard comptent par processus : avec plusieurs instances, le quota est multiplié | Rate limiting |
LoginGuard bloque par nom d'utilisateur : quelqu'un peut bloquer un compte dont il connaît le nom | LoginGuard |
| Le store de sessions mémoire appartient à un processus | Store de sessions |
Un store personnalisé (with_session_store) ne peut pas être parcouru par utilisateur : un reset de mot de passe ne ferme pas ses sessions | Session |
Mode Delegated : aucun fournisseur externe (OAuth, LDAP, SAML) n'est implémenté | Mots de passe |
| ACME : un seul site par machine (port 80 exclusif) | Réseau |
Admin
| Limite | Détail |
|---|---|
.links, .search_help, .empty_value acceptés mais sans effet sur l'affichage | Builder ModelAdmin |
Pas de feuille de thème globale (contournement par un bloc).with_htmx est global, pas de fragment de liste par modèleUn template de page ou une feuille de style absents ne sont signalés qu'à l'affichage | Surcharge |
| Pas d'interface de gestion des sessions | — |
| Many-to-many : déclaré dans le DSL par une table pivot, sans gestion dans l'admin | Évolutions |
Développement local
| Limite | Détail |
|---|---|
Les cookies ignorent le port : deux projets sur 127.0.0.1 partagent (et s'écrasent) le cookie de session | Dépannage |
Solutions en attendant
| Limite | Solution |
|---|---|
RateLimiter et LoginGuard sans plafond mémoire, compteurs par processus | Limiter aussi au reverse proxy (exemple ci-dessous) |
LoginGuard bloque par nom d'utilisateur | L'associer à une limite par IP, au reverse proxy ou avec RateLimiter |
| Store de sessions mémoire propre à un processus | Une seule instance, ou un reverse proxy qui renvoie chaque visiteur vers la même (ip_hash avec nginx) |
| ACME : un seul site par machine | Confier le TLS à un reverse proxy dès qu'il y a plusieurs sites |
| SQLite : un seul écrivain à la fois | PostgreSQL pour une charge en écriture soutenue |
| Migration atomique seulement sous PostgreSQL | Sauvegarder la base avant migration up sur SQLite et MySQL / MariaDB |
| Changement de type de colonne non migré | Compléter à la main la migration écrite par --force |
Cookies communs à deux projets sur 127.0.0.1 | Ouvrir l'un sur 127.0.0.1, l'autre sur localhost |
Limite par IP avec nginx, à adapter. La mémoire est plafonnée par la zone, et une requête refusée reçoit un code 503 par défaut :
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
location /login {
limit_req zone=login burst=5 nodelay;
proxy_pass http://127.0.0.1:3000;
}
À venir
Sans date ni engagement : un bug ou une faille découverts passent avant les chantiers prévus.
- Publication de la 4.0 : finalisation avant la mise en ligne sur crates.io, avec des tests sur des comptes à faibles droits et des groupes, et une revue OWASP complète.
- Correctifs de sécurité (4.0.x) : plafonner le nombre de clés gardées en mémoire par
RateLimiteretLoginGuard, et tester la protection contre les open redirects avec des charges connues de contournement d'URL. - 4.1, makemigrations : générer toutes les opérations de schéma sans étape manuelle : suppression
de tables, création et suppression des tables de relation, respect de cascade, restrict et
set_null, vrai
ALTERlors d'un changement de type, détection des changements de valeur par défaut, et colonnes à la fois renommées et modifiées. Les noms de contraintes sont lus en base, et les changements destructeurs indiquent ce qu'ils touchent. La table pivot du many-to-many est générée depuis le DSL du modèle ; une table écrite à la main, avec des colonnes en plus, reste possible. - 4.2, base de données :
get_or_create,update_or_create,exists(),last(), chargement des relations en une requête, méthodes d'accès du many-to-many et sa gestion dans l'admin.search!accepte aussi des groupes ET dans un OU, et la négation d'une condition ou d'un groupe, toujours avec des requêtes typées. - Tracing : finir la sortie fichier avec rotation, ajouter un identifiant de requête et ne laisser aucune erreur ignorée en silence. Les clés de traduction manquantes seront journalisées.
- Langues ajoutées par un projet : permettre à un projet de fournir une langue absente de Runique, enregistrée par le builder.
- Admin : filtres par date et par clé étrangère, navigation par date,
fieldsets, champs en lecture seule, autocomplétion, édition inline, actions de groupe en Rust, export CSV/JSON et « enregistrer et continuer ».
Sans engagement : ces pistes remonteront quand un chantier se terminera.
- Authentification : protection à deux niveaux contre la force brute, puis OAuth/OIDC, JWT, clés d'API, journal des connexions et rapports de violation CSP.
- ORM et DSL : hooks de modèle, agrégats dans
search!, index déclarés, bornes et paliers numériques. - Outils et stockage : vues CRUD publiques, client de test intégré, fixtures, commandes personnalisées, sitemap et RSS, redimensionnement d'images, et point d'extension de stockage pour les services compatibles S3.