feat: landing page analytics goals and sticky pay button
- Add sticky_pay_button, analytics_view_enabled, analytics_view_goal, analytics_click_enabled, analytics_click_goal columns to LandingPage - Add fields to CRUD updatable fields, admin create/update/detail schemas, create_landing() kwargs, _landing_to_detail() response - Expose sticky_pay_button and analytics fields in public landing config response for frontend Yandex Metrika integration - Add migration 0062 with idempotent column checks Based on PR #2852 by @smediainfo — CI/CD workflow changes excluded (hardcoded version strings would regress dynamic manifest reading)
This commit is contained in:
@@ -205,6 +205,11 @@ class LandingCreateRequest(BaseModel):
|
||||
discount_ends_at: datetime | None = None
|
||||
discount_badge_text: dict[str, str] | None = None
|
||||
background_config: dict | None = None
|
||||
sticky_pay_button: bool = False
|
||||
analytics_view_enabled: bool = False
|
||||
analytics_view_goal: str | None = Field(default=None, max_length=64)
|
||||
analytics_click_enabled: bool = False
|
||||
analytics_click_goal: str | None = Field(default=None, max_length=64)
|
||||
|
||||
@field_validator('background_config')
|
||||
@classmethod
|
||||
@@ -314,6 +319,11 @@ class LandingUpdateRequest(BaseModel):
|
||||
discount_ends_at: datetime | None = None
|
||||
discount_badge_text: dict[str, str] | None = None
|
||||
background_config: dict | None = None
|
||||
sticky_pay_button: bool | None = None
|
||||
analytics_view_enabled: bool | None = None
|
||||
analytics_view_goal: str | None = Field(default=None, max_length=64)
|
||||
analytics_click_enabled: bool | None = None
|
||||
analytics_click_goal: str | None = Field(default=None, max_length=64)
|
||||
|
||||
@field_validator('background_config')
|
||||
@classmethod
|
||||
@@ -461,6 +471,11 @@ class LandingDetailResponse(BaseModel):
|
||||
discount_ends_at: datetime | None = None
|
||||
discount_badge_text: dict[str, str] | None = None
|
||||
background_config: dict | None = None
|
||||
sticky_pay_button: bool = False
|
||||
analytics_view_enabled: bool = False
|
||||
analytics_view_goal: str | None = None
|
||||
analytics_click_enabled: bool = False
|
||||
analytics_click_goal: str | None = None
|
||||
created_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
|
||||
@@ -638,6 +653,11 @@ async def create_landing_page(
|
||||
discount_ends_at=request.discount_ends_at,
|
||||
discount_badge_text=request.discount_badge_text,
|
||||
background_config=request.background_config,
|
||||
sticky_pay_button=request.sticky_pay_button,
|
||||
analytics_view_enabled=request.analytics_view_enabled,
|
||||
analytics_view_goal=request.analytics_view_goal,
|
||||
analytics_click_enabled=request.analytics_click_enabled,
|
||||
analytics_click_goal=request.analytics_click_goal,
|
||||
)
|
||||
|
||||
logger.info('Admin created landing page', admin_id=admin.id, slug=landing.slug, landing_id=landing.id)
|
||||
@@ -1068,6 +1088,11 @@ def _landing_to_detail(landing: LandingPage) -> LandingDetailResponse:
|
||||
discount_ends_at=landing.discount_ends_at,
|
||||
discount_badge_text=landing.discount_badge_text,
|
||||
background_config=landing.background_config,
|
||||
sticky_pay_button=landing.sticky_pay_button,
|
||||
analytics_view_enabled=landing.analytics_view_enabled,
|
||||
analytics_view_goal=landing.analytics_view_goal,
|
||||
analytics_click_enabled=landing.analytics_click_enabled,
|
||||
analytics_click_goal=landing.analytics_click_goal,
|
||||
created_at=landing.created_at,
|
||||
updated_at=landing.updated_at,
|
||||
)
|
||||
|
||||
@@ -99,6 +99,11 @@ class LandingConfigResponse(BaseModel):
|
||||
meta_description: str | None = None
|
||||
discount: LandingDiscountInfo | None = None # null if no active discount
|
||||
background_config: dict | None = None
|
||||
sticky_pay_button: bool = False
|
||||
analytics_view_enabled: bool = False
|
||||
analytics_view_goal: str | None = None
|
||||
analytics_click_enabled: bool = False
|
||||
analytics_click_goal: str | None = None
|
||||
|
||||
|
||||
_EMAIL_RE = re.compile(r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$')
|
||||
@@ -535,6 +540,11 @@ async def get_landing_config(
|
||||
meta_description=resolve_locale_text(landing.meta_description, lang) or None,
|
||||
discount=discount,
|
||||
background_config=landing.background_config,
|
||||
sticky_pay_button=landing.sticky_pay_button,
|
||||
analytics_view_enabled=landing.analytics_view_enabled,
|
||||
analytics_view_goal=landing.analytics_view_goal,
|
||||
analytics_click_enabled=landing.analytics_click_enabled,
|
||||
analytics_click_goal=landing.analytics_click_goal,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -76,6 +76,11 @@ _LANDING_UPDATABLE_FIELDS = frozenset(
|
||||
'discount_ends_at',
|
||||
'discount_badge_text',
|
||||
'background_config',
|
||||
'sticky_pay_button',
|
||||
'analytics_view_enabled',
|
||||
'analytics_view_goal',
|
||||
'analytics_click_enabled',
|
||||
'analytics_click_goal',
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -3459,6 +3459,13 @@ class LandingPage(Base):
|
||||
background_config = Column(
|
||||
JSON, nullable=True
|
||||
) # AnimationConfig: {enabled, type, settings, opacity, blur, reducedOnMobile}
|
||||
# Sticky pay button on mobile (full-width fixed bottom)
|
||||
sticky_pay_button = Column(Boolean, nullable=False, default=False, server_default=text('false'))
|
||||
# Yandex Metrika landing-level conversion goals
|
||||
analytics_view_enabled = Column(Boolean, nullable=False, default=False, server_default=text('false'))
|
||||
analytics_view_goal = Column(String(64), nullable=True)
|
||||
analytics_click_enabled = Column(Boolean, nullable=False, default=False, server_default=text('false'))
|
||||
analytics_click_goal = Column(String(64), nullable=True)
|
||||
created_at = Column(AwareDateTime(), server_default=func.now())
|
||||
updated_at = Column(AwareDateTime(), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""add landing_pages analytics columns + sticky_pay_button
|
||||
|
||||
Revision ID: 0062
|
||||
Revises: 0061
|
||||
Create Date: 2026-04-22
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = '0062'
|
||||
down_revision: Union[str, None] = '0061'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
NEW_COLUMNS = (
|
||||
(
|
||||
'sticky_pay_button',
|
||||
sa.Column('sticky_pay_button', sa.Boolean(), nullable=False, server_default=sa.text('false')),
|
||||
),
|
||||
(
|
||||
'analytics_view_enabled',
|
||||
sa.Column('analytics_view_enabled', sa.Boolean(), nullable=False, server_default=sa.text('false')),
|
||||
),
|
||||
('analytics_view_goal', sa.Column('analytics_view_goal', sa.String(64), nullable=True)),
|
||||
(
|
||||
'analytics_click_enabled',
|
||||
sa.Column('analytics_click_enabled', sa.Boolean(), nullable=False, server_default=sa.text('false')),
|
||||
),
|
||||
('analytics_click_goal', sa.Column('analytics_click_goal', sa.String(64), nullable=True)),
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
for col_name, col_def in NEW_COLUMNS:
|
||||
result = conn.execute(
|
||||
sa.text(
|
||||
'SELECT EXISTS (SELECT 1 FROM information_schema.columns '
|
||||
"WHERE table_name = 'landing_pages' AND column_name = :col)"
|
||||
),
|
||||
{'col': col_name},
|
||||
)
|
||||
if not result.scalar():
|
||||
op.add_column('landing_pages', col_def)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
for col_name, _ in reversed(NEW_COLUMNS):
|
||||
result = conn.execute(
|
||||
sa.text(
|
||||
'SELECT EXISTS (SELECT 1 FROM information_schema.columns '
|
||||
"WHERE table_name = 'landing_pages' AND column_name = :col)"
|
||||
),
|
||||
{'col': col_name},
|
||||
)
|
||||
if result.scalar():
|
||||
op.drop_column('landing_pages', col_name)
|
||||
Reference in New Issue
Block a user