Files
ahab/src/commands/django.rs

125 lines
3.3 KiB
Rust

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 `<name>.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<String> {
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}");
}
}
}