mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-31 20:13:46 +00:00 
			
		
		
		
	In Django 5.2, manage.py shell automatically imports models. Signed-off-by: Anders Kaseorg <anders@zulip.com>
		
			
				
	
	
		
			158 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			158 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/env python3
 | |
| import configparser
 | |
| import os
 | |
| import sys
 | |
| from collections import defaultdict
 | |
| 
 | |
| BASE_DIR = os.path.dirname(os.path.abspath(__file__))
 | |
| sys.path.append(BASE_DIR)
 | |
| from scripts.lib.setup_path import setup_path
 | |
| 
 | |
| setup_path()
 | |
| 
 | |
| from django.core.management import ManagementUtility, get_commands
 | |
| from django.core.management.color import color_style
 | |
| from typing_extensions import override
 | |
| 
 | |
| from scripts.lib.zulip_tools import assert_not_running_as_root
 | |
| 
 | |
| 
 | |
| def get_filtered_commands() -> dict[str, str]:
 | |
|     """Because Zulip uses management commands in production, `manage.py
 | |
|     help` is a form of documentation for users. Here we exclude from
 | |
|     that documentation built-in commands that are not constructive for
 | |
|     end users or even Zulip developers to run.
 | |
| 
 | |
|     Ideally, we'd do further customization to display management
 | |
|     commands with more organization in the help text, and also hide
 | |
|     development-focused management commands in production.
 | |
|     """
 | |
|     all_commands = get_commands()
 | |
|     documented_commands = dict()
 | |
|     documented_apps = [
 | |
|         # "auth" removed because its commands are not applicable to Zulip.
 | |
|         # "contenttypes" removed because we don't use that subsystem, and
 | |
|         #   even if we did.
 | |
|         "django.core",
 | |
|         "analytics",
 | |
|         # "otp_static" removed because it's a 2FA internals detail.
 | |
|         # "sessions" removed because it's just a cron job with a misleading
 | |
|         #   name, since all it does is delete expired sessions.
 | |
|         # "social_django" removed for similar reasons to sessions.
 | |
|         # "staticfiles" removed because its commands are only usefully run when
 | |
|         #   wrapped by Zulip tooling.
 | |
|         # "two_factor" removed because it's a 2FA internals detail.
 | |
|         "zerver",
 | |
|         "zilencer",
 | |
|     ]
 | |
|     documented_command_subsets = {
 | |
|         "django.core": {
 | |
|             "changepassword",
 | |
|             "dbshell",
 | |
|             "makemigrations",
 | |
|             "migrate",
 | |
|             "shell",
 | |
|             "showmigrations",
 | |
|         },
 | |
|     }
 | |
|     for command, app in all_commands.items():
 | |
|         if app not in documented_apps:
 | |
|             continue
 | |
|         if app in documented_command_subsets and command not in documented_command_subsets[app]:
 | |
|             continue
 | |
| 
 | |
|         documented_commands[command] = app
 | |
|     return documented_commands
 | |
| 
 | |
| 
 | |
| class FilteredManagementUtility(ManagementUtility):
 | |
|     """Replaces the main_help_text function of ManagementUtility with one
 | |
|     that calls our get_filtered_commands(), rather than the default
 | |
|     get_commands() function.
 | |
| 
 | |
|     All other change are just code style differences to pass the Zulip linter.
 | |
|     """
 | |
| 
 | |
|     @override
 | |
|     def main_help_text(self, commands_only: bool = False) -> str:
 | |
|         """Return the script's main help text, as a string."""
 | |
|         if commands_only:
 | |
|             usage = sorted(get_filtered_commands())
 | |
|         else:
 | |
|             usage = [
 | |
|                 "",
 | |
|                 f"Type '{self.prog_name} help <subcommand>' for help on a specific subcommand.",
 | |
|                 "",
 | |
|                 "Available subcommands:",
 | |
|             ]
 | |
|             commands_dict = defaultdict(list)
 | |
|             for name, app in get_filtered_commands().items():
 | |
|                 if app == "django.core":
 | |
|                     app = "django"
 | |
|                 else:
 | |
|                     app = app.rpartition(".")[-1]
 | |
|                 commands_dict[app].append(name)
 | |
|             style = color_style()
 | |
|             for app in sorted(commands_dict):
 | |
|                 usage.append("")
 | |
|                 usage.append(style.NOTICE(f"[{app}]"))
 | |
|                 usage.extend(f"    {name}" for name in sorted(commands_dict[app]))
 | |
|             # Output an extra note if settings are not properly configured
 | |
|             if self.settings_exception is not None:
 | |
|                 usage.append(
 | |
|                     style.NOTICE(
 | |
|                         "Note that only Django core commands are listed "
 | |
|                         f"as settings are not properly configured (error: {self.settings_exception})."
 | |
|                     )
 | |
|                 )
 | |
| 
 | |
|         return "\n".join(usage)
 | |
| 
 | |
| 
 | |
| def execute_from_command_line(argv: list[str] | None = None) -> None:
 | |
|     """Run a FilteredManagementUtility."""
 | |
|     utility = FilteredManagementUtility(argv)
 | |
|     utility.execute()
 | |
| 
 | |
| 
 | |
| if __name__ == "__main__":
 | |
|     if len(sys.argv) > 1 and sys.argv[1] == "email_server":
 | |
|         # This needs to be able to run as root to read certificates
 | |
|         # for STARTTLS, and bind to port 25.
 | |
|         pass
 | |
|     else:
 | |
|         assert_not_running_as_root()
 | |
| 
 | |
|     config_file = configparser.RawConfigParser()
 | |
|     config_file.read("/etc/zulip/zulip.conf")
 | |
|     PRODUCTION = config_file.has_option("machine", "deploy_type")
 | |
|     HAS_SECRETS = os.access("/etc/zulip/zulip-secrets.conf", os.R_OK)
 | |
| 
 | |
|     if PRODUCTION and not HAS_SECRETS:
 | |
|         # The best way to detect running manage.py as another user in
 | |
|         # production before importing anything that would require that
 | |
|         # access is to check for access to /etc/zulip/zulip.conf (in
 | |
|         # which case it's a production server, not a dev environment)
 | |
|         # and lack of access for /etc/zulip/zulip-secrets.conf (which
 | |
|         # should be only readable by root and zulip)
 | |
|         print(
 | |
|             "Error accessing Zulip secrets; manage.py in production must be run as the zulip user."
 | |
|         )
 | |
|         sys.exit(1)
 | |
| 
 | |
|     os.environ.setdefault("DJANGO_SETTINGS_MODULE", "zproject.settings")
 | |
|     from django.conf import settings
 | |
|     from django.core.management.base import CommandError
 | |
| 
 | |
|     from scripts.lib.zulip_tools import log_management_command
 | |
| 
 | |
|     log_management_command(sys.argv, settings.MANAGEMENT_LOG_PATH)
 | |
| 
 | |
|     if "--no-traceback" not in sys.argv and len(sys.argv) > 1:
 | |
|         sys.argv.append("--traceback")
 | |
|     try:
 | |
|         execute_from_command_line(sys.argv)
 | |
|     except CommandError as e:
 | |
|         print(e, file=sys.stderr)
 | |
|         sys.exit(1)
 |