initial commit
This commit is contained in:
480
docs/user-guide/admin-panel/adding-models.md
Normal file
480
docs/user-guide/admin-panel/adding-models.md
Normal file
@ -0,0 +1,480 @@
|
||||
# Adding Models
|
||||
|
||||
Learn how to extend the admin interface with your new models by following the patterns established in the FastAPI boilerplate. The boilerplate already includes User, Tier, and Post models - we'll show you how to add your own models using these working examples.
|
||||
|
||||
> **CRUDAdmin Features**: This guide shows boilerplate-specific patterns. For advanced model configuration options and features, see the [CRUDAdmin documentation](https://benavlabs.github.io/crudadmin/).
|
||||
|
||||
## Understanding the Existing Setup
|
||||
|
||||
The boilerplate comes with three models already registered in the admin interface. Understanding how they're implemented will help you add your own models successfully.
|
||||
|
||||
### Current Model Registration
|
||||
|
||||
The admin interface is configured in `src/app/admin/views.py`:
|
||||
|
||||
```python
|
||||
def register_admin_views(admin: CRUDAdmin) -> None:
|
||||
"""Register all models and their schemas with the admin interface."""
|
||||
|
||||
# User model with password handling
|
||||
password_transformer = PasswordTransformer(
|
||||
password_field="password",
|
||||
hashed_field="hashed_password",
|
||||
hash_function=get_password_hash,
|
||||
required_fields=["name", "username", "email"],
|
||||
)
|
||||
|
||||
admin.add_view(
|
||||
model=User,
|
||||
create_schema=UserCreate,
|
||||
update_schema=UserUpdate,
|
||||
allowed_actions={"view", "create", "update"},
|
||||
password_transformer=password_transformer,
|
||||
)
|
||||
|
||||
admin.add_view(
|
||||
model=Tier,
|
||||
create_schema=TierCreate,
|
||||
update_schema=TierUpdate,
|
||||
allowed_actions={"view", "create", "update", "delete"}
|
||||
)
|
||||
|
||||
admin.add_view(
|
||||
model=Post,
|
||||
create_schema=PostCreateAdmin, # Special admin-only schema
|
||||
update_schema=PostUpdate,
|
||||
allowed_actions={"view", "create", "update", "delete"}
|
||||
)
|
||||
```
|
||||
|
||||
Each model registration follows the same pattern: specify the SQLAlchemy model, appropriate Pydantic schemas for create/update operations, and define which actions are allowed.
|
||||
|
||||
## Step-by-Step Model Addition
|
||||
|
||||
Let's walk through adding a new model to your admin interface using a product catalog example.
|
||||
|
||||
### Step 1: Create Your Model
|
||||
|
||||
First, create your SQLAlchemy model following the boilerplate's patterns:
|
||||
|
||||
```python
|
||||
# src/app/models/product.py
|
||||
from decimal import Decimal
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy import String, Numeric, ForeignKey, Text, Boolean
|
||||
from sqlalchemy.types import DateTime
|
||||
from datetime import datetime
|
||||
|
||||
from ..core.db.database import Base
|
||||
|
||||
class Product(Base):
|
||||
__tablename__ = "products"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
price: Mapped[Decimal] = mapped_column(Numeric(10, 2), nullable=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# Foreign key relationship (similar to Post.created_by_user_id)
|
||||
category_id: Mapped[int] = mapped_column(ForeignKey("categories.id"))
|
||||
```
|
||||
|
||||
### Step 2: Create Pydantic Schemas
|
||||
|
||||
Create schemas for the admin interface following the boilerplate's pattern:
|
||||
|
||||
```python
|
||||
# src/app/schemas/product.py
|
||||
from decimal import Decimal
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Annotated
|
||||
|
||||
class ProductCreate(BaseModel):
|
||||
name: Annotated[str, Field(min_length=2, max_length=100)]
|
||||
description: Annotated[str | None, Field(max_length=1000, default=None)]
|
||||
price: Annotated[Decimal, Field(gt=0, le=999999.99)]
|
||||
is_active: Annotated[bool, Field(default=True)]
|
||||
category_id: Annotated[int, Field(gt=0)]
|
||||
|
||||
class ProductUpdate(BaseModel):
|
||||
name: Annotated[str | None, Field(min_length=2, max_length=100, default=None)]
|
||||
description: Annotated[str | None, Field(max_length=1000, default=None)]
|
||||
price: Annotated[Decimal | None, Field(gt=0, le=999999.99, default=None)]
|
||||
is_active: Annotated[bool | None, Field(default=None)]
|
||||
category_id: Annotated[int | None, Field(gt=0, default=None)]
|
||||
```
|
||||
|
||||
### Step 3: Register with Admin Interface
|
||||
|
||||
Add your model to `src/app/admin/views.py`:
|
||||
|
||||
```python
|
||||
# Add import at the top
|
||||
from ..models.product import Product
|
||||
from ..schemas.product import ProductCreate, ProductUpdate
|
||||
|
||||
def register_admin_views(admin: CRUDAdmin) -> None:
|
||||
"""Register all models and their schemas with the admin interface."""
|
||||
|
||||
# ... existing model registrations ...
|
||||
|
||||
# Add your new model
|
||||
admin.add_view(
|
||||
model=Product,
|
||||
create_schema=ProductCreate,
|
||||
update_schema=ProductUpdate,
|
||||
allowed_actions={"view", "create", "update", "delete"}
|
||||
)
|
||||
```
|
||||
|
||||
### Step 4: Create and Run Migration
|
||||
|
||||
Generate the database migration for your new model:
|
||||
|
||||
```bash
|
||||
# Generate migration
|
||||
uv run alembic revision --autogenerate -m "Add product model"
|
||||
|
||||
# Apply migration
|
||||
uv run alembic upgrade head
|
||||
```
|
||||
|
||||
### Step 5: Test Your New Model
|
||||
|
||||
Start your application and test the new model in the admin interface:
|
||||
|
||||
```bash
|
||||
# Start the application
|
||||
uv run fastapi dev
|
||||
|
||||
# Visit http://localhost:8000/admin
|
||||
# Login with your admin credentials
|
||||
# You should see "Products" in the admin navigation
|
||||
```
|
||||
|
||||
## Learning from Existing Models
|
||||
|
||||
Each model in the boilerplate demonstrates different admin interface patterns you can follow.
|
||||
|
||||
### User Model - Password Handling
|
||||
|
||||
The User model shows how to handle sensitive fields like passwords:
|
||||
|
||||
```python
|
||||
# Password transformer for secure password handling
|
||||
password_transformer = PasswordTransformer(
|
||||
password_field="password", # Field in the schema
|
||||
hashed_field="hashed_password", # Field in the database model
|
||||
hash_function=get_password_hash, # Your app's hash function
|
||||
required_fields=["name", "username", "email"], # Fields required for user creation
|
||||
)
|
||||
|
||||
admin.add_view(
|
||||
model=User,
|
||||
create_schema=UserCreate,
|
||||
update_schema=UserUpdate,
|
||||
allowed_actions={"view", "create", "update"}, # No delete for users
|
||||
password_transformer=password_transformer,
|
||||
)
|
||||
```
|
||||
|
||||
**When to use this pattern:**
|
||||
|
||||
- Models with password fields
|
||||
- Any field that needs transformation before storage
|
||||
- Fields requiring special security handling
|
||||
|
||||
### Tier Model - Simple CRUD
|
||||
|
||||
The Tier model demonstrates straightforward CRUD operations:
|
||||
|
||||
```python
|
||||
admin.add_view(
|
||||
model=Tier,
|
||||
create_schema=TierCreate,
|
||||
update_schema=TierUpdate,
|
||||
allowed_actions={"view", "create", "update", "delete"} # Full CRUD
|
||||
)
|
||||
```
|
||||
|
||||
**When to use this pattern:**
|
||||
|
||||
- Reference data (categories, types, statuses)
|
||||
- Configuration models
|
||||
- Simple data without complex relationships
|
||||
|
||||
### Post Model - Admin-Specific Schemas
|
||||
|
||||
The Post model shows how to create admin-specific schemas when the regular API schemas don't work for admin purposes:
|
||||
|
||||
```python
|
||||
# Special admin schema (different from regular PostCreate)
|
||||
class PostCreateAdmin(BaseModel):
|
||||
title: Annotated[str, Field(min_length=2, max_length=30)]
|
||||
text: Annotated[str, Field(min_length=1, max_length=63206)]
|
||||
created_by_user_id: int # Required in admin, but not in API
|
||||
media_url: Annotated[str | None, Field(pattern=r"^(https?|ftp)://[^\s/$.?#].[^\s]*$", default=None)]
|
||||
|
||||
admin.add_view(
|
||||
model=Post,
|
||||
create_schema=PostCreateAdmin, # Admin-specific schema
|
||||
update_schema=PostUpdate, # Regular update schema works fine
|
||||
allowed_actions={"view", "create", "update", "delete"}
|
||||
)
|
||||
```
|
||||
|
||||
**When to use this pattern:**
|
||||
|
||||
- Models where admins need to set fields that users can't
|
||||
- Models requiring additional validation for admin operations
|
||||
- Cases where API schemas are too restrictive or too permissive for admin use
|
||||
|
||||
## Advanced Model Configuration
|
||||
|
||||
### Customizing Field Display
|
||||
|
||||
You can control how fields appear in the admin interface by modifying your schemas:
|
||||
|
||||
```python
|
||||
class ProductCreateAdmin(BaseModel):
|
||||
name: Annotated[str, Field(
|
||||
min_length=2,
|
||||
max_length=100,
|
||||
description="Product name as shown to customers"
|
||||
)]
|
||||
description: Annotated[str | None, Field(
|
||||
max_length=1000,
|
||||
description="Detailed product description (supports HTML)"
|
||||
)]
|
||||
price: Annotated[Decimal, Field(
|
||||
gt=0,
|
||||
le=999999.99,
|
||||
description="Price in USD (up to 2 decimal places)"
|
||||
)]
|
||||
category_id: Annotated[int, Field(
|
||||
gt=0,
|
||||
description="Product category (creates dropdown automatically)"
|
||||
)]
|
||||
```
|
||||
|
||||
### Restricting Actions
|
||||
|
||||
Control what operations are available for each model:
|
||||
|
||||
```python
|
||||
# Read-only model (reports, logs, etc.)
|
||||
admin.add_view(
|
||||
model=AuditLog,
|
||||
create_schema=None, # No creation allowed
|
||||
update_schema=None, # No updates allowed
|
||||
allowed_actions={"view"} # Only viewing
|
||||
)
|
||||
|
||||
# No deletion allowed (users, critical data)
|
||||
admin.add_view(
|
||||
model=User,
|
||||
create_schema=UserCreate,
|
||||
update_schema=UserUpdate,
|
||||
allowed_actions={"view", "create", "update"} # No delete
|
||||
)
|
||||
```
|
||||
|
||||
### Handling Complex Fields
|
||||
|
||||
Some models may have fields that don't work well in the admin interface. Use select schemas to exclude problematic fields:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Create a simplified view schema
|
||||
class ProductAdminView(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
price: Decimal
|
||||
is_active: bool
|
||||
# Exclude complex fields like large text or binary data
|
||||
|
||||
admin.add_view(
|
||||
model=Product,
|
||||
create_schema=ProductCreate,
|
||||
update_schema=ProductUpdate,
|
||||
select_schema=ProductAdminView, # Controls what's shown in lists
|
||||
allowed_actions={"view", "create", "update", "delete"}
|
||||
)
|
||||
```
|
||||
|
||||
## Common Model Patterns
|
||||
|
||||
### Reference Data Models
|
||||
|
||||
For categories, types, and other reference data:
|
||||
|
||||
```python
|
||||
# Simple reference model
|
||||
class Category(Base):
|
||||
__tablename__ = "categories"
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(50), unique=True)
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
# Simple schemas
|
||||
class CategoryCreate(BaseModel):
|
||||
name: str = Field(..., min_length=2, max_length=50)
|
||||
description: str | None = None
|
||||
|
||||
# Registration
|
||||
admin.add_view(
|
||||
model=Category,
|
||||
create_schema=CategoryCreate,
|
||||
update_schema=CategoryCreate, # Same schema for create and update
|
||||
allowed_actions={"view", "create", "update", "delete"}
|
||||
)
|
||||
```
|
||||
|
||||
### User-Generated Content
|
||||
|
||||
For content models with user associations:
|
||||
|
||||
```python
|
||||
class BlogPost(Base):
|
||||
__tablename__ = "blog_posts"
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
title: Mapped[str] = mapped_column(String(200))
|
||||
content: Mapped[str] = mapped_column(Text)
|
||||
author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
|
||||
published_at: Mapped[datetime | None] = mapped_column(DateTime)
|
||||
|
||||
# Admin schema with required author
|
||||
class BlogPostCreateAdmin(BaseModel):
|
||||
title: str = Field(..., min_length=5, max_length=200)
|
||||
content: str = Field(..., min_length=10)
|
||||
author_id: int = Field(..., gt=0) # Admin must specify author
|
||||
published_at: datetime | None = None
|
||||
|
||||
admin.add_view(
|
||||
model=BlogPost,
|
||||
create_schema=BlogPostCreateAdmin,
|
||||
update_schema=BlogPostUpdate,
|
||||
allowed_actions={"view", "create", "update", "delete"}
|
||||
)
|
||||
```
|
||||
|
||||
### Configuration Models
|
||||
|
||||
For application settings and configuration:
|
||||
|
||||
```python
|
||||
class SystemSetting(Base):
|
||||
__tablename__ = "system_settings"
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
key: Mapped[str] = mapped_column(String(100), unique=True)
|
||||
value: Mapped[str] = mapped_column(Text)
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
# Restricted actions - settings shouldn't be deleted
|
||||
admin.add_view(
|
||||
model=SystemSetting,
|
||||
create_schema=SystemSettingCreate,
|
||||
update_schema=SystemSettingUpdate,
|
||||
allowed_actions={"view", "create", "update"} # No delete
|
||||
)
|
||||
```
|
||||
|
||||
## Testing Your Models
|
||||
|
||||
After adding models to the admin interface, test them thoroughly:
|
||||
|
||||
### Manual Testing
|
||||
|
||||
1. **Access**: Navigate to `/admin` and log in
|
||||
2. **Create**: Try creating new records with valid and invalid data
|
||||
3. **Edit**: Test updating existing records
|
||||
4. **Validation**: Verify that your schema validation works correctly
|
||||
5. **Relationships**: Test foreign key relationships (dropdowns should populate)
|
||||
|
||||
### Development Testing
|
||||
|
||||
```python
|
||||
# Test your admin configuration
|
||||
# src/scripts/test_admin.py
|
||||
from app.admin.initialize import create_admin_interface
|
||||
|
||||
def test_admin_setup():
|
||||
admin = create_admin_interface()
|
||||
if admin:
|
||||
print("Admin interface created successfully")
|
||||
print(f"Models registered: {len(admin._views)}")
|
||||
for model_name in admin._views:
|
||||
print(f" - {model_name}")
|
||||
else:
|
||||
print("Admin interface disabled")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_admin_setup()
|
||||
```
|
||||
|
||||
```bash
|
||||
# Run the test
|
||||
uv run python src/scripts/test_admin.py
|
||||
```
|
||||
|
||||
## Updating Model Registration
|
||||
|
||||
When you need to modify how existing models appear in the admin interface:
|
||||
|
||||
### Adding Actions
|
||||
|
||||
```python
|
||||
# Enable deletion for a model that previously didn't allow it
|
||||
admin.add_view(
|
||||
model=Product,
|
||||
create_schema=ProductCreate,
|
||||
update_schema=ProductUpdate,
|
||||
allowed_actions={"view", "create", "update", "delete"} # Added delete
|
||||
)
|
||||
```
|
||||
|
||||
### Changing Schemas
|
||||
|
||||
```python
|
||||
# Switch to admin-specific schemas
|
||||
admin.add_view(
|
||||
model=User,
|
||||
create_schema=UserCreateAdmin, # New admin schema
|
||||
update_schema=UserUpdateAdmin, # New admin schema
|
||||
allowed_actions={"view", "create", "update"},
|
||||
password_transformer=password_transformer,
|
||||
)
|
||||
```
|
||||
|
||||
### Performance Optimization
|
||||
|
||||
For models with many records, consider using select schemas to limit data:
|
||||
|
||||
```python
|
||||
# Only show essential fields in lists
|
||||
class UserListView(BaseModel):
|
||||
id: int
|
||||
username: str
|
||||
email: str
|
||||
is_active: bool
|
||||
|
||||
admin.add_view(
|
||||
model=User,
|
||||
create_schema=UserCreate,
|
||||
update_schema=UserUpdate,
|
||||
select_schema=UserListView, # Faster list loading
|
||||
allowed_actions={"view", "create", "update"},
|
||||
password_transformer=password_transformer,
|
||||
)
|
||||
```
|
||||
|
||||
## What's Next
|
||||
|
||||
With your models successfully added to the admin interface, you're ready to:
|
||||
|
||||
1. **[User Management](user-management.md)** - Learn how to manage admin users and implement security best practices
|
||||
|
||||
Your models are now fully integrated into the admin interface and ready for production use. The admin panel will automatically handle form generation, validation, and database operations based on your model and schema definitions.
|
||||
Reference in New Issue
Block a user