feat: add demand page modules and admin endpoints
Add full backend support for a demand request page, including: - New database models for demand hero, feature cards, form configuration, and product recommendations - Alembic migration for the new tables - Default seed data for demand page components - Public site config endpoint exposing demand data - Admin CRUD operations and configuration for demand modules - Extended lead query filters for source page, keyword, and date range - Updated Pydantic schemas for lead query parameters - Comprehensive test coverage for all new endpoints
This commit is contained in:
56
app/seed.py
56
app/seed.py
@@ -8,13 +8,18 @@ from urllib.parse import quote
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.orm import Session
|
||||
from .auth import hash_password
|
||||
from .content import ALIASES, CAMPAIGNS, CTAS, DESTINATION_HERO, DESTINATION_REGIONS, DESTINATIONS, HERO_SLIDES, HOTEL_GROUPS, THEMES, VEHICLE_OPTIONS
|
||||
from .content import ALIASES, CAMPAIGNS, CTAS, DEMAND_FEATURE_CARDS, DEMAND_FORM, DEMAND_HERO, DEMAND_RECOMMENDATIONS, DESTINATION_HERO, DESTINATION_REGIONS, DESTINATIONS, HERO_SLIDES, HOTEL_GROUPS, THEMES, VEHICLE_OPTIONS
|
||||
from .database import Base, SessionLocal, engine
|
||||
from .models import (
|
||||
AdminUser,
|
||||
Campaign,
|
||||
CampaignProduct,
|
||||
CtaBanner,
|
||||
DemandFeatureCard,
|
||||
DemandForm,
|
||||
DemandHero,
|
||||
DemandRecommendation,
|
||||
DemandRecommendationProduct,
|
||||
Destination,
|
||||
DestinationAlias,
|
||||
DestinationHero,
|
||||
@@ -104,7 +109,7 @@ def load_products() -> list[dict]:
|
||||
|
||||
def reset_guizhou_content(db: Session) -> dict:
|
||||
products = load_products()
|
||||
for model in [SiteVersion, CampaignProduct, RouteSectionProduct, Campaign, RouteSection, ProductImage, Product, VehicleOption, HotelGroup, CtaBanner, ThemeCard, DestinationRegion, DestinationHero, HeroSlide, DestinationAlias, Destination, MediaAsset]:
|
||||
for model in [SiteVersion, DemandRecommendationProduct, DemandRecommendation, DemandForm, DemandFeatureCard, DemandHero, CampaignProduct, RouteSectionProduct, Campaign, RouteSection, ProductImage, Product, VehicleOption, HotelGroup, CtaBanner, ThemeCard, DestinationRegion, DestinationHero, HeroSlide, DestinationAlias, Destination, MediaAsset]:
|
||||
db.execute(delete(model))
|
||||
db.flush()
|
||||
|
||||
@@ -188,6 +193,29 @@ def reset_guizhou_content(db: Session) -> dict:
|
||||
sortOrder=index,
|
||||
)
|
||||
)
|
||||
|
||||
for index, item in enumerate(DEMAND_HERO):
|
||||
db.add(
|
||||
DemandHero(
|
||||
title=item["title"],
|
||||
kicker=item.get("kicker"),
|
||||
description=item.get("description"),
|
||||
steps=item.get("steps", []),
|
||||
sortOrder=index,
|
||||
)
|
||||
)
|
||||
|
||||
for index, item in enumerate(DEMAND_FEATURE_CARDS):
|
||||
db.add(
|
||||
DemandFeatureCard(
|
||||
title=item["title"],
|
||||
description=item.get("description"),
|
||||
sortOrder=index,
|
||||
)
|
||||
)
|
||||
|
||||
db.add(DemandForm(**DEMAND_FORM))
|
||||
|
||||
for product in products:
|
||||
create_media(db, product.get("image"), "product", product["title"])
|
||||
matched_destination = product.get("destinationName") if product.get("destinationName") in destination_map else None
|
||||
@@ -245,6 +273,22 @@ def reset_guizhou_content(db: Session) -> dict:
|
||||
for index, product in enumerate(section_products):
|
||||
db.add(RouteSectionProduct(sectionId=section.id, productId=product.id, sortOrder=index))
|
||||
|
||||
for sort_order, seed in enumerate(DEMAND_RECOMMENDATIONS):
|
||||
recommendation = DemandRecommendation(
|
||||
title=seed["title"],
|
||||
subtitle=seed.get("subtitle"),
|
||||
sortOrder=sort_order,
|
||||
)
|
||||
db.add(recommendation)
|
||||
db.flush()
|
||||
linked_products = db.scalars(
|
||||
select(Product)
|
||||
.where(Product.sourceId >= seed["start"] + 1, Product.sourceId <= seed["end"])
|
||||
.order_by(Product.sourceId.asc())
|
||||
).all()
|
||||
for index, product in enumerate(linked_products):
|
||||
db.add(DemandRecommendationProduct(recommendationId=recommendation.id, productId=product.id, sortOrder=index))
|
||||
|
||||
snapshot = SiteVersion(
|
||||
title="guizhou-content-reset",
|
||||
status="published",
|
||||
@@ -259,6 +303,10 @@ def reset_guizhou_content(db: Session) -> dict:
|
||||
"routeSections": len(ROUTE_SECTION_DEFAULTS),
|
||||
"hotelGroups": len(HOTEL_GROUPS),
|
||||
"vehicleOptions": len(VEHICLE_OPTIONS),
|
||||
"demandHero": len(DEMAND_HERO),
|
||||
"demandFeatureCards": len(DEMAND_FEATURE_CARDS),
|
||||
"demandForm": 1,
|
||||
"demandRecommendations": len(DEMAND_RECOMMENDATIONS),
|
||||
},
|
||||
)
|
||||
db.add(snapshot)
|
||||
@@ -274,6 +322,10 @@ def reset_guizhou_content(db: Session) -> dict:
|
||||
"routeSections": len(ROUTE_SECTION_DEFAULTS),
|
||||
"hotelGroups": len(HOTEL_GROUPS),
|
||||
"vehicleOptions": len(VEHICLE_OPTIONS),
|
||||
"demandHero": len(DEMAND_HERO),
|
||||
"demandFeatureCards": len(DEMAND_FEATURE_CARDS),
|
||||
"demandForm": 1,
|
||||
"demandRecommendations": len(DEMAND_RECOMMENDATIONS),
|
||||
"siteVersionId": snapshot.id,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user