draft-seo-handling
All checks were successful
Deploy on push / deploy (push) Successful in 1m10s

This commit is contained in:
Untone 2025-04-15 20:16:01 +03:00
parent bd129efde6
commit eb216a5f36
5 changed files with 30 additions and 30 deletions

View File

@ -32,7 +32,7 @@ class Draft(Base):
created_at: int = Column(Integer, nullable=False, default=lambda: int(time.time())) created_at: int = Column(Integer, nullable=False, default=lambda: int(time.time()))
created_by: int = Column(ForeignKey("author.id"), nullable=False) created_by: int = Column(ForeignKey("author.id"), nullable=False)
community: int = Column(ForeignKey("community.id"), nullable=False, default=1) community: int = Column(ForeignKey("community.id"), nullable=False, default=1)
# optional # optional
layout: str = Column(String, nullable=True, default="article") layout: str = Column(String, nullable=True, default="article")
slug: str = Column(String, unique=True) slug: str = Column(String, unique=True)

View File

@ -1,8 +1,8 @@
import time import time
from operator import or_ from operator import or_
from sqlalchemy.sql import and_
import trafilatura import trafilatura
from sqlalchemy.sql import and_
from cache.cache import ( from cache.cache import (
cache_author, cache_author,
@ -104,7 +104,7 @@ async def create_draft(_, info, draft_input):
if "title" not in draft_input or not draft_input["title"]: if "title" not in draft_input or not draft_input["title"]:
draft_input["title"] = "" # Пустая строка вместо NULL draft_input["title"] = "" # Пустая строка вместо NULL
# Проверяем slug - он должен быть или не пустым, или не передаваться вообще # Проверяем slug - он должен быть или не пустым, или не передаваться вообще
if "slug" in draft_input and (draft_input["slug"] is None or draft_input["slug"] == ""): if "slug" in draft_input and (draft_input["slug"] is None or draft_input["slug"] == ""):
# При создании черновика удаляем пустой slug из входных данных # При создании черновика удаляем пустой slug из входных данных
@ -115,9 +115,9 @@ async def create_draft(_, info, draft_input):
# Remove id from input if present since it's auto-generated # Remove id from input if present since it's auto-generated
if "id" in draft_input: if "id" in draft_input:
del draft_input["id"] del draft_input["id"]
if "seo" not in draft_input and not draft_input["seo"]: if "seo" not in draft_input and not draft_input["seo"]:
body_teaser = draft_input.get("body", "")[:300].split('\n')[:-1].join("\n") body_teaser = draft_input.get("body", "")[:300].split("\n")[:-1].join("\n")
draft_input["seo"] = draft_input.get("lead", body_teaser) draft_input["seo"] = draft_input.get("lead", body_teaser)
# Добавляем текущее время создания # Добавляем текущее время создания
@ -164,13 +164,13 @@ async def update_draft(_, info, draft_id: int, draft_input):
draft = session.query(Draft).filter(Draft.id == draft_id).first() draft = session.query(Draft).filter(Draft.id == draft_id).first()
if not draft: if not draft:
return {"error": "Draft not found"} return {"error": "Draft not found"}
if "seo" not in draft_input and not draft.seo: if "seo" not in draft_input and not draft.seo:
body_src = draft_input["body"] if "body" in draft_input else draft.body body_src = draft_input["body"] if "body" in draft_input else draft.body
body_text = trafilatura.extract(body_src) body_text = trafilatura.extract(body_src)
lead_src = draft_input["lead"] if "lead" in draft_input else draft.lead lead_src = draft_input["lead"] if "lead" in draft_input else draft.lead
lead_text = trafilatura.extract(lead_src) lead_text = trafilatura.extract(lead_src)
body_teaser = body_text[:300].split('. ')[:-1].join(".\n") body_teaser = body_text[:300].split(". ")[:-1].join(".\n")
draft_input["seo"] = lead_text or body_teaser draft_input["seo"] = lead_text or body_teaser
Draft.update(draft, draft_input) Draft.update(draft, draft_input)
@ -178,7 +178,7 @@ async def update_draft(_, info, draft_id: int, draft_input):
current_time = int(time.time()) current_time = int(time.time())
draft.updated_at = current_time draft.updated_at = current_time
draft.updated_by = author_id draft.updated_by = author_id
session.commit() session.commit()
return {"draft": draft} return {"draft": draft}

View File

@ -1,6 +1,7 @@
import time import time
import orjson import orjson
import trafilatura
from sqlalchemy import and_, desc, select from sqlalchemy import and_, desc, select
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from sqlalchemy.sql.functions import coalesce from sqlalchemy.sql.functions import coalesce
@ -22,7 +23,6 @@ from services.notify import notify_shout
from services.schema import query from services.schema import query
from services.search import search_service from services.search import search_service
from utils.logger import root_logger as logger from utils.logger import root_logger as logger
import trafilatura
async def cache_by_id(entity, entity_id: int, cache_method): async def cache_by_id(entity, entity_id: int, cache_method):
@ -181,7 +181,7 @@ async def create_shout(_, info, inp):
lead = inp.get("lead", "") lead = inp.get("lead", "")
body_text = trafilatura.extract(body) body_text = trafilatura.extract(body)
lead_text = trafilatura.extract(lead) lead_text = trafilatura.extract(lead)
seo = inp.get("seo", lead_text or body_text[:300].split('. ')[:-1].join(". ")) seo = inp.get("seo", lead_text or body_text[:300].split(". ")[:-1].join(". "))
new_shout = Shout( new_shout = Shout(
slug=slug, slug=slug,
body=body, body=body,
@ -388,7 +388,7 @@ def patch_topics(session, shout, topics_input):
# @login_required # @login_required
async def update_shout(_, info, shout_id: int, shout_input=None, publish=False): async def update_shout(_, info, shout_id: int, shout_input=None, publish=False):
logger.info(f"Starting update_shout with id={shout_id}, publish={publish}") logger.info(f"Starting update_shout with id={shout_id}, publish={publish}")
logger.debug(f"Full shout_input: {shout_input}") # DraftInput logger.debug(f"Full shout_input: {shout_input}") # DraftInput
user_id = info.context.get("user_id") user_id = info.context.get("user_id")
roles = info.context.get("roles", []) roles = info.context.get("roles", [])

View File

@ -6,7 +6,7 @@ from cache.cache import (
get_cached_topic_authors, get_cached_topic_authors,
get_cached_topic_by_slug, get_cached_topic_by_slug,
get_cached_topic_followers, get_cached_topic_followers,
invalidate_cache_by_prefix invalidate_cache_by_prefix,
) )
from orm.author import Author from orm.author import Author
from orm.topic import Topic from orm.topic import Topic
@ -126,7 +126,7 @@ async def get_topics_with_stats(limit=100, offset=0, community_id=None, by=None)
GROUP BY topic GROUP BY topic
""" """
followers_stats = {row[0]: row[1] for row in session.execute(text(followers_stats_query))} followers_stats = {row[0]: row[1] for row in session.execute(text(followers_stats_query))}
# Запрос на получение статистики авторов для выбранных тем # Запрос на получение статистики авторов для выбранных тем
authors_stats_query = f""" authors_stats_query = f"""
SELECT st.topic, COUNT(DISTINCT sa.author) as authors_count SELECT st.topic, COUNT(DISTINCT sa.author) as authors_count
@ -149,7 +149,6 @@ async def get_topics_with_stats(limit=100, offset=0, community_id=None, by=None)
""" """
comments_stats = {row[0]: row[1] for row in session.execute(text(comments_stats_query))} comments_stats = {row[0]: row[1] for row in session.execute(text(comments_stats_query))}
# Формируем результат с добавлением статистики # Формируем результат с добавлением статистики
result = [] result = []
for topic in topics: for topic in topics:
@ -158,7 +157,7 @@ async def get_topics_with_stats(limit=100, offset=0, community_id=None, by=None)
"shouts": shouts_stats.get(topic.id, 0), "shouts": shouts_stats.get(topic.id, 0),
"followers": followers_stats.get(topic.id, 0), "followers": followers_stats.get(topic.id, 0),
"authors": authors_stats.get(topic.id, 0), "authors": authors_stats.get(topic.id, 0),
"comments": comments_stats.get(topic.id, 0) "comments": comments_stats.get(topic.id, 0),
} }
result.append(topic_dict) result.append(topic_dict)

View File

@ -30,6 +30,7 @@ class ViewedStorage:
Класс для хранения и доступа к данным о просмотрах. Класс для хранения и доступа к данным о просмотрах.
Использует Redis в качестве основного хранилища и Google Analytics для сбора новых данных. Использует Redis в качестве основного хранилища и Google Analytics для сбора новых данных.
""" """
lock = asyncio.Lock() lock = asyncio.Lock()
views_by_shout = {} views_by_shout = {}
shouts_by_topic = {} shouts_by_topic = {}
@ -68,42 +69,42 @@ class ViewedStorage:
async def load_views_from_redis(): async def load_views_from_redis():
"""Загрузка предварительно подсчитанных просмотров из Redis""" """Загрузка предварительно подсчитанных просмотров из Redis"""
self = ViewedStorage self = ViewedStorage
# Подключаемся к Redis если соединение не установлено # Подключаемся к Redis если соединение не установлено
if not redis._client: if not redis._client:
await redis.connect() await redis.connect()
# Получаем список всех ключей migrated_views_* и находим самый последний # Получаем список всех ключей migrated_views_* и находим самый последний
keys = await redis.execute("KEYS", "migrated_views_*") keys = await redis.execute("KEYS", "migrated_views_*")
if not keys: if not keys:
logger.warning(" * No migrated_views keys found in Redis") logger.warning(" * No migrated_views keys found in Redis")
return return
# Фильтруем только ключи timestamp формата (исключаем migrated_views_slugs) # Фильтруем только ключи timestamp формата (исключаем migrated_views_slugs)
timestamp_keys = [k for k in keys if k != "migrated_views_slugs"] timestamp_keys = [k for k in keys if k != "migrated_views_slugs"]
if not timestamp_keys: if not timestamp_keys:
logger.warning(" * No migrated_views timestamp keys found in Redis") logger.warning(" * No migrated_views timestamp keys found in Redis")
return return
# Сортируем по времени создания (в названии ключа) и берем последний # Сортируем по времени создания (в названии ключа) и берем последний
timestamp_keys.sort() timestamp_keys.sort()
latest_key = timestamp_keys[-1] latest_key = timestamp_keys[-1]
self.redis_views_key = latest_key self.redis_views_key = latest_key
# Получаем метку времени создания для установки start_date # Получаем метку времени создания для установки start_date
timestamp = await redis.execute("HGET", latest_key, "_timestamp") timestamp = await redis.execute("HGET", latest_key, "_timestamp")
if timestamp: if timestamp:
self.last_update_timestamp = int(timestamp) self.last_update_timestamp = int(timestamp)
timestamp_dt = datetime.fromtimestamp(int(timestamp)) timestamp_dt = datetime.fromtimestamp(int(timestamp))
self.start_date = timestamp_dt.strftime("%Y-%m-%d") self.start_date = timestamp_dt.strftime("%Y-%m-%d")
# Если данные сегодняшние, считаем их актуальными # Если данные сегодняшние, считаем их актуальными
now_date = datetime.now().strftime("%Y-%m-%d") now_date = datetime.now().strftime("%Y-%m-%d")
if now_date == self.start_date: if now_date == self.start_date:
logger.info(" * Views data is up to date!") logger.info(" * Views data is up to date!")
else: else:
logger.warning(f" * Views data is from {self.start_date}, may need update") logger.warning(f" * Views data is from {self.start_date}, may need update")
# Выводим информацию о количестве загруженных записей # Выводим информацию о количестве загруженных записей
total_entries = await redis.execute("HGET", latest_key, "_total") total_entries = await redis.execute("HGET", latest_key, "_total")
if total_entries: if total_entries:
@ -160,33 +161,33 @@ class ViewedStorage:
async def get_shout(shout_slug="", shout_id=0) -> int: async def get_shout(shout_slug="", shout_id=0) -> int:
""" """
Получение метрики просмотров shout по slug или id. Получение метрики просмотров shout по slug или id.
Args: Args:
shout_slug: Slug публикации shout_slug: Slug публикации
shout_id: ID публикации shout_id: ID публикации
Returns: Returns:
int: Количество просмотров int: Количество просмотров
""" """
self = ViewedStorage self = ViewedStorage
# Получаем данные из Redis для новой схемы хранения # Получаем данные из Redis для новой схемы хранения
if not redis._client: if not redis._client:
await redis.connect() await redis.connect()
fresh_views = self.views_by_shout.get(shout_slug, 0) fresh_views = self.views_by_shout.get(shout_slug, 0)
# Если есть id, пытаемся получить данные из Redis по ключу migrated_views_<timestamp> # Если есть id, пытаемся получить данные из Redis по ключу migrated_views_<timestamp>
if shout_id and self.redis_views_key: if shout_id and self.redis_views_key:
precounted_views = await redis.execute("HGET", self.redis_views_key, str(shout_id)) precounted_views = await redis.execute("HGET", self.redis_views_key, str(shout_id))
if precounted_views: if precounted_views:
return fresh_views + int(precounted_views) return fresh_views + int(precounted_views)
# Если нет id или данных, пытаемся получить по slug из отдельного хеша # Если нет id или данных, пытаемся получить по slug из отдельного хеша
precounted_views = await redis.execute("HGET", "migrated_views_slugs", shout_slug) precounted_views = await redis.execute("HGET", "migrated_views_slugs", shout_slug)
if precounted_views: if precounted_views:
return fresh_views + int(precounted_views) return fresh_views + int(precounted_views)
return fresh_views return fresh_views
@staticmethod @staticmethod
@ -316,4 +317,4 @@ class ViewedStorage:
except Exception as e: except Exception as e:
logger.error(f"Google Analytics API Error: {e}") logger.error(f"Google Analytics API Error: {e}")
return 0 return 0