use std::path::Path; use anyhow::{Result, anyhow}; use crate::cmd::{Bash, Cmd, Manage, Words}; use crate::ctx::Ctx; use crate::output::note; use crate::project::Project; const DEBUG_TEMPLATE: &str = r#"from django.core.management.base import BaseCommand class Command(BaseCommand): def handle(self, *args, **options): pass "#; pub fn make_command(ctx: &Ctx, app: &Path, name: &str) -> Result<()> { let app_name = app.to_string_lossy(); let app_dir = app; // it becomes `.py` under the app, and django imports it by this name, // so anything that is not an identifier would land the file somewhere else // or somewhere django will never look for it if !is_module_name(name) { return Err(anyhow!( "{name:?} is not a usable command name; \ django imports it as a python module, so it can hold only \ letters, digits and underscores" )); } if !app_dir.is_dir() { return Err(anyhow!("directory {app_name} does not exist")); } note!(ctx, "found app {app_name}"); let management_dir = app_dir.join("management"); if !management_dir.exists() { ctx.fs().create_dir(&management_dir)?; ctx.fs().touch(&management_dir.join("__init__.py"))?; note!(ctx, "created module {app_name}.management") }; let commands_dir = management_dir.join("commands"); if !commands_dir.exists() { ctx.fs().create_dir(&commands_dir)?; ctx.fs().touch(&commands_dir.join("__init__.py"))?; note!(ctx, "created module {app_name}.management.commands") }; ctx.fs().write_new( &commands_dir.join(format!("{name}.py")), DEBUG_TEMPLATE.as_bytes(), )?; note!(ctx, "created command {app_name}.management.commands.{name}"); Ok(()) } fn is_module_name(name: &str) -> bool { !name.is_empty() && !name.starts_with(|c: char| c.is_ascii_digit()) && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') } pub fn bash(ctx: &Ctx) -> Result<()> { Bash.in_service(ctx, &service(ctx)?).replace(ctx) } pub fn run(ctx: &Ctx, rest: &[String]) -> Result<()> { Words::new(rest) .in_service(ctx, &service(ctx)?) .replace(ctx) } pub fn manage(ctx: &Ctx, rest: &[String]) -> Result<()> { Manage::new(rest) .in_service(ctx, &service(ctx)?) .replace(ctx) } // shortcuts pub fn makemigrations(ctx: &Ctx) -> Result<()> { manage(ctx, &["makemigrations".to_string()]) } pub fn migrate(ctx: &Ctx, rest: &[String]) -> Result<()> { let mut args = vec!["migrate".to_string()]; args.extend_from_slice(rest); manage(ctx, &args) } pub fn shell(ctx: &Ctx) -> Result<()> { manage(ctx, &["shell".to_string()]) } fn service(ctx: &Ctx) -> Result { Project::resolve(ctx)?.django() } #[cfg(test)] mod tests { use super::is_module_name; #[test] fn a_command_name_has_to_be_a_python_module_name() { for name in ["report", "send_mail", "_private", "sync2"] { assert!(is_module_name(name), "should be usable: {name}"); } // a path would put the file somewhere other than the app for name in ["../../../etc/cron.d/x", "a/b", "with space", "dash-ed", ""] { assert!(!is_module_name(name), "should be refused: {name}"); } } }