diff --git a/src/flask/cli.py b/src/flask/cli.py index 1a9159ec7c..a2d4add0e9 100644 --- a/src/flask/cli.py +++ b/src/flask/cli.py @@ -594,6 +594,7 @@ def __init__( self.add_command(run_command) self.add_command(shell_command) self.add_command(routes_command) + self.add_command(create_command) self._loaded_plugin_commands = False @@ -1107,6 +1108,384 @@ def routes_command(sort: str, all_methods: bool) -> None: click.echo(template.format(*row)) +@click.command("create", short_help="Create a new Flask project with customizable features.") +@click.option("--project", is_flag=True, help="Create a new project structure.") +@click.option( + "--includes", + default="all", + help="Comma-separated features: sqlite,admin-user,admin-panel,bootstrap,api,all", +) +@click.option("--path", default=".", help="Path where project will be created.") +@click.option("--name", default="flask-app", help="Name of the project.") +def create_command(project: bool, includes: str, path: str, name: str) -> None: + """Create a new Flask project with customizable features. + + By default, creates a full project with all features enabled. + Use --includes to specify which features to include. + + \b + Examples: + flask create --project --name my-app + flask create --project --includes sqlite,admin-user --name simple-app + flask create --project --includes all --path ./projects --name full-app + """ + import os + import shutil + from pathlib import Path + + # Parse includes + if includes == "all": + features = ["sqlite", "admin-user", "admin-panel", "bootstrap", "api"] + else: + features = [f.strip() for f in includes.split(",")] + + # Validate features + valid_features = {"sqlite", "admin-user", "admin-panel", "bootstrap", "api"} + for feature in features: + if feature not in valid_features: + click.echo(f"Error: Invalid feature '{feature}'. Valid features: {', '.join(sorted(valid_features))}", err=True) + return + + # Create project directory + project_path = Path(path) / name + if project_path.exists(): + click.echo(f"Error: Directory '{project_path}' already exists.", err=True) + return + + click.echo(f"Creating project '{name}' at {project_path}...") + click.echo(f"Features: {', '.join(features)}") + + # Create directory structure + directories = [ + project_path, + project_path / "app", + project_path / "static", + project_path / "templates", + ] + + if "admin-panel" in features: + directories.append(project_path / "templates" / "admin") + + for directory in directories: + directory.mkdir(parents=True, exist_ok=True) + + # Get template directory + template_dir = Path(__file__).parent / "cli_templates" + + # Helper function to render template using Jinja2 + def render_template(template_path: Path, output_path: Path, templates_dir: Path = None) -> None: + from jinja2 import Environment, FileSystemLoader, BaseLoader + + # Use FileSystemLoader if templates_dir is provided + if templates_dir and templates_dir.exists(): + env = Environment(loader=FileSystemLoader(str(templates_dir))) + else: + env = Environment(loader=BaseLoader()) + + with open(template_path, "r") as f: + template_content = f.read() + + # Mock url_for for template rendering + def mock_url_for(endpoint, **kwargs): + # Map endpoints to URL patterns + url_map = { + "main.index": "/", + "main.items": "/items", + "main.create_item": "/items/create", + "main.edit_item": f"/items/{kwargs.get('item_id', 1)}/edit", + "main.delete_item": f"/items/{kwargs.get('item_id', 1)}/delete", + "auth.login": "/login", + "auth.logout": "/logout", + "auth.register": "/register", + "admin.dashboard": "/admin/", + "admin.users": "/admin/users", + "admin.toggle_admin": f"/admin/users/{kwargs.get('user_id', 1)}/toggle-admin", + "admin.delete_user": f"/admin/users/{kwargs.get('user_id', 1)}/delete", + "admin.items": "/admin/items", + "admin.delete_item": f"/admin/items/{kwargs.get('item_id', 1)}/delete", + "static": f"/static/{kwargs.get('filename', '')}", + } + return url_map.get(endpoint, f"/{endpoint.replace('.', '/')}") + + # Mock flash function + def mock_flash(message, category="info"): + pass + + # Mock get_flashed_messages function + def mock_get_flashed_messages(with_categories=False): + return [] + + template = env.from_string(template_content) + + # Prepare template context + context = { + "project_name": name, + "features": features, + "url_for": mock_url_for, + "flash": mock_flash, + "get_flashed_messages": mock_get_flashed_messages, + "current_user": type("User", (), {"is_authenticated": False, "is_admin": False, "username": "User"})(), + } + + rendered = template.render(**context) + + with open(output_path, "w") as f: + f.write(rendered) + + # Render base templates + base_templates = { + "app.py.j2": "app/__init__.py", + "config.py.j2": "config.py", + "run.py.j2": "run.py", + "requirements.txt.j2": "requirements.txt", + "routes.py.j2": "app/routes.py", + ".env.j2": ".env", + } + + for template_file, output_file in base_templates.items(): + template_path = template_dir / "base" / template_file + output_path = project_path / output_file + if template_path.exists(): + render_template(template_path, output_path) + + # Create app/__init__.py (empty) + (project_path / "app" / "__init__.py").touch() + + # Render SQLite templates + if "sqlite" in features: + sqlite_templates = { + "models.py.j2": "app/models.py", + "extensions.py.j2": "app/extensions.py", + } + for template_file, output_file in sqlite_templates.items(): + template_path = template_dir / "sqlite" / template_file + output_path = project_path / output_file + if template_path.exists(): + render_template(template_path, output_path) + + # Render admin-user templates + if "admin-user" in features: + admin_user_templates = { + "auth.py.j2": "app/auth.py", + } + for template_file, output_file in admin_user_templates.items(): + template_path = template_dir / "admin_user" / template_file + output_path = project_path / output_file + if template_path.exists(): + render_template(template_path, output_path) + + # Render admin-panel templates + if "admin-panel" in features: + admin_panel_templates = { + "admin.py.j2": "app/admin.py", + } + for template_file, output_file in admin_panel_templates.items(): + template_path = template_dir / "admin_panel" / template_file + output_path = project_path / output_file + if template_path.exists(): + render_template(template_path, output_path) + + # Render API templates + if "api" in features: + api_templates = { + "api.py.j2": "app/api.py", + } + for template_file, output_file in api_templates.items(): + template_path = template_dir / "api" / template_file + output_path = project_path / output_file + if template_path.exists(): + render_template(template_path, output_path) + + # Render HTML templates + html_templates = [ + "base.html", + "index.html", + ] + + if "admin-user" in features: + html_templates.extend([ + "login.html", + "register.html", + ]) + + if "admin-panel" in features: + html_templates.extend([ + "admin/dashboard.html", + "admin/users.html", + "admin/items.html", + ]) + + if "sqlite" in features and "admin-user" in features: + html_templates.extend([ + "items.html", + "create_item.html", + "edit_item.html", + ]) + + for template_file in html_templates: + template_path = template_dir / "templates" / template_file + output_path = project_path / "templates" / template_file + if template_path.exists(): + output_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(template_path, output_path) + + # Render static files + static_templates = [ + "style.css.j2", + ] + + for template_file in static_templates: + template_path = template_dir / "static" / template_file + output_path = project_path / "static" / template_file.replace(".j2", "") + if template_path.exists(): + render_template(template_path, output_path) + + # Create setup script + setup_template = template_dir / "base" / "setup.sh.j2" + setup_path = project_path / "setup.sh" + if setup_template.exists(): + render_template(setup_template, setup_path) + os.chmod(setup_path, 0o755) + + # Create .gitignore + gitignore_content = """# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Environments +.env +.venv +env/ +venv/ +ENV/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Flask +instance/ +.webassets-cache +""" + with open(project_path / ".gitignore", "w") as f: + f.write(gitignore_content) + + # Create README + readme_content = f"""# {name} + +A Flask application created with the Flask CLI. + +## Features + +""" + for feature in features: + readme_content += f"- {feature.replace('-', ' ').title()}\n" + + readme_content += """ +## Setup + +```bash +# Make the setup script executable +chmod +x setup.sh + +# Run the setup script +./setup.sh +``` + +## Running + +```bash +# Activate virtual environment +source venv/bin/activate + +# Run the application +python run.py +``` + +## API Endpoints + +""" + if "api" in features: + readme_content += """ +- `GET /api/items` - Get all items +- `GET /api/items/` - Get a specific item +- `POST /api/items` - Create a new item (requires authentication) +- `PUT /api/items/` - Update an item (requires authentication) +- `DELETE /api/items/` - Delete an item (requires authentication) +""" + + with open(project_path / "README.md", "w") as f: + f.write(readme_content) + + click.echo(f"\nProject '{name}' created successfully!") + click.echo(f"\nNext steps:") + click.echo(f" cd {project_path}") + click.echo(f" chmod +x setup.sh") + click.echo(f" ./setup.sh") + click.echo(f"\nOr manually:") + click.echo(f" python3 -m venv venv") + click.echo(f" source venv/bin/activate") + click.echo(f" pip install -r requirements.txt") + if "sqlite" in features: + click.echo(f" python -c \"from app import create_app; from extensions import db; app = create_app(); app.app_context().push(); db.create_all()\"") + click.echo(f" python run.py") + + cli = FlaskGroup( name="flask", help="""\ diff --git a/src/flask/cli_templates/admin_panel/admin.py.j2 b/src/flask/cli_templates/admin_panel/admin.py.j2 new file mode 100644 index 0000000000..4f21704640 --- /dev/null +++ b/src/flask/cli_templates/admin_panel/admin.py.j2 @@ -0,0 +1,61 @@ +from flask import Blueprint, render_template, redirect, url_for, request, flash +from flask_login import login_required, current_user +from .models import User, Item +from .extensions import db + +admin_bp = Blueprint('admin', __name__, url_prefix='/admin') + +@admin_bp.before_request +@login_required +def before_request(): + if not current_user.is_admin: + flash('You do not have permission to access this page.', 'danger') + return redirect(url_for('main.index')) + +@admin_bp.route('/') +def dashboard(): + users_count = User.query.count() + items_count = Item.query.count() + return render_template('admin/dashboard.html', + users_count=users_count, + items_count=items_count) + +@admin_bp.route('/users') +def users(): + users = User.query.all() + return render_template('admin/users.html', users=users) + +@admin_bp.route('/users//toggle-admin', methods=['POST']) +def toggle_admin(user_id): + user = User.query.get_or_404(user_id) + if user.id == current_user.id: + flash('You cannot change your own admin status.', 'danger') + else: + user.is_admin = not user.is_admin + db.session.commit() + flash(f'User {user.username} admin status updated.', 'success') + return redirect(url_for('admin.users')) + +@admin_bp.route('/users//delete', methods=['POST']) +def delete_user(user_id): + user = User.query.get_or_404(user_id) + if user.id == current_user.id: + flash('You cannot delete yourself.', 'danger') + else: + db.session.delete(user) + db.session.commit() + flash(f'User {user.username} has been deleted.', 'success') + return redirect(url_for('admin.users')) + +@admin_bp.route('/items') +def items(): + items = Item.query.all() + return render_template('admin/items.html', items=items) + +@admin_bp.route('/items//delete', methods=['POST']) +def delete_item(item_id): + item = Item.query.get_or_404(item_id) + db.session.delete(item) + db.session.commit() + flash(f'Item {item.name} has been deleted.', 'success') + return redirect(url_for('admin.items')) diff --git a/src/flask/cli_templates/admin_user/auth.py.j2 b/src/flask/cli_templates/admin_user/auth.py.j2 new file mode 100644 index 0000000000..9bf668da29 --- /dev/null +++ b/src/flask/cli_templates/admin_user/auth.py.j2 @@ -0,0 +1,69 @@ +from flask import Blueprint, render_template, redirect, url_for, request, flash +from flask_login import login_user, logout_user, login_required, current_user +from .models import User +from .extensions import db + +auth_bp = Blueprint('auth', __name__) + +@auth_bp.route('/login', methods=['GET', 'POST']) +def login(): + if current_user.is_authenticated: + return redirect(url_for('main.index')) + + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + remember = request.form.get('remember', False) + + user = User.query.filter_by(username=username).first() + + if user is None or not user.check_password(password): + flash('Invalid username or password', 'danger') + return redirect(url_for('auth.login')) + + login_user(user, remember=remember) + next_page = request.args.get('next') + flash('Welcome back!', 'success') + return redirect(next_page or url_for('main.index')) + + return render_template('login.html') + +@auth_bp.route('/logout') +@login_required +def logout(): + logout_user() + flash('You have been logged out.', 'info') + return redirect(url_for('main.index')) + +@auth_bp.route('/register', methods=['GET', 'POST']) +def register(): + if current_user.is_authenticated: + return redirect(url_for('main.index')) + + if request.method == 'POST': + username = request.form.get('username') + email = request.form.get('email') + password = request.form.get('password') + confirm_password = request.form.get('confirm_password') + + if password != confirm_password: + flash('Passwords do not match', 'danger') + return redirect(url_for('auth.register')) + + if User.query.filter_by(username=username).first(): + flash('Username already taken', 'danger') + return redirect(url_for('auth.register')) + + if User.query.filter_by(email=email).first(): + flash('Email already registered', 'danger') + return redirect(url_for('auth.register')) + + user = User(username=username, email=email) + user.set_password(password) + db.session.add(user) + db.session.commit() + + flash('Registration successful! Please login.', 'success') + return redirect(url_for('auth.login')) + + return render_template('register.html') diff --git a/src/flask/cli_templates/api/api.py.j2 b/src/flask/cli_templates/api/api.py.j2 new file mode 100644 index 0000000000..550c519bae --- /dev/null +++ b/src/flask/cli_templates/api/api.py.j2 @@ -0,0 +1,67 @@ +from flask import Blueprint, jsonify, request +from .models import Item +from .extensions import db +from flask_login import login_required, current_user + +api_bp = Blueprint('api', __name__) + +@api_bp.route('/items', methods=['GET']) +def get_items(): + items = Item.query.all() + return jsonify([item.to_dict() for item in items]) + +@api_bp.route('/items/', methods=['GET']) +def get_item(item_id): + item = Item.query.get_or_404(item_id) + return jsonify(item.to_dict()) + +@api_bp.route('/items', methods=['POST']) +@login_required +def create_item(): + data = request.get_json() + + if not data or 'name' not in data: + return jsonify({'error': 'Name is required'}), 400 + + item = Item( + name=data['name'], + description=data.get('description', ''), + user_id=current_user.id + ) + + db.session.add(item) + db.session.commit() + + return jsonify(item.to_dict()), 201 + +@api_bp.route('/items/', methods=['PUT']) +@login_required +def update_item(item_id): + item = Item.query.get_or_404(item_id) + + if item.user_id != current_user.id and not current_user.is_admin: + return jsonify({'error': 'Unauthorized'}), 403 + + data = request.get_json() + + if 'name' in data: + item.name = data['name'] + if 'description' in data: + item.description = data['description'] + + db.session.commit() + + return jsonify(item.to_dict()) + +@api_bp.route('/items/', methods=['DELETE']) +@login_required +def delete_item(item_id): + item = Item.query.get_or_404(item_id) + + if item.user_id != current_user.id and not current_user.is_admin: + return jsonify({'error': 'Unauthorized'}), 403 + + db.session.delete(item) + db.session.commit() + + return '', 204 diff --git a/src/flask/cli_templates/base/.env.j2 b/src/flask/cli_templates/base/.env.j2 new file mode 100644 index 0000000000..f768229067 --- /dev/null +++ b/src/flask/cli_templates/base/.env.j2 @@ -0,0 +1,5 @@ +# {{ project_name }} Environment Variables +SECRET_KEY=your-secret-key-here +FLASK_APP=run.py +FLASK_ENV=development +DATABASE_URL=sqlite:///app.db diff --git a/src/flask/cli_templates/base/app.py.j2 b/src/flask/cli_templates/base/app.py.j2 new file mode 100644 index 0000000000..7e68c8f986 --- /dev/null +++ b/src/flask/cli_templates/base/app.py.j2 @@ -0,0 +1,55 @@ +from flask import Flask +from config import Config +{% if 'sqlite' in features %} +from .extensions import db, migrate +{% endif %} +{% if 'admin-user' in features %} +from flask_login import LoginManager +{% endif %} + +def create_app(config_class=Config): + app = Flask(__name__) + app.config.from_object(config_class) + +{% if 'sqlite' in features %} + db.init_app(app) + migrate.init_app(app, db) +{% endif %} + +{% if 'admin-user' in features %} + login_manager = LoginManager() + login_manager.init_app(app) + login_manager.login_view = 'auth.login' + login_manager.login_message_category = 'info' + + @login_manager.user_loader + def load_user(user_id): + from .models import User + return User.query.get(int(user_id)) +{% endif %} + +{% if 'admin-panel' in features %} + from .admin import admin_bp + app.register_blueprint(admin_bp) +{% endif %} + +{% if 'api' in features %} + from .api import api_bp + app.register_blueprint(api_bp, url_prefix='/api') +{% endif %} + + from .routes import main_bp + app.register_blueprint(main_bp) + +{% if 'admin-user' in features %} + from .auth import auth_bp + app.register_blueprint(auth_bp) +{% endif %} + + with app.app_context(): +{% if 'sqlite' in features %} + from .models import Item + db.create_all() +{% endif %} + + return app diff --git a/src/flask/cli_templates/base/config.py.j2 b/src/flask/cli_templates/base/config.py.j2 new file mode 100644 index 0000000000..03d06c41ee --- /dev/null +++ b/src/flask/cli_templates/base/config.py.j2 @@ -0,0 +1,11 @@ +import os + +basedir = os.path.abspath(os.path.dirname(__file__)) + +class Config: + SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess' +{% if 'sqlite' in features %} + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ + 'sqlite:///' + os.path.join(basedir, 'app.db') + SQLALCHEMY_TRACK_MODIFICATIONS = False +{% endif %} diff --git a/src/flask/cli_templates/base/requirements.txt.j2 b/src/flask/cli_templates/base/requirements.txt.j2 new file mode 100644 index 0000000000..db0ecfc5e8 --- /dev/null +++ b/src/flask/cli_templates/base/requirements.txt.j2 @@ -0,0 +1,12 @@ +flask>=3.0.0 +{% if 'sqlite' in features %} +flask-sqlalchemy>=3.1.0 +flask-migrate>=4.0.0 +{% endif %} +{% if 'admin-user' in features %} +flask-login>=0.6.0 +werkzeug>=3.0.0 +{% endif %} +{% if 'api' in features %} +flask-restful>=0.3.10 +{% endif %} diff --git a/src/flask/cli_templates/base/routes.py.j2 b/src/flask/cli_templates/base/routes.py.j2 new file mode 100644 index 0000000000..f59c7781f1 --- /dev/null +++ b/src/flask/cli_templates/base/routes.py.j2 @@ -0,0 +1,81 @@ +from flask import Blueprint, render_template, redirect, url_for, request, flash +{% if 'admin-user' in features %} +from flask_login import login_required, current_user +{% endif %} +{% if 'sqlite' in features %} +from .models import Item +from .extensions import db +{% endif %} + +main_bp = Blueprint('main', __name__) + +@main_bp.route('/') +def index(): +{% if 'sqlite' in features %} + items = Item.query.order_by(Item.created_at.desc()).limit(10).all() + return render_template('index.html', items=items) +{% else %} + return render_template('index.html') +{% endif %} + +{% if 'sqlite' in features and 'admin-user' in features %} +@main_bp.route('/items') +@login_required +def items(): + items = Item.query.filter_by(user_id=current_user.id).order_by(Item.created_at.desc()).all() + return render_template('items.html', items=items) + +@main_bp.route('/items/create', methods=['GET', 'POST']) +@login_required +def create_item(): + if request.method == 'POST': + name = request.form.get('name') + description = request.form.get('description') + + if not name: + flash('Name is required', 'danger') + return redirect(url_for('main.create_item')) + + item = Item(name=name, description=description, user_id=current_user.id) + db.session.add(item) + db.session.commit() + + flash('Item created successfully!', 'success') + return redirect(url_for('main.items')) + + return render_template('create_item.html') + +@main_bp.route('/items//edit', methods=['GET', 'POST']) +@login_required +def edit_item(item_id): + item = Item.query.get_or_404(item_id) + + if item.user_id != current_user.id: + flash('You do not have permission to edit this item.', 'danger') + return redirect(url_for('main.items')) + + if request.method == 'POST': + item.name = request.form.get('name') + item.description = request.form.get('description') + db.session.commit() + + flash('Item updated successfully!', 'success') + return redirect(url_for('main.items')) + + return render_template('edit_item.html', item=item) + +@main_bp.route('/items//delete', methods=['POST']) +@login_required +def delete_item(item_id): + item = Item.query.get_or_404(item_id) + + if item.user_id != current_user.id: + flash('You do not have permission to delete this item.', 'danger') + return redirect(url_for('main.items')) + + db.session.delete(item) + db.session.commit() + + flash('Item deleted successfully!', 'success') + return redirect(url_for('main.items')) +{% endif %} diff --git a/src/flask/cli_templates/base/run.py.j2 b/src/flask/cli_templates/base/run.py.j2 new file mode 100644 index 0000000000..a3fdaf3ca2 --- /dev/null +++ b/src/flask/cli_templates/base/run.py.j2 @@ -0,0 +1,6 @@ +from app import create_app + +app = create_app() + +if __name__ == '__main__': + app.run(debug=True) diff --git a/src/flask/cli_templates/base/setup.sh.j2 b/src/flask/cli_templates/base/setup.sh.j2 new file mode 100644 index 0000000000..56badc1ff1 --- /dev/null +++ b/src/flask/cli_templates/base/setup.sh.j2 @@ -0,0 +1,71 @@ +#!/bin/bash +# {{ project_name }} Setup Script + +set -e + +echo "Setting up {{ project_name }}..." + +# Check if Python is installed +if ! command -v python3 &> /dev/null; then + echo "Error: Python3 is required but not installed." + exit 1 +fi + +# Create virtual environment +echo "Creating virtual environment..." +python3 -m venv venv + +# Activate virtual environment +echo "Activating virtual environment..." +source venv/bin/activate + +# Install dependencies +echo "Installing dependencies..." +pip install -r requirements.txt + +# Initialize database +{% if 'sqlite' in features %} +echo "Initializing database..." +python3 -c " +from app import create_app +from extensions import db +app = create_app() +with app.app_context(): + db.create_all() + print('Database initialized successfully!') +" +{% endif %} + +# Create admin user +{% if 'admin-user' in features %} +echo "Creating admin user..." +python3 -c " +from app import create_app +from extensions import db +from models import User +app = create_app() +with app.app_context(): + admin = User.query.filter_by(username='admin').first() + if not admin: + admin = User(username='admin', email='admin@example.com', is_admin=True) + admin.set_password('admin123') + db.session.add(admin) + db.session.commit() + print('Admin user created successfully!') + print('Username: admin') + print('Password: admin123') + else: + print('Admin user already exists.') +" +{% endif %} + +echo "" +echo "Setup complete!" +echo "" +echo "To run the application:" +echo " source venv/bin/activate" +echo " python run.py" +echo "" +echo "Or use Flask:" +echo " export FLASK_APP=run.py" +echo " flask run" diff --git a/src/flask/cli_templates/sqlite/extensions.py.j2 b/src/flask/cli_templates/sqlite/extensions.py.j2 new file mode 100644 index 0000000000..378f0df344 --- /dev/null +++ b/src/flask/cli_templates/sqlite/extensions.py.j2 @@ -0,0 +1,5 @@ +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate + +db = SQLAlchemy() +migrate = Migrate() diff --git a/src/flask/cli_templates/sqlite/models.py.j2 b/src/flask/cli_templates/sqlite/models.py.j2 new file mode 100644 index 0000000000..877fe0b50f --- /dev/null +++ b/src/flask/cli_templates/sqlite/models.py.j2 @@ -0,0 +1,51 @@ +from .extensions import db +{% if 'admin-user' in features %} +from flask_login import UserMixin +from werkzeug.security import generate_password_hash, check_password_hash +{% endif %} + +{% if 'admin-user' in features %} +class User(UserMixin, db.Model): + __tablename__ = 'users' + + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False) + email = db.Column(db.String(120), unique=True, nullable=False) + password_hash = db.Column(db.String(128)) + is_admin = db.Column(db.Boolean, default=False) + created_at = db.Column(db.DateTime, server_default=db.func.now()) + + def set_password(self, password): + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + return check_password_hash(self.password_hash, password) + + def __repr__(self): + return f'' +{% endif %} + +class Item(db.Model): + __tablename__ = 'items' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + description = db.Column(db.Text) + created_at = db.Column(db.DateTime, server_default=db.func.now()) + updated_at = db.Column(db.DateTime, server_default=db.func.now(), onupdate=db.func.now()) +{% if 'admin-user' in features %} + user_id = db.Column(db.Integer, db.ForeignKey('users.id')) + user = db.relationship('User', backref=db.backref('items', lazy='dynamic')) +{% endif %} + + def to_dict(self): + return { + 'id': self.id, + 'name': self.name, + 'description': self.description, + 'created_at': self.created_at.isoformat() if self.created_at else None, + 'updated_at': self.updated_at.isoformat() if self.updated_at else None + } + + def __repr__(self): + return f'' diff --git a/src/flask/cli_templates/static/style.css.j2 b/src/flask/cli_templates/static/style.css.j2 new file mode 100644 index 0000000000..ef66016e13 --- /dev/null +++ b/src/flask/cli_templates/static/style.css.j2 @@ -0,0 +1,25 @@ +/* {{ project_name }} - Custom Styles */ + +body { + padding-top: 56px; +} + +.navbar-brand { + font-weight: bold; +} + +.card { + margin-bottom: 20px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.list-group-item { + margin-bottom: 10px; +} + +.footer { + margin-top: 50px; + padding: 20px 0; + background-color: #f5f5f5; + text-align: center; +} diff --git a/src/flask/cli_templates/templates/admin/dashboard.html b/src/flask/cli_templates/templates/admin/dashboard.html new file mode 100644 index 0000000000..77846b7971 --- /dev/null +++ b/src/flask/cli_templates/templates/admin/dashboard.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Admin Dashboard

+ +
+
+
+
+
Users
+

{{ users_count }}

+ View all users +
+
+
+
+
+
+
Items
+

{{ items_count }}

+ View all items +
+
+
+
+ +
+ +

Quick Actions

+ +
+
+{% endblock %} diff --git a/src/flask/cli_templates/templates/admin/items.html b/src/flask/cli_templates/templates/admin/items.html new file mode 100644 index 0000000000..cfe98e63e5 --- /dev/null +++ b/src/flask/cli_templates/templates/admin/items.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Manage Items

+ + + + + + + + + + + + + + {% for item in items %} + + + + + + + + + {% endfor %} + +
IDNameDescriptionOwnerCreatedActions
{{ item.id }}{{ item.name }}{{ item.description[:50] }}{% if item.description|length > 50 %}...{% endif %}{{ item.user.username if item.user else 'N/A' }}{{ item.created_at.strftime('%Y-%m-%d') if item.created_at else 'N/A' }} +
+ +
+
+ + Back to Dashboard +
+
+{% endblock %} diff --git a/src/flask/cli_templates/templates/admin/users.html b/src/flask/cli_templates/templates/admin/users.html new file mode 100644 index 0000000000..684a90fd06 --- /dev/null +++ b/src/flask/cli_templates/templates/admin/users.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Manage Users

+ + + + + + + + + + + + + + {% for user in users %} + + + + + + + + + {% endfor %} + +
IDUsernameEmailAdminCreatedActions
{{ user.id }}{{ user.username }}{{ user.email }} + {% if user.is_admin %} + Admin + {% else %} + User + {% endif %} + {{ user.created_at.strftime('%Y-%m-%d') if user.created_at else 'N/A' }} +
+ +
+
+ +
+
+ + Back to Dashboard +
+
+{% endblock %} diff --git a/src/flask/cli_templates/templates/base.html b/src/flask/cli_templates/templates/base.html new file mode 100644 index 0000000000..3bc7e69a04 --- /dev/null +++ b/src/flask/cli_templates/templates/base.html @@ -0,0 +1,81 @@ + + + + + + {{ project_name }} +{% if 'bootstrap' in features %} + +{% endif %} + + + +{% if 'bootstrap' in features %} + +{% endif %} + +
+{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} +{% endwith %} + + {% block content %}{% endblock %} +
+ +{% if 'bootstrap' in features %} + +{% endif %} + + diff --git a/src/flask/cli_templates/templates/create_item.html b/src/flask/cli_templates/templates/create_item.html new file mode 100644 index 0000000000..29d2dfd489 --- /dev/null +++ b/src/flask/cli_templates/templates/create_item.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Create New Item

+ +
+
+ + +
+
+ + +
+ + Cancel +
+
+
+{% endblock %} diff --git a/src/flask/cli_templates/templates/edit_item.html b/src/flask/cli_templates/templates/edit_item.html new file mode 100644 index 0000000000..44124eab35 --- /dev/null +++ b/src/flask/cli_templates/templates/edit_item.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Edit Item

+ +
+
+ + +
+
+ + +
+ + Cancel +
+
+
+{% endblock %} diff --git a/src/flask/cli_templates/templates/index.html b/src/flask/cli_templates/templates/index.html new file mode 100644 index 0000000000..dd7e66d6dd --- /dev/null +++ b/src/flask/cli_templates/templates/index.html @@ -0,0 +1,53 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Welcome to {{ project_name }}

+

A Flask application created with the Flask CLI.

+ +{% if 'sqlite' in features %} +

Recent Items

+ {% if items %} +
+ {% for item in items %} +
+
{{ item.name }}
+

{{ item.description }}

+ Created: {{ item.created_at.strftime('%Y-%m-%d %H:%M') if item.created_at else 'N/A' }} +
+ {% endfor %} +
+ {% else %} +

No items yet. {% if current_user.is_authenticated %}Create one!{% endif %}

+ {% endif %} +{% endif %} +
+ +
+
+
+
Quick Start
+

This project was created with the Flask CLI create command.

+
    +{% if 'sqlite' in features %} +
  • SQLite Database
  • +{% endif %} +{% if 'admin-user' in features %} +
  • User Authentication
  • +{% endif %} +{% if 'admin-panel' in features %} +
  • Admin Panel
  • +{% endif %} +{% if 'api' in features %} +
  • REST API
  • +{% endif %} +{% if 'bootstrap' in features %} +
  • Bootstrap UI
  • +{% endif %} +
+
+
+
+
+{% endblock %} diff --git a/src/flask/cli_templates/templates/items.html b/src/flask/cli_templates/templates/items.html new file mode 100644 index 0000000000..3a13bd8272 --- /dev/null +++ b/src/flask/cli_templates/templates/items.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

My Items

+ + Create New Item + + {% if items %} +
+ {% for item in items %} +
+
+
+
{{ item.name }}
+

{{ item.description }}

+ Created: {{ item.created_at.strftime('%Y-%m-%d %H:%M') if item.created_at else 'N/A' }} +
+
+ Edit +
+ +
+
+
+
+ {% endfor %} +
+ {% else %} +

You don't have any items yet. Create your first item!

+ {% endif %} +
+
+{% endblock %} diff --git a/src/flask/cli_templates/templates/login.html b/src/flask/cli_templates/templates/login.html new file mode 100644 index 0000000000..c126d0c3bc --- /dev/null +++ b/src/flask/cli_templates/templates/login.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+

Login

+
+
+ + +
+
+ + +
+
+ + +
+ +
+

+ Don't have an account? Register here +

+
+
+
+
+{% endblock %} diff --git a/src/flask/cli_templates/templates/register.html b/src/flask/cli_templates/templates/register.html new file mode 100644 index 0000000000..1d51be8f10 --- /dev/null +++ b/src/flask/cli_templates/templates/register.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
+

Register

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+

+ Already have an account? Login here +

+
+
+
+
+{% endblock %} diff --git a/todo.md b/todo.md new file mode 100644 index 0000000000..f85ea62f37 --- /dev/null +++ b/todo.md @@ -0,0 +1,6 @@ +# Essentiels + + +1. modify the cli 'flask' to generate a full project structure with customised configs +- examples : + flask create --include admin-user,sqlite,crud