Overview
The PortfolioItem model represents images and media in the tattoo studio’s portfolio, linking photos to artists, clients, sessions, and transactions with metadata for categorization and public display.
Model Definition
from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean, ForeignKey
from sqlalchemy.orm import relationship
class PortfolioItem(Base):
__tablename__ = "portfolio_items"
Fields
Primary Key
Unique identifier for the portfolio item
Relationships (All Nullable)
Foreign key reference to the artistForeign Key: artists.idNullable: True (portfolio item may not be tied to specific artist)
Foreign key reference to the user who uploaded the itemForeign Key: users.idNullable: True
Foreign key reference to the client shown in the imageForeign Key: clients.idNullable: True
Foreign key reference to the tattoo sessionForeign Key: sessions.idNullable: True
Foreign key reference to the payment transactionForeign Key: transactions.idNullable: True
File Paths
File path to the full-size imageUsage: Relative or absolute path to stored image
File path to the thumbnail imageNullable: TrueUsage: Optimized smaller version for galleries
Description
Image caption or descriptionNullable: TrueUsage: Description, story, or context about the tattoo
Categorization Metadata
Tattoo styleNullable: TrueExamples: “Traditional”, “Realism”, “Japanese”, “Blackwork”, “Watercolor”
Body placement areaNullable: TrueExamples: “arm”, “back”, “leg”, “chest”, “sleeve”
Color mode of the tattooNullable: TrueValid Values: “color”, “bn” (black and white)
Healing status when photo was takenNullable: TrueValid Values: “fresh”, “healed”
Display Settings
Whether the image is visible to publicUsage: Control visibility on public portfolio/website
Whether this image should be featured as a cover imageUsage: Flag for highlighting in galleries or artist profiles
Metadata
created_at
DateTime
default:"datetime.utcnow"
Timestamp when the portfolio item was created
Relationship Objects
The Artist object this portfolio item belongs toLazy Loading: joined (eager loading)
The User object who uploaded this itemLazy Loading: joined (eager loading)
The Client object shown in this imageLazy Loading: joined (eager loading)
The TattooSession object this image is fromLazy Loading: joined (eager loading)
The Transaction object associated with this workLazy Loading: joined (eager loading)
Database Schema
CREATE TABLE portfolio_items (
id INTEGER PRIMARY KEY,
-- Relationships (all nullable)
artist_id INTEGER,
user_id INTEGER,
client_id INTEGER,
session_id INTEGER,
transaction_id INTEGER,
-- File paths
path VARCHAR NOT NULL,
thumb_path VARCHAR,
-- Description
caption TEXT,
-- Categorization
style VARCHAR,
body_area VARCHAR,
color_mode VARCHAR,
fresh_or_healed VARCHAR,
-- Display settings
is_public BOOLEAN NOT NULL DEFAULT 1,
is_cover BOOLEAN NOT NULL DEFAULT 0,
-- Metadata
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (artist_id) REFERENCES artists(id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (client_id) REFERENCES clients(id),
FOREIGN KEY (session_id) REFERENCES sessions(id),
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
);
Usage Examples
Create Portfolio Item from Session
from data.models.portfolio import PortfolioItem
from datetime import datetime
# Add photo after completing a session
portfolio_item = PortfolioItem(
artist_id=1,
user_id=2,
client_id=5,
session_id=10,
path="/uploads/portfolio/2024/03/tattoo_001.jpg",
thumb_path="/uploads/portfolio/2024/03/thumbs/tattoo_001.jpg",
caption="Japanese dragon sleeve - first session",
style="Japanese",
body_area="sleeve",
color_mode="color",
fresh_or_healed="fresh",
is_public=True,
is_cover=False
)
db.add(portfolio_item)
db.commit()
Create Standalone Portfolio Item
# Add a general portfolio image not tied to a specific session
portfolio_item = PortfolioItem(
artist_id=1,
path="/uploads/portfolio/general/flash_design_01.jpg",
thumb_path="/uploads/portfolio/general/thumbs/flash_design_01.jpg",
caption="Available flash design",
style="Traditional",
color_mode="color",
is_public=True
)
db.add(portfolio_item)
db.commit()
Get Public Portfolio for Artist
# Get all public portfolio items for an artist
artist_id = 1
portfolio = db.query(PortfolioItem).filter(
PortfolioItem.artist_id == artist_id,
PortfolioItem.is_public == True
).order_by(PortfolioItem.created_at.desc()).all()
for item in portfolio:
print(f"{item.style or 'Uncategorized'} - {item.caption or 'No caption'}")
Get Cover Images
# Get featured/cover images
cover_images = db.query(PortfolioItem).filter(
PortfolioItem.is_cover == True,
PortfolioItem.is_public == True
).all()
Filter by Style
# Get all Japanese style tattoos
japanese_work = db.query(PortfolioItem).filter(
PortfolioItem.style == "Japanese",
PortfolioItem.is_public == True
).all()
for item in japanese_work:
if item.artist:
print(f"By {item.artist.name}: {item.caption}")
Filter by Body Area
# Get all sleeve tattoos
sleeves = db.query(PortfolioItem).filter(
PortfolioItem.body_area == "sleeve",
PortfolioItem.is_public == True
).order_by(PortfolioItem.created_at.desc()).all()
Get Client’s Tattoo History
# Get all portfolio images for a specific client
client_id = 5
client_tattoos = db.query(PortfolioItem).filter(
PortfolioItem.client_id == client_id
).order_by(PortfolioItem.created_at).all()
print(f"Client's tattoo history:")
for item in client_tattoos:
print(f" {item.created_at.strftime('%Y-%m-%d')} - {item.caption}")
if item.session:
print(f" Session with {item.session.artist.name}")
Toggle Privacy
# Make an image private
item = db.query(PortfolioItem).filter(PortfolioItem.id == 1).first()
item.is_public = False
db.commit()
# Make it public again
item.is_public = True
db.commit()
Set Cover Image
# Set a new cover image (unset previous covers for the artist)
artist_id = 1
new_cover_id = 10
# Remove existing cover flags for this artist
db.query(PortfolioItem).filter(
PortfolioItem.artist_id == artist_id,
PortfolioItem.is_cover == True
).update({"is_cover": False})
# Set new cover
item = db.query(PortfolioItem).filter(PortfolioItem.id == new_cover_id).first()
item.is_cover = True
db.commit()
Search Portfolio
# Search by caption
search_term = "dragon"
results = db.query(PortfolioItem).filter(
PortfolioItem.caption.ilike(f"%{search_term}%"),
PortfolioItem.is_public == True
).all()
Filtering and Gallery Organization
Multi-Filter Gallery
def get_filtered_portfolio(artist_id=None, style=None, body_area=None,
color_mode=None, fresh_or_healed=None):
query = db.query(PortfolioItem).filter(
PortfolioItem.is_public == True
)
if artist_id:
query = query.filter(PortfolioItem.artist_id == artist_id)
if style:
query = query.filter(PortfolioItem.style == style)
if body_area:
query = query.filter(PortfolioItem.body_area == body_area)
if color_mode:
query = query.filter(PortfolioItem.color_mode == color_mode)
if fresh_or_healed:
query = query.filter(PortfolioItem.fresh_or_healed == fresh_or_healed)
return query.order_by(PortfolioItem.created_at.desc()).all()
Common Categorization Values
Styles
- Traditional
- Japanese
- Realism
- Blackwork
- Watercolor
- Geometric
- Tribal
- Neo-Traditional
- Minimalist
Body Areas
- arm
- leg
- back
- chest
- sleeve
- half-sleeve
- hand
- neck
- foot
Color Modes
- color - Color tattoo
- bn - Black and white (black and grey)
Healing Status
- fresh - Freshly done (same day)
- healed - Fully healed (weeks/months later)
Image Management Best Practices
- Always generate thumbnails for better performance in galleries
- Use relative paths for portability
- Organize by date (e.g.,
/uploads/portfolio/YYYY/MM/)
- Store metadata (style, body_area) for better filtering
- Respect privacy - check
is_public flag before displaying
- Link to sessions when possible for complete record-keeping
- Use cover images to feature best work
Notes
- All relationship fields are nullable for maximum flexibility
- The
artist relationship has cascade delete from Artist model
- Images can exist without being tied to any session (flash, designs)
- Use
is_public to control portfolio visibility without deleting
- The
created_at timestamp helps with chronological sorting
thumb_path is essential for performance in grid galleries
- Lazy loading is set to “joined” for eager loading of relationships
- Location in codebase:
data/models/portfolio.py:7