feat: per-task on/off switches on /sync page for staged rollout
Adds evo_mirror_enabled and vk_mirror_enabled flags to SyncConfig. Each of the three background tasks (Зеркало Эвотор / Зеркало ВК / Синхронизация) can now be enabled independently from the /sync page. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
18
web/migrations/versions/0009_sync_config_task_flags.py
Normal file
18
web/migrations/versions/0009_sync_config_task_flags.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"""Add evo_mirror_enabled and vk_mirror_enabled to sync_configs."""
|
||||||
|
revision = "0009"
|
||||||
|
down_revision = "0008"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.add_column("sync_configs", sa.Column("evo_mirror_enabled", sa.Boolean, nullable=False, server_default="0"))
|
||||||
|
op.add_column("sync_configs", sa.Column("vk_mirror_enabled", sa.Boolean, nullable=False, server_default="0"))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_column("sync_configs", "vk_mirror_enabled")
|
||||||
|
op.drop_column("sync_configs", "evo_mirror_enabled")
|
||||||
@@ -57,6 +57,8 @@ class SyncConfig(Base):
|
|||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
||||||
is_enabled = Column(Boolean, nullable=False, default=False)
|
is_enabled = Column(Boolean, nullable=False, default=False)
|
||||||
|
evo_mirror_enabled = Column(Boolean, nullable=False, default=False)
|
||||||
|
vk_mirror_enabled = Column(Boolean, nullable=False, default=False)
|
||||||
confirmed_at = Column(DateTime, nullable=True)
|
confirmed_at = Column(DateTime, nullable=True)
|
||||||
price_multiplier = Column(Numeric(10, 4), nullable=False, default=1.0)
|
price_multiplier = Column(Numeric(10, 4), nullable=False, default=1.0)
|
||||||
created_at = Column(DateTime, nullable=False, server_default=func.now())
|
created_at = Column(DateTime, nullable=False, server_default=func.now())
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from datetime import datetime, timezone
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends, Request
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from web.auth.session import get_current_user
|
from web.auth.session import get_current_user
|
||||||
@@ -21,10 +20,6 @@ def _render(request: Request, ctx: dict):
|
|||||||
return templates.TemplateResponse(ctx.pop("request"), "sync.html", ctx)
|
return templates.TemplateResponse(ctx.pop("request"), "sync.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
def _now():
|
|
||||||
return datetime.now(timezone.utc).replace(tzinfo=None)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/sync")
|
@router.get("/sync")
|
||||||
async def sync_get(request: Request, db: Session = Depends(get_db)):
|
async def sync_get(request: Request, db: Session = Depends(get_db)):
|
||||||
try:
|
try:
|
||||||
@@ -48,8 +43,12 @@ async def sync_settings_post(request: Request, db: Session = Depends(get_db)):
|
|||||||
return RedirectResponse("/login", 303)
|
return RedirectResponse("/login", 303)
|
||||||
|
|
||||||
form = await request.form()
|
form = await request.form()
|
||||||
raw_multiplier = str(form.get("price_multiplier", "1")).strip()
|
|
||||||
|
|
||||||
|
evo_mirror_enabled = form.get("evo_mirror_enabled") == "1"
|
||||||
|
vk_mirror_enabled = form.get("vk_mirror_enabled") == "1"
|
||||||
|
sync_enabled = form.get("is_enabled") == "1"
|
||||||
|
|
||||||
|
raw_multiplier = str(form.get("price_multiplier", "1")).strip()
|
||||||
try:
|
try:
|
||||||
multiplier = float(raw_multiplier)
|
multiplier = float(raw_multiplier)
|
||||||
if multiplier <= 0:
|
if multiplier <= 0:
|
||||||
@@ -59,11 +58,16 @@ async def sync_settings_post(request: Request, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
config = db.query(SyncConfig).filter_by(user_id=user.id).first()
|
config = db.query(SyncConfig).filter_by(user_id=user.id).first()
|
||||||
if config:
|
if config:
|
||||||
|
config.evo_mirror_enabled = evo_mirror_enabled
|
||||||
|
config.vk_mirror_enabled = vk_mirror_enabled
|
||||||
|
config.is_enabled = sync_enabled
|
||||||
config.price_multiplier = multiplier
|
config.price_multiplier = multiplier
|
||||||
else:
|
else:
|
||||||
config = SyncConfig(
|
config = SyncConfig(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
is_enabled=False,
|
evo_mirror_enabled=evo_mirror_enabled,
|
||||||
|
vk_mirror_enabled=vk_mirror_enabled,
|
||||||
|
is_enabled=sync_enabled,
|
||||||
price_multiplier=multiplier,
|
price_multiplier=multiplier,
|
||||||
)
|
)
|
||||||
db.add(config)
|
db.add(config)
|
||||||
|
|||||||
@@ -215,6 +215,9 @@ def refresh_catalog(self) -> dict:
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
for conn in connections:
|
for conn in connections:
|
||||||
|
cfg = db.query(SyncConfig).filter_by(user_id=conn.user_id).first()
|
||||||
|
if not cfg or not cfg.evo_mirror_enabled:
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
_sync_user(db, conn.user_id, conn.access_token)
|
_sync_user(db, conn.user_id, conn.access_token)
|
||||||
results["ok"] += 1
|
results["ok"] += 1
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from celery import shared_task
|
|||||||
import web.lib.api_logger as api_logger
|
import web.lib.api_logger as api_logger
|
||||||
from web.config import settings
|
from web.config import settings
|
||||||
from web.database import SessionLocal
|
from web.database import SessionLocal
|
||||||
from web.models.connections import VkCachedAlbum, VkCachedProduct, VkConnection
|
from web.models.connections import SyncConfig, VkCachedAlbum, VkCachedProduct, VkConnection
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -172,6 +172,9 @@ def refresh_vk_catalog(self) -> dict:
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
for conn in connections:
|
for conn in connections:
|
||||||
|
cfg = db.query(SyncConfig).filter_by(user_id=conn.user_id).first()
|
||||||
|
if not cfg or not cfg.vk_mirror_enabled:
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
_sync_user(db, conn.user_id, conn.access_token, conn.vk_user_id)
|
_sync_user(db, conn.user_id, conn.access_token, conn.vk_user_id)
|
||||||
results["ok"] += 1
|
results["ok"] += 1
|
||||||
|
|||||||
@@ -10,9 +10,52 @@
|
|||||||
<div role="alert" class="alert alert-success"><p>Настройки сохранены.</p></div>
|
<div role="alert" class="alert alert-success"><p>Настройки сохранены.</p></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<article class="card">
|
|
||||||
<h2 style="font-size:1.1rem; margin-bottom:1.25rem;">Настройки цены</h2>
|
|
||||||
<form method="post" action="/sync/settings">
|
<form method="post" action="/sync/settings">
|
||||||
|
|
||||||
|
<article class="card mb-3">
|
||||||
|
<h2 style="font-size:1.1rem; margin-bottom:1.25rem;">Фоновые задачи</h2>
|
||||||
|
<p class="text-muted small" style="margin-bottom:1.25rem;">Включайте поочерёдно: сначала проверьте зеркало Эвотор, затем ВК, затем синхронизацию.</p>
|
||||||
|
|
||||||
|
<div style="display:flex; flex-direction:column; gap:1rem;">
|
||||||
|
|
||||||
|
<label style="display:flex; align-items:flex-start; gap:0.75rem; cursor:pointer;">
|
||||||
|
<input type="hidden" name="evo_mirror_enabled" value="0">
|
||||||
|
<input type="checkbox" name="evo_mirror_enabled" value="1" role="switch"
|
||||||
|
{% if config and config.evo_mirror_enabled %}checked{% endif %}
|
||||||
|
style="margin-top:0.2rem; flex-shrink:0;">
|
||||||
|
<span>
|
||||||
|
<strong>Зеркало Эвотор</strong><br>
|
||||||
|
<span class="text-muted small">Периодически импортирует товары, группы и магазины из Эвотор в локальную базу.</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label style="display:flex; align-items:flex-start; gap:0.75rem; cursor:pointer;">
|
||||||
|
<input type="hidden" name="vk_mirror_enabled" value="0">
|
||||||
|
<input type="checkbox" name="vk_mirror_enabled" value="1" role="switch"
|
||||||
|
{% if config and config.vk_mirror_enabled %}checked{% endif %}
|
||||||
|
style="margin-top:0.2rem; flex-shrink:0;">
|
||||||
|
<span>
|
||||||
|
<strong>Зеркало ВК</strong><br>
|
||||||
|
<span class="text-muted small">Периодически импортирует альбомы и товары из VK Market в локальный кэш.</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label style="display:flex; align-items:flex-start; gap:0.75rem; cursor:pointer;">
|
||||||
|
<input type="hidden" name="is_enabled" value="0">
|
||||||
|
<input type="checkbox" name="is_enabled" value="1" role="switch"
|
||||||
|
{% if config and config.is_enabled %}checked{% endif %}
|
||||||
|
style="margin-top:0.2rem; flex-shrink:0;">
|
||||||
|
<span>
|
||||||
|
<strong>Синхронизация</strong><br>
|
||||||
|
<span class="text-muted small">Зеркалит каталог Эвотор в VK Market: создаёт, обновляет и удаляет товары.</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="card mb-3">
|
||||||
|
<h2 style="font-size:1.1rem; margin-bottom:1.25rem;">Настройки цены</h2>
|
||||||
<label style="max-width:320px;">
|
<label style="max-width:320px;">
|
||||||
Множитель цены
|
Множитель цены
|
||||||
<input type="number" name="price_multiplier" step="0.0001" min="0.0001"
|
<input type="number" name="price_multiplier" step="0.0001" min="0.0001"
|
||||||
@@ -20,7 +63,9 @@
|
|||||||
placeholder="1">
|
placeholder="1">
|
||||||
<small class="text-muted">Цена из Эвотор умножается на это значение перед отправкой в ВК. По умолчанию: 1.</small>
|
<small class="text-muted">Цена из Эвотор умножается на это значение перед отправкой в ВК. По умолчанию: 1.</small>
|
||||||
</label>
|
</label>
|
||||||
<button type="submit" style="margin-top:1rem;">Сохранить</button>
|
|
||||||
</form>
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
<button type="submit">Сохранить</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user