diff --git a/CHANGELOG.md b/CHANGELOG.md index fa21825..e44d467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Django's `collectstatic` command is now automatically run for Django apps that use static files. ([#108](https://github.com/heroku/buildpacks-python/pull/108)) + ## [0.6.0] - 2023-08-25 ### Changed diff --git a/src/django.rs b/src/django.rs new file mode 100644 index 0000000..80acddc --- /dev/null +++ b/src/django.rs @@ -0,0 +1,118 @@ +use crate::utils::{self, CapturedCommandError, StreamedCommandError}; +use indoc::indoc; +use libcnb::Env; +use libherokubuildpack::log::log_info; +use std::io; +use std::path::Path; +use std::process::Command; + +const MANAGEMENT_SCRIPT_NAME: &str = "manage.py"; + +pub(crate) fn is_django_installed(dependencies_layer_dir: &Path) -> io::Result { + dependencies_layer_dir.join("bin/django-admin").try_exists() +} + +pub(crate) fn run_django_collectstatic( + app_dir: &Path, + command_env: &Env, +) -> Result<(), DjangoCollectstaticError> { + if !has_management_script(app_dir) + .map_err(DjangoCollectstaticError::CheckManagementScriptExists)? + { + log_info(indoc! {" + Skipping automatic static file generation since no Django 'manage.py' + script (or symlink to one) was found in the root directory of your + application." + }); + return Ok(()); + } + + if !has_collectstatic_command(app_dir, command_env) + .map_err(DjangoCollectstaticError::CheckCollectstaticCommandExists)? + { + log_info(indoc! {" + Skipping automatic static file generation since the 'django.contrib.staticfiles' + feature is not enabled in your app's Django configuration." + }); + return Ok(()); + } + + log_info("Running 'manage.py collectstatic'"); + utils::run_command_and_stream_output( + Command::new("python") + .args([ + MANAGEMENT_SCRIPT_NAME, + "collectstatic", + "--link", + // Using `--noinput` instead of `--no-input` since the latter requires Django 1.9+. + "--noinput", + ]) + .current_dir(app_dir) + .env_clear() + .envs(command_env), + ) + .map_err(DjangoCollectstaticError::CollectstaticCommand) +} + +fn has_management_script(app_dir: &Path) -> io::Result { + app_dir.join(MANAGEMENT_SCRIPT_NAME).try_exists() +} + +fn has_collectstatic_command( + app_dir: &Path, + command_env: &Env, +) -> Result { + utils::run_command_and_capture_output( + Command::new("python") + .args([MANAGEMENT_SCRIPT_NAME, "help", "collectstatic"]) + .current_dir(app_dir) + .env_clear() + .envs(command_env), + ) + .map_or_else( + |error| match error { + // We need to differentiate between the command not existing (due to the staticfiles app + // not being installed) and the Django config or mange.py script being broken. Ideally + // we'd inspect the output of `manage.py help --commands` but that command unhelpfully + // exits zero even if the app's `DJANGO_SETTINGS_MODULE` wasn't a valid module. + CapturedCommandError::NonZeroExitStatus(output) + if String::from_utf8_lossy(&output.stderr).contains("Unknown command") => + { + Ok(false) + } + _ => Err(error), + }, + |_| Ok(true), + ) +} + +/// Errors that can occur when running the Django collectstatic command. +#[derive(Debug)] +pub(crate) enum DjangoCollectstaticError { + CheckCollectstaticCommandExists(CapturedCommandError), + CheckManagementScriptExists(io::Error), + CollectstaticCommand(StreamedCommandError), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn has_management_script_django_project() { + assert!(has_management_script(Path::new( + "tests/fixtures/django_staticfiles_latest_django" + )) + .unwrap()); + } + + #[test] + fn has_management_script_empty() { + assert!(!has_management_script(Path::new("tests/fixtures/empty")).unwrap()); + } + + #[test] + fn has_management_script_io_error() { + assert!(has_management_script(Path::new("tests/fixtures/empty/.gitkeep")).is_err()); + } +} diff --git a/src/errors.rs b/src/errors.rs index 376908f..2d6809b 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,9 +1,10 @@ +use crate::django::DjangoCollectstaticError; use crate::layers::pip_dependencies::PipDependenciesLayerError; use crate::layers::python::PythonLayerError; use crate::package_manager::DeterminePackageManagerError; use crate::python_version::{PythonVersion, PythonVersionError, DEFAULT_PYTHON_VERSION}; use crate::runtime_txt::{ParseRuntimeTxtError, RuntimeTxtError}; -use crate::utils::{DownloadUnpackArchiveError, StreamedCommandError}; +use crate::utils::{CapturedCommandError, DownloadUnpackArchiveError, StreamedCommandError}; use crate::BuildpackError; use indoc::{formatdoc, indoc}; use libherokubuildpack::log::log_error; @@ -46,6 +47,8 @@ fn on_buildpack_error(error: BuildpackError) { &io_error, ), BuildpackError::DeterminePackageManager(error) => on_determine_package_manager_error(error), + BuildpackError::DjangoCollectstatic(error) => on_django_collectstatic_error(error), + BuildpackError::DjangoDetection(error) => on_django_detection_error(&error), BuildpackError::PipDependenciesLayer(error) => on_pip_dependencies_layer_error(error), BuildpackError::PythonLayer(error) => on_python_layer_error(error), BuildpackError::PythonVersion(error) => on_python_version_error(error), @@ -217,6 +220,76 @@ fn on_pip_dependencies_layer_error(error: PipDependenciesLayerError) { }; } +fn on_django_detection_error(error: &io::Error) { + log_io_error( + "Unable to determine if this is a Django-based app", + "checking if the 'django-admin' command exists", + error, + ); +} + +fn on_django_collectstatic_error(error: DjangoCollectstaticError) { + match error { + DjangoCollectstaticError::CheckCollectstaticCommandExists(error) => match error { + CapturedCommandError::Io(io_error) => log_io_error( + "Unable to inspect Django configuration", + "running 'python manage.py help collectstatic' to inspect the Django configuration", + &io_error, + ), + CapturedCommandError::NonZeroExitStatus(output) => log_error( + "Unable to inspect Django configuration", + formatdoc! {" + The 'python manage.py help collectstatic' Django management command + (used to check whether Django's static files feature is enabled) + failed ({exit_status}). + + Details: + + {stderr} + + This indicates there is a problem with your application code or Django + configuration. Try running the 'manage.py' script locally to see if the + same error occurs. + ", + exit_status = &output.status, + stderr = String::from_utf8_lossy(&output.stderr) + }, + ), + }, + DjangoCollectstaticError::CheckManagementScriptExists(io_error) => log_io_error( + "Unable to inspect Django configuration", + "checking if the 'manage.py' script exists", + &io_error, + ), + DjangoCollectstaticError::CollectstaticCommand(error) => match error { + StreamedCommandError::Io(io_error) => log_io_error( + "Unable to generate Django static files", + "running 'python manage.py collectstatic' to generate Django static files", + &io_error, + ), + StreamedCommandError::NonZeroExitStatus(exit_status) => log_error( + "Unable to generate Django static files", + formatdoc! {" + The 'python manage.py collectstatic --link --noinput' Django management + command to generate static files failed ({exit_status}). + + This is most likely due an issue in your application code or Django + configuration. See the log output above for more information. + + If you are using the WhiteNoise package to optimize the serving of static + files with Django (recommended), check that your app is using the Django + config options shown here: + https://whitenoise.readthedocs.io/en/stable/django.html + + Or, if you do not need to use static files in your app, disable the + Django static files feature by removing 'django.contrib.staticfiles' + from 'INSTALLED_APPS' in your app's Django configuration. + "}, + ), + }, + }; +} + fn log_io_error(header: &str, occurred_whilst: &str, io_error: &io::Error) { // We don't suggest opening a support ticket, since a subset of I/O errors can be caused // by issues in the application. In the future, perhaps we should try and split these out? diff --git a/src/main.rs b/src/main.rs index 87c8119..87d5fa7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ #![allow(clippy::large_enum_variant)] #![allow(clippy::result_large_err)] +mod django; mod errors; mod layers; mod package_manager; @@ -13,6 +14,7 @@ mod python_version; mod runtime_txt; mod utils; +use crate::django::DjangoCollectstaticError; use crate::layers::pip_cache::PipCacheLayer; use crate::layers::pip_dependencies::{PipDependenciesLayer, PipDependenciesLayerError}; use crate::layers::python::{PythonLayer, PythonLayerError}; @@ -80,7 +82,7 @@ impl Buildpack for PythonBuildpack { // Create the layers for the application dependencies and package manager cache. // In the future support will be added for package managers other than pip. - match package_manager { + let (dependencies_layer_dir, dependencies_layer_env) = match package_manager { PackageManager::Pip => { log_header("Installing dependencies using Pip"); let pip_cache_layer = context.handle_layer( @@ -97,9 +99,18 @@ impl Buildpack for PythonBuildpack { pip_cache_dir: pip_cache_layer.path, }, )?; - pip_layer.env + (pip_layer.path, pip_layer.env) } }; + command_env = dependencies_layer_env.apply(Scope::Build, &command_env); + + if django::is_django_installed(&dependencies_layer_dir) + .map_err(BuildpackError::DjangoDetection)? + { + log_header("Generating Django static files"); + django::run_django_collectstatic(&context.app_dir, &command_env) + .map_err(BuildpackError::DjangoCollectstatic)?; + } BuildResultBuilder::new().build() } @@ -115,6 +126,10 @@ pub(crate) enum BuildpackError { DetectIo(io::Error), /// Errors determining which Python package manager to use for a project. DeterminePackageManager(DeterminePackageManagerError), + /// Errors running the Django collectstatic command. + DjangoCollectstatic(DjangoCollectstaticError), + /// IO errors when detecting whether Django is installed. + DjangoDetection(io::Error), /// Errors installing the project's dependencies into a layer using Pip. PipDependenciesLayer(PipDependenciesLayerError), /// Errors installing Python and required packaging tools into a layer. diff --git a/tests/django_test.rs b/tests/django_test.rs new file mode 100644 index 0000000..5029b8e --- /dev/null +++ b/tests/django_test.rs @@ -0,0 +1,174 @@ +use crate::tests::builder; +use indoc::indoc; +use libcnb_test::{assert_contains, assert_empty, BuildConfig, PackResult, TestRunner}; + +// This test uses symlinks for requirements.txt and manage.py to confirm that it's possible to use +// them when the Django app is nested inside a subdirectory (such as in backend+frontend monorepos). +#[test] +#[ignore = "integration test"] +fn django_staticfiles_latest_django() { + TestRunner::default().build( + BuildConfig::new(builder(), "tests/fixtures/django_staticfiles_latest_django") + // Tests that env vars are passed to the 'manage.py' script invocations. + .env("EXPECTED_ENV_VAR", "1"), + |context| { + assert_empty!(context.pack_stderr); + assert_contains!( + context.pack_stdout, + indoc! {" + [Generating Django static files] + Running 'manage.py collectstatic' + + 1 static file symlinked to '/workspace/backend/staticfiles'. + "} + ); + }, + ); +} + +// This tests the oldest Django version that works on Python 3.9 (which is the +// oldest Python that is available on all of our supported builders). +#[test] +#[ignore = "integration test"] +fn django_staticfiles_legacy_django() { + TestRunner::default().build( + BuildConfig::new(builder(), "tests/fixtures/django_staticfiles_legacy_django"), + |context| { + assert_empty!(context.pack_stderr); + assert_contains!( + context.pack_stdout, + indoc! {" + Successfully installed Django-1.8.19 + + [Generating Django static files] + Running 'manage.py collectstatic' + Linking '/workspace/testapp/static/robots.txt' + + 1 static file symlinked to '/workspace/staticfiles'. + "} + ); + }, + ); +} + +#[test] +#[ignore = "integration test"] +fn django_no_manage_py() { + TestRunner::default().build( + BuildConfig::new(builder(), "tests/fixtures/django_no_manage_py"), + |context| { + assert_empty!(context.pack_stderr); + assert_contains!( + context.pack_stdout, + indoc! {" + [Generating Django static files] + Skipping automatic static file generation since no Django 'manage.py' + script (or symlink to one) was found in the root directory of your + application. + "} + ); + }, + ); +} + +#[test] +#[ignore = "integration test"] +fn django_staticfiles_app_not_enabled() { + TestRunner::default().build( + BuildConfig::new( + builder(), + "tests/fixtures/django_staticfiles_app_not_enabled", + ), + |context| { + assert_empty!(context.pack_stderr); + assert_contains!( + context.pack_stdout, + indoc! {" + [Generating Django static files] + Skipping automatic static file generation since the 'django.contrib.staticfiles' + feature is not enabled in your app's Django configuration. + "} + ); + }, + ); +} + +#[test] +#[ignore = "integration test"] +fn django_invalid_settings_module() { + TestRunner::default().build( + BuildConfig::new(builder(), "tests/fixtures/django_invalid_settings_module") + .expected_pack_result(PackResult::Failure), + |context| { + assert_contains!( + context.pack_stdout, + indoc! {" + [Generating Django static files] + "} + ); + assert_contains!( + context.pack_stderr, + indoc! {" + [Error: Unable to inspect Django configuration] + The 'python manage.py help collectstatic' Django management command + (used to check whether Django's static files feature is enabled) + failed (exit status: 1). + + Details: + + Traceback (most recent call last): + "} + ); + // Full traceback omitted since it will change across Django/Python versions causing test churn. + assert_contains!( + context.pack_stderr, + indoc! {" + ModuleNotFoundError: No module named 'nonexistent-module' + + + This indicates there is a problem with your application code or Django + configuration. Try running the 'manage.py' script locally to see if the + same error occurs. + "} + ); + }, + ); +} + +#[test] +#[ignore = "integration test"] +fn django_staticfiles_misconfigured() { + TestRunner::default().build( + BuildConfig::new(builder(), "tests/fixtures/django_staticfiles_misconfigured") + .expected_pack_result(PackResult::Failure), + |context| { + assert_contains!( + context.pack_stdout, + indoc! {" + [Generating Django static files] + Running 'manage.py collectstatic' + "} + ); + assert_contains!( + context.pack_stderr, + indoc! {" + [Error: Unable to generate Django static files] + The 'python manage.py collectstatic --link --noinput' Django management + command to generate static files failed (exit status: 1). + + This is most likely due an issue in your application code or Django + configuration. See the log output above for more information. + + If you are using the WhiteNoise package to optimize the serving of static + files with Django (recommended), check that your app is using the Django + config options shown here: + https://whitenoise.readthedocs.io/en/stable/django.html + + Or, if you do not need to use static files in your app, disable the + Django static files feature by removing 'django.contrib.staticfiles' + from 'INSTALLED_APPS' in your app's Django configuration. + "} + ); + }, + ); +} diff --git a/tests/fixtures/django_invalid_settings_module/manage.py b/tests/fixtures/django_invalid_settings_module/manage.py new file mode 100644 index 0000000..5ed9a65 --- /dev/null +++ b/tests/fixtures/django_invalid_settings_module/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "nonexistent-module.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/tests/fixtures/django_invalid_settings_module/requirements.txt b/tests/fixtures/django_invalid_settings_module/requirements.txt new file mode 100644 index 0000000..94a0e83 --- /dev/null +++ b/tests/fixtures/django_invalid_settings_module/requirements.txt @@ -0,0 +1 @@ +Django diff --git a/tests/fixtures/django_no_manage_py/requirements.txt b/tests/fixtures/django_no_manage_py/requirements.txt new file mode 100644 index 0000000..94a0e83 --- /dev/null +++ b/tests/fixtures/django_no_manage_py/requirements.txt @@ -0,0 +1 @@ +Django diff --git a/tests/fixtures/django_staticfiles_app_not_enabled/manage.py b/tests/fixtures/django_staticfiles_app_not_enabled/manage.py new file mode 100644 index 0000000..8bd034f --- /dev/null +++ b/tests/fixtures/django_staticfiles_app_not_enabled/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproject.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/tests/fixtures/django_staticfiles_app_not_enabled/requirements.txt b/tests/fixtures/django_staticfiles_app_not_enabled/requirements.txt new file mode 100644 index 0000000..94a0e83 --- /dev/null +++ b/tests/fixtures/django_staticfiles_app_not_enabled/requirements.txt @@ -0,0 +1 @@ +Django diff --git a/tests/fixtures/django_staticfiles_app_not_enabled/testproject/__init__.py b/tests/fixtures/django_staticfiles_app_not_enabled/testproject/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/django_staticfiles_app_not_enabled/testproject/settings.py b/tests/fixtures/django_staticfiles_app_not_enabled/testproject/settings.py new file mode 100644 index 0000000..5922fd0 --- /dev/null +++ b/tests/fixtures/django_staticfiles_app_not_enabled/testproject/settings.py @@ -0,0 +1,15 @@ +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent + +INSTALLED_APPS = [ + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + # The staticfiles app (which is what provides the collectstatic command) is not enabled. + # "django.contrib.staticfiles", +] + +STATIC_ROOT = BASE_DIR / "staticfiles" +STATIC_URL = "static/" diff --git a/tests/fixtures/django_staticfiles_latest_django/backend/manage.py b/tests/fixtures/django_staticfiles_latest_django/backend/manage.py new file mode 100644 index 0000000..8bd034f --- /dev/null +++ b/tests/fixtures/django_staticfiles_latest_django/backend/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproject.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/tests/fixtures/django_staticfiles_latest_django/backend/requirements.txt b/tests/fixtures/django_staticfiles_latest_django/backend/requirements.txt new file mode 100644 index 0000000..94a0e83 --- /dev/null +++ b/tests/fixtures/django_staticfiles_latest_django/backend/requirements.txt @@ -0,0 +1 @@ +Django diff --git a/tests/fixtures/django_staticfiles_latest_django/backend/testapp/__init__.py b/tests/fixtures/django_staticfiles_latest_django/backend/testapp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/django_staticfiles_latest_django/backend/testapp/static/robots.txt b/tests/fixtures/django_staticfiles_latest_django/backend/testapp/static/robots.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/django_staticfiles_latest_django/backend/testproject/__init__.py b/tests/fixtures/django_staticfiles_latest_django/backend/testproject/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/django_staticfiles_latest_django/backend/testproject/settings.py b/tests/fixtures/django_staticfiles_latest_django/backend/testproject/settings.py new file mode 100644 index 0000000..4e0abde --- /dev/null +++ b/tests/fixtures/django_staticfiles_latest_django/backend/testproject/settings.py @@ -0,0 +1,15 @@ +import os +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent + +INSTALLED_APPS = [ + "django.contrib.staticfiles", + "testapp", +] + +STATIC_ROOT = BASE_DIR / "staticfiles" +STATIC_URL = "static/" + +# Tests that app env vars are passed to the 'manage.py' script invocations. +assert "EXPECTED_ENV_VAR" in os.environ diff --git a/tests/fixtures/django_staticfiles_latest_django/manage.py b/tests/fixtures/django_staticfiles_latest_django/manage.py new file mode 120000 index 0000000..9bdc62b --- /dev/null +++ b/tests/fixtures/django_staticfiles_latest_django/manage.py @@ -0,0 +1 @@ +backend/manage.py \ No newline at end of file diff --git a/tests/fixtures/django_staticfiles_latest_django/requirements.txt b/tests/fixtures/django_staticfiles_latest_django/requirements.txt new file mode 120000 index 0000000..ed17bf4 --- /dev/null +++ b/tests/fixtures/django_staticfiles_latest_django/requirements.txt @@ -0,0 +1 @@ +backend/requirements.txt \ No newline at end of file diff --git a/tests/fixtures/django_staticfiles_legacy_django/manage.py b/tests/fixtures/django_staticfiles_legacy_django/manage.py new file mode 100644 index 0000000..97ed576 --- /dev/null +++ b/tests/fixtures/django_staticfiles_legacy_django/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproject.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/tests/fixtures/django_staticfiles_legacy_django/requirements.txt b/tests/fixtures/django_staticfiles_legacy_django/requirements.txt new file mode 100644 index 0000000..e258a24 --- /dev/null +++ b/tests/fixtures/django_staticfiles_legacy_django/requirements.txt @@ -0,0 +1,3 @@ +# This is the oldest Django version that works on Python 3.9 (which is the +# oldest Python that is available on all of our supported builders). +Django==1.8.19 diff --git a/tests/fixtures/django_staticfiles_legacy_django/runtime.txt b/tests/fixtures/django_staticfiles_legacy_django/runtime.txt new file mode 100644 index 0000000..815b82f --- /dev/null +++ b/tests/fixtures/django_staticfiles_legacy_django/runtime.txt @@ -0,0 +1 @@ +python-3.9.18 diff --git a/tests/fixtures/django_staticfiles_legacy_django/testapp/__init__.py b/tests/fixtures/django_staticfiles_legacy_django/testapp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/django_staticfiles_legacy_django/testapp/static/robots.txt b/tests/fixtures/django_staticfiles_legacy_django/testapp/static/robots.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/django_staticfiles_legacy_django/testproject/__init__.py b/tests/fixtures/django_staticfiles_legacy_django/testproject/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/django_staticfiles_legacy_django/testproject/settings.py b/tests/fixtures/django_staticfiles_legacy_django/testproject/settings.py new file mode 100644 index 0000000..e0eaa4c --- /dev/null +++ b/tests/fixtures/django_staticfiles_legacy_django/testproject/settings.py @@ -0,0 +1,13 @@ +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent + +INSTALLED_APPS = [ + "django.contrib.staticfiles", + "testapp", +] + +STATIC_ROOT = BASE_DIR / "staticfiles" +STATIC_URL = "static/" + +SECRET_KEY = "example" diff --git a/tests/fixtures/django_staticfiles_misconfigured/manage.py b/tests/fixtures/django_staticfiles_misconfigured/manage.py new file mode 100644 index 0000000..8bd034f --- /dev/null +++ b/tests/fixtures/django_staticfiles_misconfigured/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproject.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/tests/fixtures/django_staticfiles_misconfigured/requirements.txt b/tests/fixtures/django_staticfiles_misconfigured/requirements.txt new file mode 100644 index 0000000..94a0e83 --- /dev/null +++ b/tests/fixtures/django_staticfiles_misconfigured/requirements.txt @@ -0,0 +1 @@ +Django diff --git a/tests/fixtures/django_staticfiles_misconfigured/testproject/__init__.py b/tests/fixtures/django_staticfiles_misconfigured/testproject/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/django_staticfiles_misconfigured/testproject/settings.py b/tests/fixtures/django_staticfiles_misconfigured/testproject/settings.py new file mode 100644 index 0000000..1a521e1 --- /dev/null +++ b/tests/fixtures/django_staticfiles_misconfigured/testproject/settings.py @@ -0,0 +1,6 @@ +INSTALLED_APPS = [ + "django.contrib.staticfiles", +] + +# This is an invalid STATIC_ROOT value if collectstatic is being used. +STATIC_ROOT = None diff --git a/tests/fixtures/pip_basic/manage.py b/tests/fixtures/pip_basic/manage.py new file mode 100644 index 0000000..db7e7d5 --- /dev/null +++ b/tests/fixtures/pip_basic/manage.py @@ -0,0 +1,2 @@ +# Tests that manage.py alone doesn't trigger Django collectstatic. +raise RuntimeError("This is not a Django app, so manage.py should not be run!") diff --git a/tests/mod.rs b/tests/mod.rs index f519abc..44222bf 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -6,6 +6,7 @@ use std::env; mod detect_test; +mod django_test; mod package_manager_test; mod pip_test; mod python_version_test;