This commit is contained in:
parent
bd129efde6
commit
eb216a5f36
|
@ -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)
|
||||||
|
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
@ -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", [])
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user