Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a3950c2cf | ||
|
|
482ff13dd3 | ||
|
|
7c3ec11070 | ||
|
|
aad053c52c | ||
|
|
298945835b | ||
|
|
8619c220d3 | ||
|
|
018d71e999 | ||
|
|
75c049d7ed | ||
|
|
dd3ea6ca8e | ||
|
|
d6fa789804 | ||
|
|
8b48a9b9bb | ||
|
|
9793da4dbf | ||
|
|
9b6edfec0e | ||
|
|
dfcd598da3 | ||
|
|
d64ca9ddb2 | ||
|
|
3e6cb2f055 | ||
|
|
5ce5ebbb09 | ||
|
|
ca79f1968e | ||
|
|
ca077e380c | ||
|
|
44299ed8a9 | ||
|
|
916d97ffdf | ||
|
|
66927b988a | ||
|
|
df68383b44 | ||
|
|
d3066a7ceb | ||
|
|
72e6b0df0f | ||
|
|
7b8d6f3342 | ||
|
|
fe9eec70d5 | ||
|
|
ad09fa0b79 | ||
|
|
5984961878 | ||
|
|
5b755b789c | ||
|
|
382d2a0f56 | ||
|
|
c38d4b3832 | ||
|
|
3200c9f9b2 | ||
|
|
fafe29c2cb | ||
|
|
9e3f5ead8f | ||
|
|
260d41b10d | ||
|
|
2d7d712ecf | ||
|
|
3ffd85d2ca | ||
|
|
8c1195880d | ||
|
|
c1262d02e9 | ||
|
|
63989b7f67 | ||
|
|
b3b0c660e6 | ||
|
|
3de5d577a1 | ||
|
|
e5f93e829b | ||
|
|
ce5cda9330 |
No files matched your search
@@ -16,10 +16,10 @@ jobs:
|
||||
python-version: ["3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
|
||||
@@ -19,4 +19,4 @@ jobs:
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
use-github-auto-merge: true
|
||||
target: minor
|
||||
target: squash
|
||||
@@ -12,10 +12,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ jobs:
|
||||
permissions:
|
||||
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#--------------------------------------------------#
|
||||
# The following was generated by djinit: #
|
||||
#--------------------------------------------------#
|
||||
AGENTS.md
|
||||
|
||||
# Python
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
+2
-1
@@ -7,7 +7,7 @@ packages = ["src/djinit"]
|
||||
|
||||
[project]
|
||||
name = "djinitx"
|
||||
version = "0.3.5"
|
||||
version = "0.3.8"
|
||||
description = "A CLI tool to setup Django project"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
@@ -38,6 +38,7 @@ classifiers=[
|
||||
"Framework :: Django :: 4.2",
|
||||
"Framework :: Django :: 5.1",
|
||||
"Framework :: Django :: 5.2",
|
||||
"Framework :: Django :: 6.0",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
questionary==2.1.1
|
||||
rich==15.0.0
|
||||
# dev
|
||||
ruff==0.15.14
|
||||
pre-commit==4.6.0
|
||||
ruff==0.16.9
|
||||
pre-commit==4.6.2
|
||||
@@ -1,63 +0,0 @@
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
setup(
|
||||
name="djinitx",
|
||||
version="0.3.5",
|
||||
description="A CLI tool to setup Django projects",
|
||||
long_description=open("README.md").read(),
|
||||
long_description_content_type="text/markdown",
|
||||
author="Sankalp Tharu",
|
||||
author_email="sankalptharu50028@gmail.com",
|
||||
url="https://github.com/S4NKALP/djinit",
|
||||
packages=find_packages(where="src"),
|
||||
package_dir={"": "src"},
|
||||
include_package_data=True,
|
||||
install_requires=[
|
||||
"rich>=14.2.0",
|
||||
"questionary>=2.0.1",
|
||||
"typer>=0.21.1",
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"djinit=djinit.cli:app",
|
||||
"dj=djinit.cli:app",
|
||||
]
|
||||
},
|
||||
classifiers=[
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Environment :: Web Environment",
|
||||
"Framework :: Django",
|
||||
"Framework :: Django :: 4.2",
|
||||
"Framework :: Django :: 5.1",
|
||||
"Framework :: Django :: 5.2",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"Topic :: Software Development :: Code Generators",
|
||||
],
|
||||
keywords=[
|
||||
"django",
|
||||
"cli",
|
||||
"project",
|
||||
"djinit",
|
||||
"djinitx",
|
||||
"dj",
|
||||
"django-init",
|
||||
"django project",
|
||||
"django starter",
|
||||
],
|
||||
python_requires=">=3.10",
|
||||
project_urls={
|
||||
"Bug Reports": "https://github.com/S4NKALP/djinit/issues",
|
||||
"Source": "https://github.com/S4NKALP/djinit",
|
||||
"Documentation": "https://github.com/S4NKALP/djinit#readme",
|
||||
},
|
||||
)
|
||||
+392
-17
@@ -1,5 +1,375 @@
|
||||
"""
|
||||
Safe template expression evaluation for djinit.
|
||||
|
||||
Templates use a tiny, comment-style logic language:
|
||||
|
||||
# @IF <expression> ... # @ELSEIF <expression> ... # @ELSE ... # @ENDIF
|
||||
# @LOOP <var> in <expression> ... # @ENDLOOP
|
||||
[[ variable ]] variable / attribute / index substitution
|
||||
|
||||
Expressions are evaluated by walking the parsed syntax tree with an explicit
|
||||
allow-list. ``eval()`` is never used: restricting ``__builtins__`` is *not* a
|
||||
sandbox, because object graphs reachable from ordinary literals (for example
|
||||
``().__class__.__base__.__subclasses__()``) still hand out ``os.system`` and
|
||||
friends. An explicit interpreter has no such escape hatch.
|
||||
"""
|
||||
|
||||
import ast
|
||||
import operator
|
||||
import re
|
||||
from typing import Any, Dict, List
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
__all__ = ["InFileLogicParser", "SafeExpressionError"]
|
||||
|
||||
|
||||
class SafeExpressionError(ValueError):
|
||||
"""Raised when a template expression is malformed or uses a disallowed feature."""
|
||||
|
||||
|
||||
# The only names an expression may reference besides the render context. These are
|
||||
# side-effect free helpers; nothing that can import, open, execute or mutate.
|
||||
_SAFE_BUILTINS: Mapping[str, Any] = {
|
||||
"True": True,
|
||||
"False": False,
|
||||
"None": None,
|
||||
"abs": abs,
|
||||
"all": all,
|
||||
"any": any,
|
||||
"bool": bool,
|
||||
"dict": dict,
|
||||
"enumerate": enumerate,
|
||||
"float": float,
|
||||
"int": int,
|
||||
"len": len,
|
||||
"list": list,
|
||||
"max": max,
|
||||
"min": min,
|
||||
"reversed": reversed,
|
||||
"sorted": sorted,
|
||||
"str": str,
|
||||
"zip": zip,
|
||||
}
|
||||
|
||||
# Attribute names that must never be reachable, even on an otherwise harmless
|
||||
# object. Together with rejecting every ``_``-prefixed name this closes the usual
|
||||
# attribute-hop escape routes (``__class__``, ``__globals__``, ``__subclasses__``…).
|
||||
# ``format``/``format_map`` are included because the *format string* they receive is
|
||||
# resolved with ``getattr`` inside CPython, which would sidestep the checks below.
|
||||
_FORBIDDEN_ATTRIBUTES = frozenset(
|
||||
{
|
||||
"__import__",
|
||||
"breakpoint",
|
||||
"compile",
|
||||
"delattr",
|
||||
"eval",
|
||||
"exec",
|
||||
"exit",
|
||||
"format",
|
||||
"format_map",
|
||||
"fork",
|
||||
"getattr",
|
||||
"globals",
|
||||
"input",
|
||||
"locals",
|
||||
"open",
|
||||
"popen",
|
||||
"quit",
|
||||
"setattr",
|
||||
"spawn",
|
||||
"system",
|
||||
"vars",
|
||||
}
|
||||
)
|
||||
|
||||
# Calling a method is only allowed on plain data values. Without this, a context
|
||||
# value that happens to be a module, class or function would expose its whole
|
||||
# (dangerous) surface through dotted access.
|
||||
_PLAIN_DATA_TYPES = (str, bytes, bytearray, bool, int, float, complex, list, tuple, dict, set, frozenset, Mapping)
|
||||
|
||||
# Guards against cheap resource exhaustion such as ``2 ** 99999999``.
|
||||
_MAX_EXPONENT = 1000
|
||||
|
||||
|
||||
class _SafeExpressionEvaluator(ast.NodeVisitor):
|
||||
"""Interprets a validated expression tree against the render context."""
|
||||
|
||||
_BINARY_OPERATORS = {
|
||||
ast.Add: operator.add,
|
||||
ast.Sub: operator.sub,
|
||||
ast.Mult: operator.mul,
|
||||
ast.Div: operator.truediv,
|
||||
ast.FloorDiv: operator.floordiv,
|
||||
ast.Mod: operator.mod,
|
||||
ast.Pow: operator.pow,
|
||||
}
|
||||
|
||||
_UNARY_OPERATORS = {
|
||||
ast.UAdd: operator.pos,
|
||||
ast.USub: operator.neg,
|
||||
ast.Not: operator.not_,
|
||||
ast.Invert: operator.invert,
|
||||
}
|
||||
|
||||
_COMPARISON_OPERATORS = {
|
||||
ast.Eq: operator.eq,
|
||||
ast.NotEq: operator.ne,
|
||||
ast.Lt: operator.lt,
|
||||
ast.LtE: operator.le,
|
||||
ast.Gt: operator.gt,
|
||||
ast.GtE: operator.ge,
|
||||
ast.Is: operator.is_,
|
||||
ast.IsNot: operator.is_not,
|
||||
ast.In: lambda a, b: operator.contains(b, a),
|
||||
ast.NotIn: lambda a, b: not operator.contains(b, a),
|
||||
}
|
||||
|
||||
def __init__(self, context: Mapping[str, Any]) -> None:
|
||||
# Outermost scope is the render context; comprehension bodies push their own
|
||||
# dicts so loop variables never leak into the surrounding scope.
|
||||
self._scopes: List[Any] = [context]
|
||||
|
||||
# -- entry point ---------------------------------------------------------
|
||||
|
||||
def evaluate(self, tree: ast.Expression) -> Any:
|
||||
return self.visit(tree.body)
|
||||
|
||||
def generic_visit(self, node: ast.AST) -> Any:
|
||||
raise SafeExpressionError(f"{type(node).__name__} is not allowed in template expressions")
|
||||
|
||||
# -- literals ------------------------------------------------------------
|
||||
|
||||
def visit_Constant(self, node: ast.Constant) -> Any:
|
||||
return node.value
|
||||
|
||||
def visit_List(self, node: ast.List) -> list:
|
||||
return [self.visit(element) for element in node.elts]
|
||||
|
||||
def visit_Tuple(self, node: ast.Tuple) -> tuple:
|
||||
return tuple(self.visit(element) for element in node.elts)
|
||||
|
||||
def visit_Set(self, node: ast.Set) -> set:
|
||||
return {self.visit(element) for element in node.elts}
|
||||
|
||||
def visit_Dict(self, node: ast.Dict) -> dict:
|
||||
result: dict = {}
|
||||
for key_node, value_node in zip(node.keys, node.values):
|
||||
if key_node is None:
|
||||
raise SafeExpressionError("Dictionary unpacking is not allowed")
|
||||
result[self.visit(key_node)] = self.visit(value_node)
|
||||
return result
|
||||
|
||||
def visit_JoinedStr(self, node: ast.JoinedStr) -> str:
|
||||
return "".join(str(self.visit(value)) for value in node.values)
|
||||
|
||||
def visit_FormattedValue(self, node: ast.FormattedValue) -> str:
|
||||
value = self.visit(node.value)
|
||||
if node.conversion == 115: # !s
|
||||
return f"{value:s}"
|
||||
if node.conversion == 114: # !r
|
||||
return repr(value)
|
||||
if node.conversion == 97: # !a
|
||||
return ascii(value)
|
||||
if node.format_spec is not None:
|
||||
return format(value, self.visit(node.format_spec))
|
||||
return str(value)
|
||||
|
||||
# -- names and attributes ------------------------------------------------
|
||||
|
||||
def visit_Name(self, node: ast.Name) -> Any:
|
||||
return self._lookup(node.id)
|
||||
|
||||
def _lookup(self, name: str) -> Any:
|
||||
for scope in reversed(self._scopes):
|
||||
if name in scope:
|
||||
return scope[name]
|
||||
if name in _SAFE_BUILTINS:
|
||||
return _SAFE_BUILTINS[name]
|
||||
raise SafeExpressionError(f"Unknown name: {name!r}")
|
||||
|
||||
def visit_Attribute(self, node: ast.Attribute) -> Any:
|
||||
return self._resolve_attribute(self.visit(node.value), node.attr)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_attribute(value: Any, name: str) -> Any:
|
||||
if name.startswith("_") or name in _FORBIDDEN_ATTRIBUTES:
|
||||
raise SafeExpressionError(f"Access to attribute {name!r} is not allowed")
|
||||
if isinstance(value, Mapping):
|
||||
# Templates routinely index plain dicts with dotted syntax.
|
||||
try:
|
||||
return value[name]
|
||||
except KeyError:
|
||||
raise AttributeError(name) from None
|
||||
resolved = getattr(value, name)
|
||||
if callable(resolved) and not isinstance(value, _PLAIN_DATA_TYPES):
|
||||
raise SafeExpressionError(f"Access to method {name!r} is not allowed here")
|
||||
return resolved
|
||||
|
||||
# -- operators -----------------------------------------------------------
|
||||
|
||||
def visit_BoolOp(self, node: ast.BoolOp) -> Any:
|
||||
if isinstance(node.op, ast.And):
|
||||
result: Any = True
|
||||
for value in node.values:
|
||||
result = self.visit(value)
|
||||
if not result:
|
||||
return result
|
||||
return result
|
||||
result = False
|
||||
for value in node.values:
|
||||
result = self.visit(value)
|
||||
if result:
|
||||
return result
|
||||
return result
|
||||
|
||||
def visit_UnaryOp(self, node: ast.UnaryOp) -> Any:
|
||||
handler = self._UNARY_OPERATORS.get(type(node.op))
|
||||
if handler is None:
|
||||
raise SafeExpressionError(f"Unary operator {type(node.op).__name__} is not allowed")
|
||||
return handler(self.visit(node.operand))
|
||||
|
||||
def visit_BinOp(self, node: ast.BinOp) -> Any:
|
||||
handler = self._BINARY_OPERATORS.get(type(node.op))
|
||||
if handler is None:
|
||||
raise SafeExpressionError(f"Binary operator {type(node.op).__name__} is not allowed")
|
||||
left = self.visit(node.left)
|
||||
right = self.visit(node.right)
|
||||
if isinstance(node.op, ast.Pow) and isinstance(left, int) and isinstance(right, int):
|
||||
if abs(right) > _MAX_EXPONENT:
|
||||
raise SafeExpressionError("Exponent is too large")
|
||||
return handler(left, right)
|
||||
|
||||
def visit_Compare(self, node: ast.Compare) -> bool:
|
||||
left = self.visit(node.left)
|
||||
for operator_node, comparator_node in zip(node.ops, node.comparators):
|
||||
handler = self._COMPARISON_OPERATORS.get(type(operator_node))
|
||||
if handler is None:
|
||||
raise SafeExpressionError(f"Comparison {type(operator_node).__name__} is not allowed")
|
||||
right = self.visit(comparator_node)
|
||||
if not handler(left, right):
|
||||
return False
|
||||
left = right
|
||||
return True
|
||||
|
||||
def visit_IfExp(self, node: ast.IfExp) -> Any:
|
||||
return self.visit(node.body) if self.visit(node.test) else self.visit(node.orelse)
|
||||
|
||||
# -- subscripts ----------------------------------------------------------
|
||||
|
||||
def visit_Subscript(self, node: ast.Subscript) -> Any:
|
||||
value = self.visit(node.value)
|
||||
key = self._resolve_subscript_key(node.slice, value)
|
||||
try:
|
||||
return value[key]
|
||||
except TypeError as exc:
|
||||
raise SafeExpressionError(f"Cannot subscript {type(value).__name__}") from exc
|
||||
|
||||
def _resolve_subscript_key(self, node: ast.AST, value: Any) -> Any:
|
||||
if isinstance(node, ast.Slice):
|
||||
return slice(
|
||||
self._resolve_subscript_bound(node.lower),
|
||||
self._resolve_subscript_bound(node.upper),
|
||||
self._resolve_subscript_bound(node.step),
|
||||
)
|
||||
if isinstance(node, ast.Tuple):
|
||||
return tuple(self.visit(element) for element in node.elts)
|
||||
return self.visit(node)
|
||||
|
||||
def _resolve_subscript_bound(self, node: Optional[ast.AST]) -> Optional[Any]:
|
||||
return None if node is None else self.visit(node)
|
||||
|
||||
# -- calls ---------------------------------------------------------------
|
||||
|
||||
def visit_Call(self, node: ast.Call) -> Any:
|
||||
if any(keyword.arg is None for keyword in node.keywords):
|
||||
raise SafeExpressionError("** unpacking is not allowed in calls")
|
||||
args = [self.visit(argument) for argument in node.args]
|
||||
kwargs = {keyword.arg: self.visit(keyword.value) for keyword in node.keywords}
|
||||
callee = self.visit(node.func)
|
||||
if not callable(callee):
|
||||
raise SafeExpressionError(f"{callee!r} is not callable")
|
||||
return callee(*args, **kwargs)
|
||||
|
||||
# -- comprehensions ------------------------------------------------------
|
||||
|
||||
def visit_ListComp(self, node: ast.ListComp) -> list:
|
||||
result: list = []
|
||||
self._run_comprehension(node.generators, 0, lambda: result.append(self.visit(node.elt)))
|
||||
return result
|
||||
|
||||
def visit_SetComp(self, node: ast.SetComp) -> set:
|
||||
result: set = set()
|
||||
self._run_comprehension(node.generators, 0, lambda: result.add(self.visit(node.elt)))
|
||||
return result
|
||||
|
||||
def visit_GeneratorExp(self, node: ast.GeneratorExp) -> tuple:
|
||||
result: list = []
|
||||
self._run_comprehension(node.generators, 0, lambda: result.append(self.visit(node.elt)))
|
||||
return tuple(result)
|
||||
|
||||
def visit_DictComp(self, node: ast.DictComp) -> dict:
|
||||
result: dict = {}
|
||||
self._run_comprehension(
|
||||
node.generators,
|
||||
0,
|
||||
lambda: result.__setitem__(self.visit(node.key), self.visit(node.value)),
|
||||
)
|
||||
return result
|
||||
|
||||
def _run_comprehension(self, generators: List[ast.comprehension], index: int, emit) -> None:
|
||||
if index >= len(generators):
|
||||
emit()
|
||||
return
|
||||
|
||||
generator = generators[index]
|
||||
for value in self._as_iterable(self.visit(generator.iter)):
|
||||
self._scopes.append({})
|
||||
try:
|
||||
self._assign(generator.target, value)
|
||||
if all(self.visit(condition) for condition in generator.ifs):
|
||||
self._run_comprehension(generators, index + 1, emit)
|
||||
finally:
|
||||
self._scopes.pop()
|
||||
|
||||
@staticmethod
|
||||
def _as_iterable(value: Any) -> Any:
|
||||
if isinstance(value, (str, bytes)) or not hasattr(value, "__iter__"):
|
||||
raise SafeExpressionError(f"{type(value).__name__} is not iterable")
|
||||
return value
|
||||
|
||||
def _assign(self, target: ast.AST, value: Any) -> None:
|
||||
if isinstance(target, ast.Name):
|
||||
self._scopes[-1][target.id] = value
|
||||
return
|
||||
if isinstance(target, (ast.Tuple, ast.List)) and isinstance(value, (tuple, list)):
|
||||
if len(target.elts) != len(value):
|
||||
raise SafeExpressionError("Cannot unpack mismatched number of loop variables")
|
||||
for element, item in zip(target.elts, value):
|
||||
self._assign(element, item)
|
||||
return
|
||||
raise SafeExpressionError("Unsupported loop target")
|
||||
|
||||
|
||||
def safe_eval(expression: str, context: Optional[Mapping[str, Any]] = None) -> Any:
|
||||
"""Evaluate a template expression without ``eval()``.
|
||||
|
||||
Args:
|
||||
expression: The expression source, e.g. ``"use_tailwind and len(app_names) > 0"``.
|
||||
context: Names available to the expression.
|
||||
|
||||
Raises:
|
||||
SafeExpressionError: If the expression is empty, malformed or disallowed.
|
||||
"""
|
||||
source = expression.strip()
|
||||
if not source:
|
||||
raise SafeExpressionError("Empty expression")
|
||||
|
||||
try:
|
||||
tree = ast.parse(source, mode="eval")
|
||||
except SyntaxError as exc:
|
||||
raise SafeExpressionError(f"Invalid expression syntax: {source!r}") from exc
|
||||
|
||||
return _SafeExpressionEvaluator(context or {}).evaluate(tree)
|
||||
|
||||
|
||||
class InFileLogicParser:
|
||||
@@ -11,18 +381,21 @@ class InFileLogicParser:
|
||||
def __init__(self, context: Dict[str, Any] = None):
|
||||
self.context = context or {}
|
||||
|
||||
def _evaluate(self, expression: str) -> Any:
|
||||
"""Evaluate ``expression`` against the current context, safely."""
|
||||
return safe_eval(expression, self.context)
|
||||
|
||||
def _get_value(self, key: str) -> str:
|
||||
"""
|
||||
Supports nested/dotted access like [[ user.name ]] or [[ settings["DEBUG"] ]].
|
||||
For simplicity, it uses eval in the provided context for complex expressions,
|
||||
but falls back to safe dictionary lookup for simple keys.
|
||||
Dotted names are resolved against the context, and arbitrary expressions are
|
||||
evaluated by the safe interpreter. Anything that cannot be resolved is
|
||||
returned verbatim so a missing context key stays visible in the output.
|
||||
"""
|
||||
key = key.strip()
|
||||
try:
|
||||
# Try evaluating the expression in the context
|
||||
return str(eval(key, {"__builtins__": {}}, self.context))
|
||||
except (NameError, SyntaxError, KeyError, TypeError, AttributeError):
|
||||
# Fallback for common patterns or just return the key as is if it fails
|
||||
return str(self._evaluate(key))
|
||||
except (AttributeError, IndexError, KeyError, TypeError, ValueError):
|
||||
return f"[[ {key} ]]"
|
||||
|
||||
def render(self, template_text: str, context: Dict[str, Any] = None) -> str:
|
||||
@@ -44,8 +417,8 @@ class InFileLogicParser:
|
||||
if stripped.startswith("# @IF "):
|
||||
expr = stripped[6:].strip()
|
||||
try:
|
||||
result = bool(eval(expr, {"__builtins__": {}}, self.context))
|
||||
except Exception:
|
||||
result = bool(self._evaluate(expr))
|
||||
except (AttributeError, IndexError, KeyError, TypeError, ValueError):
|
||||
result = False
|
||||
stack.append([result, result])
|
||||
i += 1
|
||||
@@ -65,8 +438,8 @@ class InFileLogicParser:
|
||||
current_stack[0] = False
|
||||
else:
|
||||
try:
|
||||
result = bool(eval(expr, {"__builtins__": {}}, self.context))
|
||||
except Exception:
|
||||
result = bool(self._evaluate(expr))
|
||||
except (AttributeError, IndexError, KeyError, TypeError, ValueError):
|
||||
result = False
|
||||
current_stack[0] = result
|
||||
if result:
|
||||
@@ -113,13 +486,15 @@ class InFileLogicParser:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
parts = stripped[7:].strip().split(" in ")
|
||||
if len(parts) == 2:
|
||||
parts = stripped[7:].strip().split(" in ", 1)
|
||||
if len(parts) == 2 and parts[0].strip() and parts[1].strip():
|
||||
var_name = parts[0].strip()
|
||||
iterable_name = parts[1].strip()
|
||||
try:
|
||||
iterable = eval(iterable_name, {"__builtins__": {}}, self.context)
|
||||
except Exception:
|
||||
iterable = self._evaluate(iterable_name)
|
||||
if not hasattr(iterable, "__iter__"):
|
||||
raise TypeError(f"{iterable_name!r} is not iterable")
|
||||
except (AttributeError, IndexError, KeyError, TypeError, ValueError):
|
||||
iterable = []
|
||||
|
||||
# Capture loop body
|
||||
@@ -197,14 +572,14 @@ class InFileLogicParser:
|
||||
post_content = expr_parts[1].lstrip()
|
||||
|
||||
try:
|
||||
if bool(eval(expr, {"__builtins__": {}}, self.context)):
|
||||
if bool(self._evaluate(expr)):
|
||||
# If Case 1, we want pre_content. If Case 2, we want post_content.
|
||||
# Usually, if pre_content is empty (or just whitespace), it's Case 2.
|
||||
if pre_content:
|
||||
final_lines.append(pre_content + post_content)
|
||||
else:
|
||||
final_lines.append(post_content)
|
||||
except Exception:
|
||||
except (AttributeError, IndexError, KeyError, TypeError, ValueError):
|
||||
pass
|
||||
i += 1
|
||||
continue
|
||||
|
||||
@@ -8,6 +8,7 @@ import os
|
||||
from djinit.core.base import BaseService
|
||||
from djinit.templater import template_engine
|
||||
from djinit.utils.common import CommonUtils
|
||||
from djinit.utils.django import DjangoHelper
|
||||
|
||||
|
||||
class FileCreator(BaseService):
|
||||
@@ -122,6 +123,7 @@ class FileCreator(BaseService):
|
||||
"project_name": self.project_name,
|
||||
"api_module": api_module,
|
||||
"include_api": True,
|
||||
"use_tailwind": self.metadata.get("use_tailwind", False),
|
||||
}
|
||||
if comment_out_api:
|
||||
urls_context["comment_out_api_url"] = True
|
||||
@@ -194,12 +196,26 @@ class FileCreator(BaseService):
|
||||
"Created requirements.txt with Django dependencies",
|
||||
)
|
||||
|
||||
def _registered_app_modules(self) -> list:
|
||||
"""Dotted module paths of the apps that end up in INSTALLED_APPS.
|
||||
|
||||
The unified and single layouts use the generated package itself as their
|
||||
only app, so they have no separate ``app_names`` to report.
|
||||
"""
|
||||
if self.metadata.get("unified_structure"):
|
||||
return ["apps"]
|
||||
if self.metadata.get("single_structure"):
|
||||
return [self.module_name]
|
||||
return CommonUtils.calculate_app_module_paths(self.app_names, self.metadata)
|
||||
|
||||
def create_readme(self) -> None:
|
||||
context = {
|
||||
"project_name": self.project_name,
|
||||
"app_names": self.app_names,
|
||||
"app_list": self._registered_app_modules(),
|
||||
"predefined_structure": bool(self.metadata.get("predefined_structure")),
|
||||
"unified_structure": bool(self.metadata.get("unified_structure")),
|
||||
"single_structure": bool(self.metadata.get("single_structure")),
|
||||
"module_name": self.module_name,
|
||||
}
|
||||
self._render_and_create_file("README.md", "project/readme.md-tpl", context, "Created README.md file")
|
||||
@@ -247,6 +263,7 @@ class FileCreator(BaseService):
|
||||
"app_names": effective_app_modules,
|
||||
"include_apps": True,
|
||||
"include_api": True,
|
||||
"use_tailwind": self.metadata.get("use_tailwind", False),
|
||||
}
|
||||
self._render_and_create_file(
|
||||
"urls.py",
|
||||
@@ -283,6 +300,19 @@ class FileCreator(BaseService):
|
||||
core_subfolders = ["utils", "mixins", "middleware"]
|
||||
self._create_subdirectories_with_init(core_dir, core_subfolders, "apps/core/")
|
||||
|
||||
# Inject base model
|
||||
CommonUtils.create_file_from_template(
|
||||
os.path.join(core_dir, "base.py"),
|
||||
"components/base.py-tpl",
|
||||
{},
|
||||
"Created apps/core/base.py",
|
||||
)
|
||||
|
||||
# Every app that ends up in USER_DEFINED_APPS needs a real app module,
|
||||
# otherwise the generated project cannot start.
|
||||
for app_name in self.app_names or ["users", "core"]:
|
||||
DjangoHelper.startapp(app_name, apps_dir, f"apps.{app_name}")
|
||||
|
||||
# Create api directory structure
|
||||
api_dir = os.path.join(self.project_root, "api")
|
||||
CommonUtils.create_directory_with_init(api_dir, "Created api/__init__.py")
|
||||
@@ -308,7 +338,13 @@ class FileCreator(BaseService):
|
||||
CommonUtils.create_file_from_template(
|
||||
project_urls_path,
|
||||
"config/urls.py-tpl",
|
||||
{"url_type": "project", "project_name": self.project_name, "api_module": "api", "include_api": True},
|
||||
{
|
||||
"url_type": "project",
|
||||
"project_name": self.project_name,
|
||||
"api_module": "api",
|
||||
"include_api": True,
|
||||
"use_tailwind": self.metadata.get("use_tailwind", False),
|
||||
},
|
||||
"Updated project urls.py to include API routes",
|
||||
should_format=True,
|
||||
)
|
||||
@@ -344,6 +380,14 @@ class FileCreator(BaseService):
|
||||
components = ["admin", "models", "serializers", "tests", "urls", "views"]
|
||||
self._create_subdirectories_with_init(apps_dir, components, "apps/")
|
||||
|
||||
# Inject base model
|
||||
CommonUtils.create_file_from_template(
|
||||
os.path.join(apps_dir, "models", "base.py"),
|
||||
"components/base.py-tpl",
|
||||
{},
|
||||
"Created apps/models/base.py",
|
||||
)
|
||||
|
||||
# Create 'api' directory with v1
|
||||
api_dir = os.path.join(apps_dir, "api")
|
||||
CommonUtils.create_directory_with_init(api_dir, "Created apps/api/__init__.py")
|
||||
@@ -393,6 +437,14 @@ class FileCreator(BaseService):
|
||||
components = ["admin", "api", "models", "tests"]
|
||||
self._create_subdirectories_with_init(project_dir, components, f"{self.module_name}/")
|
||||
|
||||
# Inject base model
|
||||
CommonUtils.create_file_from_template(
|
||||
os.path.join(project_dir, "models", "base.py"),
|
||||
"components/base.py-tpl",
|
||||
{},
|
||||
f"Created {self.module_name}/models/base.py",
|
||||
)
|
||||
|
||||
# Create README files for guidance
|
||||
CommonUtils.create_file_from_template(
|
||||
os.path.join(project_dir, "api", "README.md"),
|
||||
@@ -502,3 +554,15 @@ class FileCreator(BaseService):
|
||||
CommonUtils.create_file_with_content(
|
||||
filepath, json.dumps(config, indent=4), "Created .djinit configuration file"
|
||||
)
|
||||
|
||||
def create_standard_shared(self) -> None:
|
||||
"""Create shared directory and inject base model for standard structure."""
|
||||
shared_dir = os.path.join(self.project_root, "shared")
|
||||
CommonUtils.create_directory_with_init(shared_dir, "Created shared/__init__.py")
|
||||
|
||||
CommonUtils.create_file_from_template(
|
||||
os.path.join(shared_dir, "base.py"),
|
||||
"components/base.py-tpl",
|
||||
{},
|
||||
"Created shared/base.py",
|
||||
)
|
||||
@@ -35,6 +35,7 @@ class SetupCreator:
|
||||
if not self.app_names:
|
||||
self.app_names = ["users", "core"]
|
||||
self.project_creator.app_names = self.app_names
|
||||
self.file_creator.app_names = self.app_names
|
||||
|
||||
elif self.metadata.get("unified_structure"):
|
||||
self.metadata.setdefault("project_module_name", "core")
|
||||
@@ -43,12 +44,14 @@ class SetupCreator:
|
||||
if not self.app_names:
|
||||
self.app_names = []
|
||||
self.project_creator.app_names = self.app_names
|
||||
self.file_creator.app_names = self.app_names
|
||||
|
||||
elif self.metadata.get("single_structure"):
|
||||
self.metadata.setdefault("nested_apps", False)
|
||||
if not self.app_names:
|
||||
self.app_names = []
|
||||
self.project_creator.app_names = self.app_names
|
||||
self.file_creator.app_names = self.app_names
|
||||
|
||||
def create(self) -> bool:
|
||||
"""Run the full setup process."""
|
||||
@@ -68,6 +71,7 @@ class SetupCreator:
|
||||
else:
|
||||
steps.append(("Creating Django apps", self.project_creator.create_apps))
|
||||
steps.append(("Creating project URLs", self.file_creator.create_project_urls))
|
||||
steps.append(("Creating shared module", self.file_creator.create_standard_shared))
|
||||
|
||||
steps.extend(
|
||||
[
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class TimeStampedModel(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@@ -1,11 +1,13 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import environ
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
env_path = Path(__file__).resolve().parent.parent / ".env"
|
||||
env = environ.Env()
|
||||
environ.Env.read_env()
|
||||
environ.Env.read_env(env_path)
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="[[ project_name ]].settings.production"))
|
||||
application = get_asgi_application()
|
||||
@@ -9,14 +9,15 @@ from pathlib import Path
|
||||
|
||||
import environ
|
||||
|
||||
env_path = Path(__file__).resolve().parent.parent.parent / ".env"
|
||||
env = environ.Env()
|
||||
environ.Env.read_env()
|
||||
environ.Env.read_env(env_path)
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
||||
|
||||
|
||||
# Application definition
|
||||
@@ -25,7 +26,6 @@ THIRD_PARTY_APPS = [
|
||||
"rest_framework_simplejwt",
|
||||
"rest_framework_simplejwt.token_blacklist",
|
||||
"corsheaders",
|
||||
"drf_yasg",
|
||||
"django_tailwind_cli", # @IF use_tailwind
|
||||
"django_htmx", # @IF use_htmx
|
||||
]
|
||||
@@ -82,7 +82,7 @@ WSGI_APPLICATION = "[[ project_name ]].wsgi.application"
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
@@ -101,12 +101,11 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.2/topics/i18n/
|
||||
# https://docs.djangoproject.com/en/6.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
TIME_ZONE = "UTC"
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Static files and media files
|
||||
STATIC_URL = "/static/"
|
||||
@@ -138,7 +137,6 @@ REST_FRAMEWORK = {
|
||||
"rest_framework_simplejwt.authentication.JWTAuthentication",
|
||||
"rest_framework.authentication.SessionAuthentication",
|
||||
),
|
||||
"DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.coreapi.AutoSchema",
|
||||
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
|
||||
"PAGE_SIZE": 20,
|
||||
}
|
||||
@@ -175,7 +173,6 @@ CORS_ALLOW_HEADERS = [
|
||||
|
||||
CORS_PREFLIGHT_MAX_AGE = 86400 # 24 hours
|
||||
# Security Settings
|
||||
SECURE_BROWSER_XSS_FILTER = True
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
X_FRAME_OPTIONS = "DENY"
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ DEBUG = True
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
||||
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
@@ -30,8 +30,12 @@ DATABASES = {
|
||||
}
|
||||
}
|
||||
|
||||
# Email backend for development
|
||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
# Email settings for development
|
||||
MAILERS = {
|
||||
"default": {
|
||||
"BACKEND": "django.core.mail.backends.console.EmailBackend",
|
||||
},
|
||||
}
|
||||
|
||||
# CORS settings for development
|
||||
# Allow all origins in development (convenient but not secure)
|
||||
|
||||
@@ -43,14 +43,20 @@ CORS_ALLOW_CREDENTIALS = True
|
||||
CORS_ALLOW_PRIVATE_NETWORK = False
|
||||
|
||||
# Email settings
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_HOST = env('EMAIL_HOST', default="smtp.gmail.com")
|
||||
EMAIL_PORT = env.int('EMAIL_PORT', default=587)
|
||||
EMAIL_USE_TLS = env.bool('EMAIL_USE_TLS', default=True)
|
||||
EMAIL_HOST_USER = env('EMAIL_HOST_USER', default="")
|
||||
EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD', default="")
|
||||
DEFAULT_FROM_EMAIL = env('EMAIL_HOST_USER', default="")
|
||||
SERVER_EMAIL = env('EMAIL_HOST_USER', default="")
|
||||
MAILERS = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.mail.backends.smtp.EmailBackend',
|
||||
'OPTIONS': {
|
||||
'host': env('EMAIL_HOST', default='smtp.gmail.com'),
|
||||
'port': env.int('EMAIL_PORT', default=587),
|
||||
'use_tls': env.bool('EMAIL_USE_TLS', default=True),
|
||||
'username': env('EMAIL_HOST_USER', default=''),
|
||||
'password': env('EMAIL_HOST_PASSWORD', default=''),
|
||||
},
|
||||
},
|
||||
}
|
||||
DEFAULT_FROM_EMAIL = env('EMAIL_HOST_USER', default='')
|
||||
SERVER_EMAIL = env('EMAIL_HOST_USER', default='')
|
||||
|
||||
# Security settings for production
|
||||
SECURE_SSL_REDIRECT = True
|
||||
@@ -63,9 +69,6 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
|
||||
# Static files
|
||||
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
||||
|
||||
# @IF use_tailwind
|
||||
## Tailwind
|
||||
TAILWIND_CLI_VERSION = "4.1.3" # Pin version
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
from django.urls import path, re_path
|
||||
from django.conf import settings
|
||||
from django.views.static import serve
|
||||
# @IF include_api or include_apps
|
||||
from django.urls import include
|
||||
# @ENDIF
|
||||
# @IF include_api
|
||||
from django.conf import settings
|
||||
from drf_yasg.views import get_schema_view
|
||||
from drf_yasg import openapi
|
||||
from rest_framework_simplejwt.views import (
|
||||
TokenObtainPairView,
|
||||
TokenRefreshView,
|
||||
@@ -38,22 +37,22 @@ urlpatterns = [
|
||||
# @ENDLOOP
|
||||
# @ENDIF
|
||||
]
|
||||
# @IF include_api
|
||||
# @IF use_tailwind
|
||||
|
||||
# Don't show schema in production
|
||||
# django-browser-reload for tailwind live reload (dev only)
|
||||
if settings.DEBUG:
|
||||
schema_view = get_schema_view(
|
||||
openapi.Info(
|
||||
title="[[ project_name ]] API",
|
||||
default_version="v1",
|
||||
),
|
||||
public=True,
|
||||
)
|
||||
urlpatterns += [
|
||||
path("docs/", schema_view.with_ui("swagger", cache_timeout=0)),
|
||||
path("schema/", schema_view.with_ui("redoc", cache_timeout=0)),
|
||||
path("__reload__/", include("django_browser_reload.urls")),
|
||||
]
|
||||
# @ENDIF
|
||||
|
||||
urlpatterns += [
|
||||
re_path(
|
||||
r"^media/(?P<path>.*)$",
|
||||
serve,
|
||||
{"document_root": settings.MEDIA_ROOT},
|
||||
),
|
||||
]
|
||||
# @ENDIF
|
||||
# @IF url_type == 'api_root'
|
||||
from django.urls import include, path
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import environ
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
env_path = Path(__file__).resolve().parent.parent / ".env"
|
||||
env = environ.Env()
|
||||
environ.Env.read_env()
|
||||
environ.Env.read_env(env_path)
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", env("DJANGO_SETTINGS_MODULE", default="[[ project_name ]].settings.production"))
|
||||
|
||||
|
||||
@@ -3,15 +3,28 @@
|
||||
"""generated with djinit"""
|
||||
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import environ
|
||||
|
||||
# Load environment variables from .env
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
env = environ.Env()
|
||||
env.read_env(BASE_DIR / ".env")
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", os.getenv("DJANGO_SETTINGS_MODULE", "[[ project_name ]].settings.production"))
|
||||
os.environ.setdefault(
|
||||
"DJANGO_SETTINGS_MODULE",
|
||||
env(
|
||||
"DJANGO_SETTINGS_MODULE",
|
||||
default="[[ project_name ]].settings.production",
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
@@ -21,12 +34,9 @@ def main():
|
||||
"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__":
|
||||
env = environ.Env()
|
||||
# Read .env file, if it exists
|
||||
environ.Env.read_env()
|
||||
|
||||
main()
|
||||
@@ -6,12 +6,11 @@ readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
license = { text = "MIT" }
|
||||
dependencies = [
|
||||
"django",
|
||||
"django>=6.0,<7.0",
|
||||
"django-environ",
|
||||
"django-jazzmin",
|
||||
"djangorestframework",
|
||||
"djangorestframework-simplejwt",
|
||||
"drf-yasg",
|
||||
"django-cors-headers",
|
||||
"whitenoise",
|
||||
# @IF database_type == 'mysql'
|
||||
@@ -37,9 +36,9 @@ dependencies = [
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"ruff>=0.14.2",
|
||||
"pytest>=8.0.0",
|
||||
"pytest-django>=4.9.0",
|
||||
"ruff>=0.16.6",
|
||||
"pytest>=9.1.1",
|
||||
"pytest-django>=4.14.0",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
@@ -63,3 +62,6 @@ ignore = [
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["[[ module_name ]]"]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"[[ module_name ]]/settings/*.py" = ["F403", "F405"]
|
||||
@@ -9,6 +9,16 @@ A Django project folder structure is generated with [djinitx/djinit/dj](https://
|
||||
- Essential dependencies and utilities
|
||||
- Production-ready configuration
|
||||
|
||||
# @IF app_list
|
||||
## Apps
|
||||
|
||||
Registered in `[[ module_name ]]/settings/base.py`:
|
||||
|
||||
# @LOOP app in app_list
|
||||
- `[[ app ]]`
|
||||
# @ENDLOOP
|
||||
|
||||
# @ENDIF
|
||||
## Setup
|
||||
|
||||
### Using Just (Recommended)
|
||||
|
||||
@@ -7,7 +7,6 @@ django-environ
|
||||
django-jazzmin
|
||||
djangorestframework
|
||||
djangorestframework_simplejwt
|
||||
drf-yasg
|
||||
django-cors-headers
|
||||
whitenoise
|
||||
django-tailwind-cli # @IF use_tailwind
|
||||
|
||||
@@ -187,6 +187,9 @@ class InputCollector:
|
||||
"""Helper method to generate metadata dictionary."""
|
||||
project_dir = options["project_dir"]
|
||||
project_name = project_dir
|
||||
if project_name == "." or not project_name:
|
||||
project_name = os.path.basename(os.path.abspath(os.getcwd()))
|
||||
|
||||
app_names: list[str] = []
|
||||
|
||||
# Default package_name to "backend" if project_dir is "." or empty
|
||||
|
||||
@@ -65,6 +65,7 @@ class DjangoHelper:
|
||||
"url_type": "project",
|
||||
"project_name": project_name,
|
||||
"django_version": DjangoHelper.DJANGO_VERSION,
|
||||
"use_tailwind": metadata.get("use_tailwind", False),
|
||||
}
|
||||
project_files = [
|
||||
("urls.py", "config/urls.py-tpl", urls_context),
|
||||
|
||||
Reference in new issue
Block a user