"""
Django Agents Auditor Module.
This module provides comprehensive auditing capabilities for Django projects,
including code quality analysis, security checks, and best practices validation.
The auditor system consists of:
- :class:`AuditReport`: Report generation and formatting
- :class:`BaseAuditor`: Base class for all auditors
- Various specialized auditors for different aspects of Django projects
Example:
Basic usage of the auditor system::
from django_agents.auditors import AuditReport, CodebaseAuditor
report = AuditReport(output_format='markdown')
auditor = CodebaseAuditor()
result = auditor.audit()
report.add_section('Codebase Analysis', result)
print(report.generate())
"""
import ast
from pathlib import Path
from django.apps import apps
from django.conf import settings
[docs]
class AuditReport:
"""
Manages audit report generation and formatting.
This class collects audit results from multiple auditors and generates
formatted reports in various output formats (text, markdown, JSON, HTML).
Args:
output_format (str): Output format for the report. Options: 'text',
'markdown', 'json', 'html'. Defaults to 'text'.
Attributes:
format (str): The output format for the report.
sections (list): List of report sections containing audit results.
total_issues (int): Total count of critical issues found.
total_warnings (int): Total count of warnings found.
total_suggestions (int): Total count of suggestions provided.
Example:
>>> report = AuditReport(output_format='markdown')
>>> report.add_section('Security', {'critical': ['Issue 1']})
>>> print(report.generate())
"""
[docs]
def __init__(self, output_format='text'):
"""
Initialize the audit report.
Args:
output_format (str, optional): Format for output generation.
Defaults to 'text'.
"""
self.format = output_format
self.sections = []
self.total_issues = 0
self.total_warnings = 0
self.total_suggestions = 0
[docs]
def add_section(self, title, content):
"""
Add a section to the report.
Args:
title (str): Section title.
content (dict): Section content containing audit results with keys:
- critical (list): Critical issues
- warnings (list): Warnings
- suggestions (list): Suggestions
- good_practices (list): Detected best practices
"""
self.sections.append({
'title': title,
'content': content,
})
# Count the issues
self.total_issues += content.get('critical_issues', 0)
self.total_warnings += content.get('warnings', 0)
self.total_suggestions += content.get('suggestions', 0)
[docs]
def generate(self):
"""
Generate the final audit report in the specified format.
Returns:
str: Formatted report content.
Note:
The format is determined by the ``output_format`` parameter
passed during initialization.
"""
match self.format:
case 'markdown':
return self._generate_markdown()
case 'json':
return self._generate_json()
case 'html':
return self._generate_html()
case _:
return self._generate_text()
def _generate_text(self):
"""
Generate a plain text formatted report.
Returns:
str: Plain text report with sections and findings.
"""
output = []
for section in self.sections:
output.append(f"\n{'='*60}")
output.append(f"{section['title'].upper()}")
output.append('='*60)
content = section['content']
# Critical issues
if content.get('critical', []):
output.append("\n🔴 CRITICAL ISSUES:")
for issue in content['critical']:
output.append(f" - {issue}")
# Warnings
if content.get('warnings', []):
output.append("\n🟡 WARNINGS:")
for warning in content['warnings']:
output.append(f" - {warning}")
# Suggestions
if content.get('suggestions', []):
output.append("\n💡 SUGGESTIONS:")
for suggestion in content['suggestions']:
output.append(f" - {suggestion}")
# Best practices
if content.get('good_practices', []):
output.append("\n✅ DETECTED BEST PRACTICES:")
for practice in content['good_practices']:
output.append(f" - {practice}")
return '\n'.join(output)
def _generate_markdown(self):
"""
Generate a Markdown formatted report.
Returns:
str: Markdown report with formatted sections.
"""
output = ['# Django audit report\n']
for section in self.sections:
output.append(f"\n## {section['title']}\n")
content = section['content']
if content.get('critical', []):
output.append("### 🔴 Critical issues\n")
for issue in content['critical']:
output.append(f"- {issue}")
output.append("")
if content.get('warnings', []):
output.append("### 🟡 Warnings\n")
for warning in content['warnings']:
output.append(f"- {warning}")
output.append("")
if content.get('suggestions', []):
output.append("### 💡 Suggestions\n")
for suggestion in content['suggestions']:
output.append(f"- {suggestion}")
output.append("")
if content.get('good_practices', []):
output.append("### ✅ Detected best practices\n")
for practice in content['good_practices']:
output.append(f"- {practice}")
output.append("")
return '\n'.join(output)
def _generate_json(self):
"""
Generate a JSON formatted report.
Returns:
str: JSON string containing all audit data and summary.
"""
import json
return json.dumps({
'sections': self.sections,
'summary': {
'critical_issues': self.total_issues,
'warnings': self.total_warnings,
'suggestions': self.total_suggestions,
}
}, indent=2)
def _generate_html(self):
"""
Generate an HTML formatted report.
Returns:
str: Complete HTML document with styled audit report.
"""
output = ['<!DOCTYPE html>', '<html>', '<head>']
output.append('<meta charset="UTF-8">')
output.append('<title>Django Audit Report</title>')
output.append('<style>')
output.append('body { font-family: Arial, sans-serif; margin: 20px; }')
output.append('h1 { color: #0C4B33; }')
output.append('h2 { color: #44B78B; margin-top: 30px; }')
output.append('.critical { color: #DC2626; }')
output.append('.warning { color: #F59E0B; }')
output.append('.suggestion { color: #3B82F6; }')
output.append('.good { color: #10B981; }')
output.append('</style>')
output.append('</head>', '<body>')
output.append('<h1>Django Audit Report</h1>')
for section in self.sections:
output.append(f"<h2>{section['title']}</h2>")
content = section['content']
if content.get('critical', []):
output.append('<h3 class="critical">🔴 Critical issues</h3>')
output.append('<ul>')
for issue in content['critical']:
output.append(f'<li>{issue}</li>')
output.append('</ul>')
if content.get('warnings', []):
output.append('<h3 class="warning">🟡 Warnings</h3>')
output.append('<ul>')
for warning in content['warnings']:
output.append(f'<li>{warning}</li>')
output.append('</ul>')
if content.get('suggestions', []):
output.append('<h3 class="suggestion">💡 Suggestions</h3>')
output.append('<ul>')
for suggestion in content['suggestions']:
output.append(f'<li>{suggestion}</li>')
output.append('</ul>')
if content.get('good_practices', []):
output.append('<h3 class="good">✅ Best practices</h3>')
output.append('<ul>')
for practice in content['good_practices']:
output.append(f'<li>{practice}</li>')
output.append('</ul>')
output.append('</body>', '</html>')
return '\n'.join(output)
[docs]
def get_summary(self):
"""
Return a summary of the audit report.
Returns:
str: Summary containing counts of issues, warnings, and suggestions.
"""
lines = [
f"Critical issues: {self.total_issues}",
f"Warnings: {self.total_warnings}",
f"Suggestions: {self.total_suggestions}",
]
return '\n'.join(lines)
[docs]
class BaseAuditor:
"""
Base class for all auditor implementations.
This abstract class provides the foundation for creating specialized
auditors that analyze different aspects of Django projects.
Attributes:
critical (list): List of critical issues found during audit.
warnings (list): List of warnings identified.
suggestions (list): List of improvement suggestions.
good_practices (list): List of detected best practices.
Note:
Subclasses must implement the :meth:`audit` method to perform
specific audit operations.
"""
[docs]
def __init__(self):
"""Initialize the auditor with empty result lists."""
self.critical = []
self.warnings = []
self.suggestions = []
self.good_practices = []
[docs]
def audit(self):
"""
Perform the audit operation.
This method must be implemented by subclasses to perform
specific audit checks.
Returns:
dict: Audit results from :meth:`get_result`.
Raises:
NotImplementedError: If not implemented by subclass.
"""
raise NotImplementedError
[docs]
def get_result(self):
"""
Return the aggregated audit results.
Returns:
dict: Dictionary containing:
- critical (list): Critical issues
- warnings (list): Warnings
- suggestions (list): Suggestions
- good_practices (list): Best practices
- critical_issues (int): Count of critical issues
"""
return {
'critical': self.critical,
'warnings': self.warnings,
'suggestions': self.suggestions,
'good_practices': self.good_practices,
'critical_issues': len(self.critical),
}
[docs]
class CodebaseAuditor(BaseAuditor):
"""
Auditor for analyzing project codebase structure and quality.
Performs comprehensive analysis of Python files, project structure,
dependencies, and documentation.
Checks include:
- Project structure validation
- Python file analysis (docstrings, function complexity)
- Dependency management
- Documentation presence
"""
[docs]
def audit(self):
"""
Run the codebase audit.
Returns:
dict: Audit results containing identified issues and recommendations.
"""
base_dir = Path(settings.BASE_DIR) if hasattr(settings, 'BASE_DIR') else Path.cwd()
# Check project structure
self._check_project_structure(base_dir)
# Analyze Python files
self._analyze_python_files(base_dir)
# Check dependencies
self._check_dependencies(base_dir)
# Check documentation
self._check_documentation(base_dir)
return self.get_result()
def _check_project_structure(self, base_dir):
"""
Check the project structure for required and recommended files.
Args:
base_dir (Path): Base directory of the project.
"""
required_files = ['manage.py', 'requirements.txt']
recommended_files = ['README.md', '.gitignore', 'setup.py', 'pyproject.toml']
for file in required_files:
if not (base_dir / file).exists():
self.critical.append(f"Missing required file: {file}")
for file in recommended_files:
if not (base_dir / file).exists():
self.warnings.append(f"Missing recommended file: {file}")
else:
self.good_practices.append(f"File {file} is present")
# Check for a tests directory
if not list(base_dir.rglob('test*.py')) and not list(base_dir.rglob('tests/')):
self.critical.append("No test files found")
def _analyze_python_files(self, base_dir):
"""
Analyze Python files for quality metrics.
Checks for:
- Module docstrings
- Function complexity
- Syntax errors
Args:
base_dir (Path): Base directory of the project.
"""
py_files = list(base_dir.rglob('*.py'))
if not py_files:
self.critical.append("No Python files found")
return
total_lines = 0
files_without_docstring = 0
files_with_long_functions = 0
for py_file in py_files:
# Ignore migrations and virtual environments
if 'migrations' in py_file.parts or 'venv' in py_file.parts or '.venv' in py_file.parts:
continue
try:
with open(py_file, 'r', encoding='utf-8') as f:
content = f.read()
total_lines += len(content.splitlines())
# Ensure module docstrings exist
try:
tree = ast.parse(content)
if not ast.get_docstring(tree):
files_without_docstring += 1
# Check function length
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
func_lines = node.end_lineno - node.lineno
if func_lines > 50:
files_with_long_functions += 1
self.warnings.append(
f"Long function ({func_lines} lines) in {py_file.name}: {node.name}"
)
except SyntaxError:
self.warnings.append(f"Syntax error in {py_file}")
except Exception as e:
self.warnings.append(f"Error analyzing {py_file}: {str(e)}")
# Suggestions
if files_without_docstring > len(py_files) * 0.5:
self.warnings.append(
f"{files_without_docstring}/{len(py_files)} files without a module docstring"
)
self.good_practices.append(f"{len(py_files)} Python files analyzed ({total_lines} lines)")
def _check_dependencies(self, base_dir):
"""
Check project dependencies and version pinning.
Args:
base_dir (Path): Base directory of the project.
"""
requirements_file = base_dir / 'requirements.txt'
if requirements_file.exists():
with open(requirements_file, 'r') as f:
deps = f.readlines()
if len(deps) == 0:
self.warnings.append("Empty requirements.txt file")
else:
self.good_practices.append(f"{len(deps)} declared dependencies")
# Ensure pinned versions
unpinned = [dep for dep in deps if '==' not in dep and dep.strip() and not dep.startswith('#')]
if unpinned:
self.suggestions.append(
f"{len(unpinned)} dependencies without pinned versions (use '==')"
)
def _check_documentation(self, base_dir):
"""
Check for project documentation files.
Args:
base_dir (Path): Base directory of the project.
"""
readme = base_dir / 'README.md'
if readme.exists():
with open(readme, 'r', encoding='utf-8') as f:
content = f.read()
if len(content) < 100:
self.warnings.append("README.md is very short (< 100 characters)")
else:
self.good_practices.append("README.md is present and detailed")
# Check the presence of agent documentation
if (base_dir / 'AGENTS.md').exists():
self.good_practices.append("AGENTS.md documentation is present")
else:
self.suggestions.append("Create an AGENTS.md file to document agents")
[docs]
class TestsAuditor(BaseAuditor):
"""
Auditor for analyzing test coverage and quality.
Examines test files, test functions, and testing frameworks
to ensure adequate test coverage.
Checks include:
- Presence of test files
- Number of test functions
- Testing framework configuration (pytest, unittest)
- Coverage tool installation
"""
[docs]
def audit(self):
"""
Run the tests audit.
Returns:
dict: Audit results with test coverage analysis.
"""
base_dir = Path(settings.BASE_DIR) if hasattr(settings, 'BASE_DIR') else Path.cwd()
# Locate every test file
test_files = list(base_dir.rglob('test*.py'))
test_files.extend(list(base_dir.rglob('tests.py')))
test_dirs = list(base_dir.rglob('tests/'))
if not test_files and not test_dirs:
self.critical.append("No test files found in the project")
self.suggestions.append("Write tests to guarantee code quality")
return self.get_result()
# Analyze test files
total_test_functions = 0
total_test_lines = 0
for test_file in test_files:
if 'migrations' in test_file.parts or 'venv' in test_file.parts or '.venv' in test_file.parts:
continue
try:
with open(test_file, 'r', encoding='utf-8') as f:
content = f.read()
total_test_lines += len(content.splitlines())
tree = ast.parse(content)
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
if node.name.startswith('test_'):
total_test_functions += 1
except Exception as e:
self.warnings.append(f"Error analyzing {test_file}: {str(e)}")
# Statistics
if total_test_functions == 0:
self.critical.append("No test functions found")
elif total_test_functions < 10:
self.warnings.append(f"Only {total_test_functions} tests found - coverage is likely insufficient")
else:
self.good_practices.append(f"{total_test_functions} test functions found")
# Check for pytest or unittest
requirements_file = base_dir / 'requirements.txt'
if requirements_file.exists():
with open(requirements_file, 'r') as f:
deps = f.read().lower()
if 'pytest' in deps:
self.good_practices.append("pytest is configured")
elif 'unittest' not in deps:
self.suggestions.append("Install pytest for a better testing experience")
# Check for coverage
if requirements_file.exists():
with open(requirements_file, 'r') as f:
deps = f.read().lower()
if 'coverage' in deps:
self.good_practices.append("coverage is installed")
else:
self.suggestions.append("Install coverage to measure test coverage")
return self.get_result()
[docs]
class AppsAuditor(BaseAuditor):
"""
Auditor for analyzing Django app structure and organization.
Validates Django app configurations and checks for proper
modular organization.
Checks include:
- Installed apps analysis
- App structure validation
- Required files in apps
- Agent integration
"""
[docs]
def audit(self):
"""
Run the Django apps audit.
Returns:
dict: Audit results with app structure analysis.
"""
installed_apps = getattr(settings, 'INSTALLED_APPS', [])
if not installed_apps:
self.critical.append("No applications are installed")
return self.get_result()
# Split Django apps from project apps
django_apps = [app for app in installed_apps if app.startswith('django.')]
third_party_apps = [app for app in installed_apps if not app.startswith('django.') and '.' in app]
project_apps = [app for app in installed_apps if not app.startswith('django.') and '.' not in app]
self.good_practices.append(f"Django applications: {len(django_apps)}")
self.good_practices.append(f"Third-party applications: {len(third_party_apps)}")
self.good_practices.append(f"Project applications: {len(project_apps)}")
# Check the structure of each project app
for app_name in project_apps:
try:
app_config = apps.get_app_config(app_name)
app_path = Path(app_config.path)
# Verify required files
required_files = ['__init__.py', 'models.py', 'views.py']
for file in required_files:
if not (app_path / file).exists():
self.warnings.append(f"App {app_name}: missing {file}")
# Verify recommended files
recommended_files = ['tests.py', 'urls.py', 'admin.py', 'apps.py']
for file in recommended_files:
if (app_path / file).exists():
self.good_practices.append(f"App {app_name}: {file} is present")
else:
self.suggestions.append(f"App {app_name}: create {file}")
# Check for agents
if (app_path / 'agents.py').exists():
self.good_practices.append(f"App {app_name}: agents.py is present")
else:
self.suggestions.append(f"App {app_name}: create agents.py for smart agents")
except LookupError:
self.warnings.append(f"App {app_name} not found")
# Modularity suggestions
if len(project_apps) == 0:
self.warnings.append("No project applications - consider modularizing the code")
elif len(project_apps) == 1:
self.suggestions.append("Only one project app - consider splitting it into functional modules")
return self.get_result()
[docs]
class SettingsAuditor(BaseAuditor):
"""
Auditor for analyzing Django settings and security configuration.
Performs security-focused analysis of Django settings including
DEBUG mode, SECRET_KEY, security middleware, and database configuration.
Checks include:
- DEBUG mode status
- SECRET_KEY security
- ALLOWED_HOSTS configuration
- Security middleware presence
- Database configuration
- Static/media files setup
"""
[docs]
def audit(self):
"""
Run the Django settings audit.
Returns:
dict: Audit results with security and configuration analysis.
"""
# Check DEBUG
debug_mode = getattr(settings, 'DEBUG', True)
if debug_mode:
self.warnings.append("DEBUG is enabled - disable it in production")
else:
self.good_practices.append("DEBUG is disabled")
# Check SECRET_KEY
secret_key = getattr(settings, 'SECRET_KEY', '')
if not secret_key:
self.critical.append("SECRET_KEY is not set")
elif secret_key == 'django-insecure-' or 'changeme' in secret_key.lower():
self.critical.append("SECRET_KEY uses an insecure default")
elif len(secret_key) < 50:
self.warnings.append("SECRET_KEY is too short (< 50 characters)")
else:
self.good_practices.append("SECRET_KEY is properly set")
# Check ALLOWED_HOSTS
allowed_hosts = getattr(settings, 'ALLOWED_HOSTS', [])
if not allowed_hosts or allowed_hosts == ['*']:
self.warnings.append("ALLOWED_HOSTS is not configured or too permissive")
else:
self.good_practices.append(f"ALLOWED_HOSTS is configured ({len(allowed_hosts)} hosts)")
# Check security middleware
middleware = getattr(settings, 'MIDDLEWARE', [])
security_middleware = [
'django.middleware.security.SecurityMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
for mw in security_middleware:
if mw in middleware:
self.good_practices.append(f"{mw.split('.')[-1]} enabled")
else:
self.warnings.append(f"{mw} is missing from MIDDLEWARE")
# Check database configuration
databases = getattr(settings, 'DATABASES', {})
if not databases:
self.critical.append("No database configured")
else:
default_db = databases.get('default', {})
db_engine = default_db.get('ENGINE', '')
if 'sqlite' in db_engine.lower():
self.warnings.append("SQLite is not recommended in production")
elif 'postgresql' in db_engine.lower():
self.good_practices.append("PostgreSQL is configured")
elif 'mysql' in db_engine.lower():
self.good_practices.append("MySQL is configured")
# Check templates configuration
templates = getattr(settings, 'TEMPLATES', [])
if templates:
self.good_practices.append("Templates are configured")
# Check static files
static_url = getattr(settings, 'STATIC_URL', None)
if not static_url:
self.warnings.append("STATIC_URL is not configured")
else:
self.good_practices.append("STATIC_URL is configured")
static_root = getattr(settings, 'STATIC_ROOT', None)
if not static_root:
self.suggestions.append("STATIC_ROOT is not configured - required for collectstatic")
# Check media configuration
media_url = getattr(settings, 'MEDIA_URL', None)
media_root = getattr(settings, 'MEDIA_ROOT', None)
if media_url and media_root:
self.good_practices.append("MEDIA is configured")
elif media_url or media_root:
self.warnings.append("MEDIA configuration is incomplete")
return self.get_result()
[docs]
class ProductionAuditor(BaseAuditor):
"""
Auditor for production deployment readiness.
Analyzes the project for production deployment requirements
including HTTPS configuration, deployment files, and security settings.
Checks include:
- DEBUG mode verification
- Deployment configuration files
- HTTPS/SSL settings
- HSTS configuration
- Logging configuration
- Environment variable templates
"""
[docs]
def audit(self):
"""
Run the production readiness audit.
Returns:
dict: Audit results with production readiness analysis.
"""
base_dir = Path(settings.BASE_DIR) if hasattr(settings, 'BASE_DIR') else Path.cwd()
# Check DEBUG
if getattr(settings, 'DEBUG', True):
self.critical.append("DEBUG=True in production is dangerous")
# Check deployment files
deployment_files = {
'Dockerfile': 'Docker',
'docker-compose.yml': 'Docker Compose',
'requirements.txt': 'Requirements',
'Procfile': 'Heroku',
'gunicorn.conf.py': 'Gunicorn',
'nginx.conf': 'Nginx',
'supervisord.conf': 'Supervisor',
}
for file, tech in deployment_files.items():
if (base_dir / file).exists():
self.good_practices.append(f"{file} is present ({tech})")
if not any((base_dir / file).exists() for file in deployment_files):
self.warnings.append("No deployment files found")
self.suggestions.append("Create deployment configuration files (Docker, Gunicorn, etc.)")
# Check environment variable templates
if (base_dir / '.env.example').exists():
self.good_practices.append(".env.example is present")
else:
self.suggestions.append("Create .env.example to document environment variables")
# Check HTTPS settings
secure_ssl_redirect = getattr(settings, 'SECURE_SSL_REDIRECT', False)
if not secure_ssl_redirect:
self.warnings.append("SECURE_SSL_REDIRECT is not enabled")
session_cookie_secure = getattr(settings, 'SESSION_COOKIE_SECURE', False)
if not session_cookie_secure:
self.warnings.append("SESSION_COOKIE_SECURE is not enabled")
csrf_cookie_secure = getattr(settings, 'CSRF_COOKIE_SECURE', False)
if not csrf_cookie_secure:
self.warnings.append("CSRF_COOKIE_SECURE is not enabled")
# Check HSTS
secure_hsts_seconds = getattr(settings, 'SECURE_HSTS_SECONDS', 0)
if secure_hsts_seconds == 0:
self.warnings.append("SECURE_HSTS_SECONDS is not configured")
elif secure_hsts_seconds < 31536000:
self.suggestions.append("SECURE_HSTS_SECONDS should be at least 31536000 (1 year)")
else:
self.good_practices.append("HSTS is properly configured")
# Check logging configuration
logging_config = getattr(settings, 'LOGGING', None)
if not logging_config:
self.warnings.append("LOGGING is not configured")
self.suggestions.append("Configure LOGGING to monitor the application in production")
else:
self.good_practices.append("LOGGING is configured")
return self.get_result()
[docs]
class DatabaseAuditor(BaseAuditor):
"""
Auditor for database models and migrations.
Analyzes Django models, migrations, and database optimization.
Checks include:
- Model count and structure
- __str__ method presence
- Migrations existence
- Database indexes
"""
[docs]
def audit(self):
"""
Run the database audit.
Returns:
dict: Audit results with database analysis.
"""
# Inspect models
all_models = apps.get_models()
if not all_models:
self.warnings.append("No Django models found")
return self.get_result()
self.good_practices.append(f"{len(all_models)} models found")
# Analyze each model
models_without_str = []
models_without_meta = []
for model in all_models:
model_name = model.__name__
# Check __str__
if not hasattr(model, '__str__') or model.__str__ is object.__str__:
models_without_str.append(model_name)
# Check Meta options
if not hasattr(model, '_meta') or not hasattr(model, 'Meta'):
models_without_meta.append(model_name)
if models_without_str:
self.suggestions.append(
f"{len(models_without_str)} models without __str__: {', '.join(models_without_str[:3])}..."
)
# Check migrations
base_dir = Path(settings.BASE_DIR) if hasattr(settings, 'BASE_DIR') else Path.cwd()
migration_dirs = list(base_dir.rglob('migrations/'))
if not migration_dirs:
self.warnings.append("No migrations directory found")
else:
total_migrations = 0
for migration_dir in migration_dirs:
migrations = list(migration_dir.glob('*.py'))
migrations = [m for m in migrations if m.name != '__init__.py']
total_migrations += len(migrations)
if total_migrations == 0:
self.warnings.append("No migrations found - run makemigrations")
else:
self.good_practices.append(f"{total_migrations} migrations found")
# Check indexes
models_with_indexes = []
for model in all_models:
if hasattr(model, '_meta') and hasattr(model._meta, 'indexes'):
if model._meta.indexes:
models_with_indexes.append(model.__name__)
if models_with_indexes:
self.good_practices.append(
f"{len(models_with_indexes)} models with custom indexes"
)
else:
self.suggestions.append(
"Add indexes to frequently queried fields to improve performance"
)
return self.get_result()
[docs]
class ServerAuditor(BaseAuditor):
"""
Auditor for server and deployment configuration.
Analyzes server setup including WSGI/ASGI, web servers,
and caching configuration.
Checks include:
- WSGI/ASGI file presence
- Gunicorn installation
- Static file serving (WhiteNoise)
- Server configuration files
- Cache configuration
"""
[docs]
def audit(self):
"""
Run the server audit.
Returns:
dict: Audit results with server configuration analysis.
"""
base_dir = Path(settings.BASE_DIR) if hasattr(settings, 'BASE_DIR') else Path.cwd()
# Check wsgi.py and asgi.py
if (base_dir / 'wsgi.py').exists() or any(base_dir.rglob('wsgi.py')):
self.good_practices.append("wsgi.py is present")
else:
self.warnings.append("wsgi.py not found")
if (base_dir / 'asgi.py').exists() or any(base_dir.rglob('asgi.py')):
self.good_practices.append("asgi.py is present (async support)")
# Check Gunicorn
requirements_file = base_dir / 'requirements.txt'
if requirements_file.exists():
with open(requirements_file, 'r') as f:
deps = f.read().lower()
if 'gunicorn' in deps:
self.good_practices.append("Gunicorn is installed")
else:
self.suggestions.append("Install Gunicorn to serve the application in production")
if 'whitenoise' in deps:
self.good_practices.append("WhiteNoise is installed (static files)")
else:
self.suggestions.append("Install WhiteNoise to serve static files")
# Check server configuration files
server_configs = {
'gunicorn.conf.py': 'Gunicorn configuration',
'nginx.conf': 'Nginx configuration',
'apache.conf': 'Apache configuration',
}
for config_file, description in server_configs.items():
if (base_dir / config_file).exists():
self.good_practices.append(f"{description} is present")
# Check async middleware
middleware = getattr(settings, 'MIDDLEWARE', [])
if any('async' in mw.lower() for mw in middleware):
self.good_practices.append("Async middleware detected")
# Check cache configuration
caches = getattr(settings, 'CACHES', {})
if caches:
default_cache = caches.get('default', {})
backend = default_cache.get('BACKEND', '')
if 'dummy' in backend.lower():
self.warnings.append("Dummy cache configured - use Redis or Memcached in production")
elif 'redis' in backend.lower():
self.good_practices.append("Redis is configured for caching")
elif 'memcached' in backend.lower():
self.good_practices.append("Memcached is configured for caching")
else:
self.suggestions.append("Configure a cache system (Redis, Memcached) to improve performance")
return self.get_result()