diff --git a/djinit/cli.py b/djinit/cli.py index 53dc429..11f8dc5 100644 --- a/djinit/cli.py +++ b/djinit/cli.py @@ -54,6 +54,15 @@ class Cli: # propagate to managers self.project_manager.app_names = self.app_names + # If single structure is enabled, adjust metadata defaults + if self.metadata.get("single_structure"): + # Module name is already set in input_handler + # No nested apps + self.metadata.setdefault("nested_apps", False) + if not self.app_names: + self.app_names = [] + self.project_manager.app_names = self.app_names + steps: List[Tuple[str, Callable[[], bool]]] = [] steps.append(("Creating Django project", self.project_manager.create_project)) @@ -61,6 +70,9 @@ class Cli: if self.metadata.get("unified_structure"): # Build unified structure steps.append(("Creating unified structure", self.file_manager.create_unified_structure)) + elif self.metadata.get("single_structure"): + # Build single folder structure + steps.append(("Creating single folder structure", self.file_manager.create_single_structure)) elif self.metadata.get("predefined_structure"): # Build custom tree and then inject apps into settings steps.append(("Creating predefined structure", self.file_manager.create_predefined_structure)) diff --git a/djinit/scripts/files.py b/djinit/scripts/files.py index b3d7fdc..5bcf197 100644 --- a/djinit/scripts/files.py +++ b/djinit/scripts/files.py @@ -303,6 +303,81 @@ class FileManager: return True + def create_single_structure(self) -> bool: + # Create project directory structure (project/) + project_dir = os.path.join(self.project_root, self.module_name) + create_directory_with_init(project_dir, f"Created {self.module_name}/__init__.py") + + # Create project/settings directory + settings_dir = os.path.join(project_dir, "settings") + create_directory_with_init(settings_dir, f"Created {self.module_name}/settings/__init__.py") + + # Generate secret key for development settings + from djinit.scripts.secretkey_generator import generate_secret_key + + secret_key = generate_secret_key() + + # Create settings files + base_context = { + "project_name": self.module_name, + "app_names": [self.module_name], # Project acts as the app + "use_database_url": self.metadata.get("use_database_url", True), + } + dev_context = {"secret_key": secret_key} + + for filename, context in [ + ("base.py", base_context), + ("development.py", dev_context), + ("production.py", base_context), + ]: + filepath = os.path.join(settings_dir, filename) + template_path = f"project/settings/{filename.replace('.py', '')}.j2" + create_file_from_template( + filepath, template_path, context, f"Created {self.module_name}/settings/{filename}", should_format=True + ) + + # Core project files + project_files = [ + ("urls.py", "project/urls_with_api.j2", {"api_module": f"{self.module_name}.api"}), + ("wsgi.py", "project/wsgi.j2", {}), + ("asgi.py", "project/asgi.j2", {}), + ] + for filename, template, context in project_files: + filepath = os.path.join(project_dir, filename) + create_file_from_template(filepath, template, context, f"Created {self.module_name}/{filename}", should_format=True) + + # Create app component directories + components = ["admin", "api", "models", "routes", "serializers", "tests", "views"] + self._create_subdirectories_with_init(project_dir, components, f"{self.module_name}/") + + # Create basic files for components to ensure valid python packages/modules + + # admin + create_file_from_template( + os.path.join(project_dir, "admin", "__init__.py"), + "base/admin.j2", + {}, + f"Created {self.module_name}/admin/__init__.py" + ) + + # api + create_file_from_template( + os.path.join(project_dir, "api", "urls.py"), + "predefined/api/urls.j2", + {}, + f"Created {self.module_name}/api/urls.py" + ) + + # models + create_file_from_template( + os.path.join(project_dir, "models", "__init__.py"), + "base/models.j2", + {}, + f"Created {self.module_name}/models/__init__.py" + ) + + return True + def create_procfile(self) -> bool: context = {"project_name": self.project_name} return self._render_and_create_file( diff --git a/djinit/scripts/input_handler.py b/djinit/scripts/input_handler.py index c6645ed..c4b345d 100644 --- a/djinit/scripts/input_handler.py +++ b/djinit/scripts/input_handler.py @@ -37,7 +37,9 @@ class ProjectMetadata: use_database_url: bool = False # predefined structure support predefined_structure: bool = False + predefined_structure: bool = False unified_structure: bool = False + single_structure: bool = False project_module_name: str | None = None def to_dict(self) -> dict: @@ -49,7 +51,9 @@ class ProjectMetadata: "nested_dir": self.nested_dir, "use_database_url": self.use_database_url, "predefined_structure": self.predefined_structure, + "predefined_structure": self.predefined_structure, "unified_structure": self.unified_structure, + "single_structure": self.single_structure, "project_module_name": self.project_module_name, } @@ -250,7 +254,7 @@ class InputCollector: return CharReader.get_yes_no(f"[{UIColors.HIGHLIGHT}]Use DATABASE_URL? (Y/n):[/{UIColors.HIGHLIGHT}]") == "y" def _get_structure_metadata( - self, project_dir: str, predefined: bool = False, unified: bool = False + self, project_dir: str, predefined: bool = False, unified: bool = False, single: bool = False, single_module_name: str = None ) -> Tuple[str, str, list[str], dict]: """Helper method to get metadata for predefined/unified structures.""" project_name = project_dir @@ -266,6 +270,9 @@ class InputCollector: package_name = get_package_name(project_dir) project_module_name = "config" if predefined else "core" if unified else None + + if single: + project_module_name = single_module_name or "project" metadata = ProjectMetadata( package_name=package_name, @@ -276,6 +283,7 @@ class InputCollector: use_database_url=use_database_url, predefined_structure=predefined, unified_structure=unified, + single_structure=single, project_module_name=project_module_name, ) @@ -389,10 +397,10 @@ class CharReader: def get_structure_choice() -> str: """Get structure type choice without requiring Enter key.""" return CharReader._get_validated_char_input( - prompt=f"[{UIColors.HIGHLIGHT}]Choose structure type (1/2/3) [default: 1]:[/{UIColors.HIGHLIGHT}]", - valid_keys=["1", "2", "3"], + prompt=f"[{UIColors.HIGHLIGHT}]Choose structure type (1/2/3/4) [default: 1]:[/{UIColors.HIGHLIGHT}]", + valid_keys=["1", "2", "3", "4"], default="1", - error_message="Invalid input '{response}'. Please enter '1', '2', or '3'.", + error_message="Invalid input '{response}'. Please enter '1', '2', '3', or '4'.", ) @staticmethod @@ -459,14 +467,16 @@ def get_user_input() -> Tuple[str, str, str, list, dict]: console.print(f"[{UIColors.MUTED}]1. Standard structure (default Django layout)[/{UIColors.MUTED}]") console.print(f"[{UIColors.MUTED}]2. Predefined structure (apps/users, apps/core, api/)[/{UIColors.MUTED}]") console.print(f"[{UIColors.MUTED}]3. Unified structure (core/, apps/core, apps/api)[/{UIColors.MUTED}]") + console.print(f"[{UIColors.MUTED}]4. Single folder layout (everything in one folder)[/{UIColors.MUTED}]") console.print() structure_choice = CharReader.get_structure_choice() use_predefined = structure_choice == "2" use_unified = structure_choice == "3" + use_single = structure_choice == "4" - if use_predefined or use_unified: + if use_predefined or use_unified or use_single: UIFormatter.print_separator() console.print(f"\n[{UIColors.INFO}]Step 1: Project Setup[/{UIColors.INFO}]\n") console.print( @@ -476,9 +486,22 @@ def get_user_input() -> Tuple[str, str, str, list, dict]: if project_dir is None: UIFormatter.print_error("Maximum attempts reached for project directory name. Exiting.") sys.exit(1) + + # Ask for module name after project directory for single layout + single_module_name = None + if use_single: + console.print() + console.print(f"[{UIColors.MUTED}]Common names: project, core, app[/{UIColors.MUTED}]") + single_module_name = collector.get_validated_input( + "Enter project configuration directory name (default: project)", + validate_project_name, + "directory name", + allow_empty=True + ) or "project" + console.print() project_name, primary_app, app_names, metadata_dict = collector._get_structure_metadata( - project_dir, predefined=use_predefined, unified=use_unified + project_dir, predefined=use_predefined, unified=use_unified, single=use_single, single_module_name=single_module_name ) return project_dir, project_name, primary_app, app_names, metadata_dict diff --git a/djinit/scripts/project.py b/djinit/scripts/project.py index bddfb99..aec852b 100644 --- a/djinit/scripts/project.py +++ b/djinit/scripts/project.py @@ -114,7 +114,7 @@ class ProjectManager: apps_base_dir = self._get_apps_base_dir() # In predefined or unified structure, apps layout differs; skip strict per-app validation - if not self.metadata.get("predefined_structure") and not self.metadata.get("unified_structure"): + if not self.metadata.get("predefined_structure") and not self.metadata.get("unified_structure") and not self.metadata.get("single_structure"): for app_name in self.app_names: app_files = [ os.path.join(apps_base_dir, app_name, "__init__.py"),