feat: add hotel and vehicle option site modules
Add full support for hotel group and vehicle option site management features: - Define SQLAlchemy models and alembic migration for the new database tables - Add default sample content entries in content.py - Extend admin and public API routes to support the new modules - Update seed script to populate default hotel and vehicle data - Update all relevant documentation and test cases
This commit is contained in:
@@ -8,7 +8,7 @@ from app import serializers
|
||||
from app.auth import hash_password, require_admin
|
||||
from app.database import get_db
|
||||
from app.main import create_app
|
||||
from app.models import AdminUser, Campaign, CtaBanner, Destination, DestinationAlias, HeroSlide, Lead, MapImage, MediaAsset, Product, ProductImage, RouteSection, RouteSectionProduct, ThemeCard
|
||||
from app.models import AdminUser, Campaign, CtaBanner, Destination, DestinationAlias, HeroSlide, HotelGroup, Lead, MapImage, MediaAsset, Product, ProductImage, RouteSection, RouteSectionProduct, ThemeCard, VehicleOption
|
||||
from app.routers import admin as admin_router
|
||||
from app.routers.admin import normalize_detail_sections, normalize_images
|
||||
from app.routers.shared import site_config
|
||||
@@ -180,6 +180,33 @@ def make_cta_banner(**overrides):
|
||||
)
|
||||
|
||||
|
||||
|
||||
def make_hotel_group(**overrides):
|
||||
return HotelGroup(
|
||||
id=overrides.get("id", "hotel-group-test"),
|
||||
title=overrides.get("title", "经典酒店"),
|
||||
description=overrides.get("description", "经典城市酒店与山野度假住宿组合"),
|
||||
image=overrides.get("image", "/assets/hotel.jpg"),
|
||||
sortOrder=overrides.get("sortOrder", 1),
|
||||
isActive=overrides.get("isActive", True),
|
||||
createdAt=datetime(2026, 1, 1),
|
||||
updatedAt=datetime(2026, 1, 2),
|
||||
)
|
||||
|
||||
|
||||
def make_vehicle_option(**overrides):
|
||||
return VehicleOption(
|
||||
id=overrides.get("id", "vehicle-option-test"),
|
||||
title=overrides.get("title", "5座舒适用车"),
|
||||
description=overrides.get("description", "2-8人小团,按人数和行李匹配车型"),
|
||||
image=overrides.get("image", "/assets/vehicle.jpg"),
|
||||
sortOrder=overrides.get("sortOrder", 2),
|
||||
isActive=overrides.get("isActive", True),
|
||||
createdAt=datetime(2026, 1, 1),
|
||||
updatedAt=datetime(2026, 1, 2),
|
||||
)
|
||||
|
||||
|
||||
def make_map_image(**overrides):
|
||||
return MapImage(
|
||||
id=overrides.get("id", "map-test"),
|
||||
@@ -442,6 +469,8 @@ def test_site_config_patch_ignores_fields_not_allowed_for_module_and_audits():
|
||||
("destinations", {"name": "新目的地", "slug": "", "region": None, "isHot": True}, {"name": "新目的地", "slug": "e696b0e79baee79a84e59cb0", "isHot": True}),
|
||||
("themes", {"label": "新主题", "image": None}, {"label": "新主题", "image": ""}),
|
||||
("ctaBanners", {"alt": "新运营入口", "image": None, "targetType": None}, {"alt": "新运营入口", "image": "", "targetType": ""}),
|
||||
("hotelGroups", {"title": "经典酒店", "description": "酒店文案", "image": None}, {"title": "经典酒店", "description": "酒店文案", "image": None}),
|
||||
("vehicleOptions", {"title": "5座舒适用车", "description": "用车文案", "image": None}, {"title": "5座舒适用车", "description": "用车文案", "image": None}),
|
||||
],
|
||||
)
|
||||
def test_site_config_create_modules_defaults_fields_and_audits(module, payload, expected):
|
||||
@@ -496,7 +525,7 @@ def test_site_config_create_requires_module_primary_field():
|
||||
|
||||
def test_admin_site_config_hero_slides_use_dedicated_contract_without_targets():
|
||||
hero_slide = make_hero_slide(targetType="campaign", targetValue="campaign-test")
|
||||
fake_db = FakeDb(scalar_results=[[], [hero_slide], [], [], [], [], [], []])
|
||||
fake_db = FakeDb(scalar_results=[[], [hero_slide], [], [], [], [], [], [], []])
|
||||
app = authenticated_app(fake_db)
|
||||
|
||||
try:
|
||||
@@ -514,7 +543,7 @@ def test_admin_site_config_hero_slides_use_dedicated_contract_without_targets():
|
||||
|
||||
def test_admin_site_config_includes_map_array_with_dedicated_contract():
|
||||
map_image = make_map_image()
|
||||
fake_db = FakeDb(scalar_results=[[], [], [map_image], [], [], [], [], []])
|
||||
fake_db = FakeDb(scalar_results=[[], [], [map_image], [], [], [], [], [], []])
|
||||
app = authenticated_app(fake_db)
|
||||
|
||||
try:
|
||||
@@ -540,7 +569,7 @@ def test_admin_site_config_includes_map_array_with_dedicated_contract():
|
||||
|
||||
def test_admin_site_config_includes_campaigns_for_special_offers():
|
||||
campaign = make_campaign(status="published")
|
||||
fake_db = FakeDb(scalar_results=[[], [], [], [], [], [campaign], [], []])
|
||||
fake_db = FakeDb(scalar_results=[[], [], [], [], [], [campaign], [], [], []])
|
||||
app = authenticated_app(fake_db)
|
||||
|
||||
try:
|
||||
@@ -579,7 +608,7 @@ def test_admin_site_config_includes_route_sections_for_featured_routes():
|
||||
make_route_section_product(productId="product-b", sortOrder=0),
|
||||
],
|
||||
)
|
||||
fake_db = FakeDb(scalar_results=[[], [], [], [], [], [], [section]])
|
||||
fake_db = FakeDb(scalar_results=[[], [], [], [], [], [], [section], [], []])
|
||||
app = authenticated_app(fake_db)
|
||||
|
||||
try:
|
||||
@@ -603,6 +632,45 @@ def test_admin_site_config_includes_route_sections_for_featured_routes():
|
||||
]
|
||||
|
||||
|
||||
def test_admin_site_config_includes_hotel_and_vehicle_modules():
|
||||
hotel_group = make_hotel_group()
|
||||
vehicle_option = make_vehicle_option()
|
||||
fake_db = FakeDb(scalar_results=[[], [], [], [], [], [], [], [hotel_group], [vehicle_option]])
|
||||
app = authenticated_app(fake_db)
|
||||
|
||||
try:
|
||||
response = TestClient(app).get("/api/admin/site-config")
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["hotelGroups"] == [
|
||||
{
|
||||
"id": "hotel-group-test",
|
||||
"title": "经典酒店",
|
||||
"description": "经典城市酒店与山野度假住宿组合",
|
||||
"image": "/assets/hotel.jpg",
|
||||
"sortOrder": 1,
|
||||
"isActive": True,
|
||||
"createdAt": "2026-01-01T00:00:00",
|
||||
"updatedAt": "2026-01-02T00:00:00",
|
||||
}
|
||||
]
|
||||
assert body["vehicleOptions"] == [
|
||||
{
|
||||
"id": "vehicle-option-test",
|
||||
"title": "5座舒适用车",
|
||||
"description": "2-8人小团,按人数和行李匹配车型",
|
||||
"image": "/assets/vehicle.jpg",
|
||||
"sortOrder": 2,
|
||||
"isActive": True,
|
||||
"createdAt": "2026-01-01T00:00:00",
|
||||
"updatedAt": "2026-01-02T00:00:00",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_site_config_create_route_section_generates_dynamic_id_and_audits():
|
||||
fake_db = FakeDb(scalar_results=[[]])
|
||||
app = authenticated_app(fake_db)
|
||||
@@ -957,7 +1025,7 @@ def test_site_config_patch_route_section_rejects_product_used_by_another_section
|
||||
|
||||
def test_public_site_config_campaigns_include_price_and_tags():
|
||||
campaign = make_campaign(status="published", tags=["自然奇景", "小众秘境"])
|
||||
fake_db = FakeDb(scalar_results=[[], [], [], [], [], [campaign], [], []])
|
||||
fake_db = FakeDb(scalar_results=[[], [], [], [], [], [campaign], [], [], []])
|
||||
|
||||
result = site_config(fake_db, active_only=True)
|
||||
|
||||
@@ -989,7 +1057,7 @@ def test_public_site_config_route_sections_filter_unpublished_products():
|
||||
make_route_section_product(product=draft, productId=draft.id, sortOrder=1),
|
||||
]
|
||||
)
|
||||
fake_db = FakeDb(scalar_results=[[], [], [], [], [], [], [section]])
|
||||
fake_db = FakeDb(scalar_results=[[], [], [], [], [], [], [section], [], []])
|
||||
|
||||
result = site_config(fake_db, active_only=True)
|
||||
|
||||
@@ -1004,6 +1072,19 @@ def test_public_site_config_route_sections_filter_unpublished_products():
|
||||
]
|
||||
|
||||
|
||||
def test_public_site_config_includes_hotel_and_vehicle_modules():
|
||||
hotel_group = make_hotel_group()
|
||||
vehicle_option = make_vehicle_option()
|
||||
fake_db = FakeDb(scalar_results=[[], [], [], [], [], [], [], [hotel_group], [vehicle_option]])
|
||||
|
||||
result = site_config(fake_db, active_only=True)
|
||||
|
||||
assert result["hotelGroups"][0]["title"] == "经典酒店"
|
||||
assert result["hotelGroups"][0]["description"] == "经典城市酒店与山野度假住宿组合"
|
||||
assert result["vehicleOptions"][0]["title"] == "5座舒适用车"
|
||||
assert result["vehicleOptions"][0]["image"] == "/assets/vehicle.jpg"
|
||||
|
||||
|
||||
def test_site_config_patch_updates_destination_contract_fields():
|
||||
destination = make_destination()
|
||||
fake_db = FakeDb(get_result=destination)
|
||||
|
||||
Reference in New Issue
Block a user