Files
news_all_spo/web/views/vk_sources.py
2026-05-15 03:31:28 +05:00

360 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import flet as ft
import designer as d
import api_client as api
from views.dashboard import _admin_appbar, _sidebar
class VkSourcesView:
def __init__(self, page: ft.Page):
self.page = page
self._sources: list = []
self._api_ok = False
self._editing_id: str | None = None # ID источника в режиме редактирования
# Статус бейдж
self._status_row = ft.Row(spacing=6)
# Уведомление (добавляется в page.overlay при load)
self._snack = ft.SnackBar(content=ft.Text(""), open=False)
# Таблица источников
self._table = ft.DataTable(
columns=[
ft.DataColumn(ft.Text("Группа", weight=ft.FontWeight.W_600, size=13)),
ft.DataColumn(ft.Text("ID", weight=ft.FontWeight.W_600, size=13)),
ft.DataColumn(ft.Text("Последний импорт", weight=ft.FontWeight.W_600, size=13)),
ft.DataColumn(ft.Text("Статус", weight=ft.FontWeight.W_600, size=13)),
ft.DataColumn(ft.Text("Действия", weight=ft.FontWeight.W_600, size=13)),
],
rows=[],
border=ft.border.all(1, d.DIVIDER),
border_radius=8,
vertical_lines=ft.BorderSide(1, d.DIVIDER),
heading_row_color=ft.colors.with_opacity(0.04, ft.colors.BLACK),
data_row_max_height=56,
column_spacing=16,
)
# Форма добавления / редактирования
self._form_gid = ft.TextField(
label="ID группы (числовой)",
hint_text="95503797",
width=220,
dense=True,
)
self._form_gname = ft.TextField(
label="Название группы",
hint_text="ЮУрГК",
width=240,
dense=True,
)
self._form_enabled = ft.Checkbox(label="Активна", value=True)
self._form_save_btn = ft.ElevatedButton(
"Добавить",
icon=ft.icons.ADD,
bgcolor=d.PRIMARY,
color=ft.colors.WHITE,
on_click=lambda e: self.page.run_task(self._save_source),
)
self._form_cancel_btn = ft.TextButton(
"Отмена",
visible=False,
on_click=lambda e: self._cancel_edit(),
)
# ── Build ──────────────────────────────────────────────────────────────────
def build_page_view(self) -> ft.View:
token_info = ft.Container(
content=ft.Column([
self._status_row,
ft.Text(
"Без токена VK API — HTML-парсинг не работает (ВК перешёл на JS-рендеринг).\n"
"Получи бесплатный сервисный ключ: vk.com/dev → Мои приложения → Создать → "
"Standalone → вкладка «Настройки» → «Сервисный ключ доступа».\n"
"Затем добавь в .env: VK_ACCESS_TOKEN=<ключ>",
size=12,
color=d.TEXT_SECONDARY,
selectable=True,
),
], spacing=6),
bgcolor=ft.colors.with_opacity(0.04, ft.colors.BLACK),
border_radius=8,
padding=14,
border=ft.border.all(1, d.DIVIDER),
)
form = ft.Container(
content=ft.Column([
d.headline("Добавить / редактировать группу", size=15),
ft.Row(
[self._form_gid, self._form_gname, self._form_enabled],
spacing=10, wrap=True,
),
ft.Row(
[self._form_save_btn, self._form_cancel_btn],
spacing=10,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
),
], spacing=10),
bgcolor=d.SURFACE,
border_radius=10,
padding=16,
border=ft.border.all(1, d.DIVIDER),
)
import_btns = ft.Row([
ft.ElevatedButton(
"Импорт новых постов",
icon=ft.icons.SYNC,
bgcolor=d.PRIMARY,
color=ft.colors.WHITE,
tooltip="Загрузить новые посты со всех активных групп",
on_click=lambda e: self.page.run_task(self._import_recent),
),
ft.ElevatedButton(
"Импорт за год (все)",
icon=ft.icons.HISTORY,
bgcolor=d.WARNING,
color=ft.colors.WHITE,
tooltip="Пагинированный импорт за 365 дней. Запускается в фоне.",
on_click=lambda e: self.page.run_task(self._import_history_all),
),
], spacing=10, wrap=True)
content = ft.Column([
d.headline("ВКонтакте — источники", size=22),
token_info,
import_btns,
ft.Divider(height=4, color="transparent"),
form,
ft.Divider(height=4, color="transparent"),
d.headline("Группы в базе", size=15),
ft.Container(
content=self._table,
border_radius=8,
clip_behavior=ft.ClipBehavior.HARD_EDGE,
),
], scroll=ft.ScrollMode.AUTO, spacing=12, expand=True)
return ft.View(
route="/admin/vk",
bgcolor=d.BACKGROUND,
appbar=_admin_appbar(self.page, "ВКонтакте"),
controls=[
ft.Row([
_sidebar(self.page, active="/admin/vk"),
ft.Container(content=content, expand=True, padding=24),
], expand=True, spacing=0),
],
)
# ── Load ───────────────────────────────────────────────────────────────────
async def load(self):
self.page.overlay.append(self._snack)
token = self.page.session.get("token")
status = await api.admin_vk_status(token)
self._api_ok = status.get("configured", False)
self._status_row.controls = [
ft.Icon(
ft.icons.CHECK_CIRCLE if self._api_ok else ft.icons.WARNING_AMBER_ROUNDED,
color=d.SUCCESS if self._api_ok else d.WARNING,
size=18,
),
ft.Text(
"API-токен подключён — VK API активен" if self._api_ok
else "Токен не задан — парсинг недоступен",
size=13,
weight=ft.FontWeight.W_600,
color=d.SUCCESS if self._api_ok else d.WARNING,
),
]
self._sources = await api.admin_vk_list_sources(token)
self._render_table()
self.page.update()
# ── Table ──────────────────────────────────────────────────────────────────
def _render_table(self):
if not self._sources:
self._table.rows = [
ft.DataRow(cells=[
ft.DataCell(ft.Text("Групп пока нет", color=d.TEXT_SECONDARY, italic=True)),
ft.DataCell(ft.Text("")),
ft.DataCell(ft.Text("")),
ft.DataCell(ft.Text("")),
ft.DataCell(ft.Text("")),
])
]
return
self._table.rows = [self._build_row(s) for s in self._sources]
def _build_row(self, src: dict) -> ft.DataRow:
last_run = src.get("last_run")
if last_run:
try:
from datetime import datetime
dt = datetime.fromisoformat(last_run.replace("Z", "+00:00"))
last_run_str = dt.strftime("%d.%m.%Y %H:%M")
except Exception:
last_run_str = last_run[:16]
else:
last_run_str = "никогда"
enabled = src.get("enabled", True)
return ft.DataRow(
cells=[
ft.DataCell(ft.Text(src["group_name"], size=13, weight=ft.FontWeight.W_500)),
ft.DataCell(ft.Text(src["group_id"], size=13, color=d.TEXT_SECONDARY, selectable=True)),
ft.DataCell(ft.Text(last_run_str, size=12, color=d.TEXT_SECONDARY)),
ft.DataCell(
ft.Container(
content=ft.Text(
"Активна" if enabled else "Отключена",
size=11,
color=ft.colors.WHITE,
),
bgcolor=d.SUCCESS if enabled else d.TEXT_SECONDARY,
border_radius=4,
padding=ft.padding.symmetric(horizontal=8, vertical=3),
)
),
ft.DataCell(
ft.Row([
ft.IconButton(
ft.icons.EDIT_OUTLINED,
icon_size=18,
icon_color=d.PRIMARY,
tooltip="Редактировать",
on_click=lambda e, s=src: self._start_edit(s),
),
ft.IconButton(
ft.icons.TOGGLE_ON if enabled else ft.icons.TOGGLE_OFF,
icon_size=22,
icon_color=d.SUCCESS if enabled else d.TEXT_SECONDARY,
tooltip="Вкл / Выкл",
on_click=lambda e, s=src: self.page.run_task(self._toggle, s),
),
ft.IconButton(
ft.icons.HISTORY,
icon_size=18,
icon_color=d.WARNING,
tooltip="Импорт за год для этой группы",
on_click=lambda e, s=src: self.page.run_task(
self._import_history_one, s["id"], s["group_name"]
),
),
ft.IconButton(
ft.icons.DELETE_OUTLINE,
icon_size=18,
icon_color=d.ERROR,
tooltip="Удалить группу",
on_click=lambda e, s=src: self.page.run_task(self._delete, s["id"]),
),
], spacing=0),
),
]
)
# ── Edit form helpers ──────────────────────────────────────────────────────
def _start_edit(self, src: dict):
self._editing_id = src["id"]
self._form_gid.value = src["group_id"]
self._form_gname.value = src["group_name"]
self._form_enabled.value = src.get("enabled", True)
self._form_save_btn.text = "Сохранить"
self._form_save_btn.icon = ft.icons.SAVE
self._form_cancel_btn.visible = True
self.page.update()
def _cancel_edit(self):
self._editing_id = None
self._form_gid.value = ""
self._form_gname.value = ""
self._form_enabled.value = True
self._form_save_btn.text = "Добавить"
self._form_save_btn.icon = ft.icons.ADD
self._form_cancel_btn.visible = False
self.page.update()
# ── CRUD actions ───────────────────────────────────────────────────────────
async def _save_source(self):
gid = self._form_gid.value.strip()
gname = self._form_gname.value.strip()
if not gid or not gname:
self._notify("Заполни ID и название", d.ERROR)
return
token = self.page.session.get("token")
if self._editing_id:
result = await api.admin_vk_update_source(
token, self._editing_id, gid, gname, self._form_enabled.value
)
self._notify("Группа обновлена" if result else "Ошибка обновления",
d.SUCCESS if result else d.ERROR)
else:
result = await api.admin_vk_add_source(token, gid, gname, self._form_enabled.value)
if result and "error" not in result:
self._notify(f"Группа «{gname}» добавлена", d.SUCCESS)
else:
err = result.get("error") if result else "Ошибка запроса"
self._notify(str(err), d.ERROR)
return
self._cancel_edit()
self._sources = await api.admin_vk_list_sources(token)
self._render_table()
self.page.update()
async def _toggle(self, src: dict):
token = self.page.session.get("token")
result = await api.admin_vk_update_source(
token, src["id"], src["group_id"], src["group_name"], not src["enabled"]
)
if result:
self._sources = await api.admin_vk_list_sources(token)
self._render_table()
self.page.update()
async def _delete(self, source_id: str):
token = self.page.session.get("token")
ok = await api.admin_vk_delete_source(token, source_id)
self._notify("Группа удалена" if ok else "Ошибка удаления",
d.TEXT_SECONDARY if ok else d.ERROR)
self._sources = await api.admin_vk_list_sources(token)
self._render_table()
self.page.update()
async def _import_recent(self):
self._notify("Запускаю импорт новых постов…", d.PRIMARY)
token = self.page.session.get("token")
result = await api.admin_vk_import(token)
msg = result.get("message", "Запущен") if result else "Ошибка"
self._notify(msg, d.SUCCESS if result and result.get("ok") else d.ERROR)
async def _import_history_all(self):
self._notify("Запускаю исторический импорт в фоне…", d.WARNING)
token = self.page.session.get("token")
result = await api.admin_vk_import_history(token)
msg = result.get("message", "Запущен") if result else "Ошибка"
self._notify(msg, d.SUCCESS if result and result.get("ok") else d.ERROR)
async def _import_history_one(self, source_id: str, name: str):
self._notify(f"Запускаю год для «{name}»…", d.WARNING)
token = self.page.session.get("token")
result = await api.admin_vk_import_history(token, source_id=source_id)
msg = result.get("message", "Запущен") if result else "Ошибка"
self._notify(msg, d.SUCCESS if result and result.get("ok") else d.ERROR)
def _notify(self, text: str, color: str = d.SUCCESS):
self._snack.content = ft.Text(text, color=ft.colors.WHITE)
self._snack.bgcolor = color
self._snack.open = True
self.page.update()