0.4.9-drafts

This commit is contained in:
2025-02-09 17:18:01 +03:00
parent dce05342df
commit 37a9a284ef
13 changed files with 468 additions and 37 deletions

View File

@@ -11,6 +11,14 @@ from resolvers.author import ( # search_authors,
update_author,
)
from resolvers.community import get_communities_all, get_community
from resolvers.draft import (
create_draft,
delete_draft,
load_drafts,
publish_draft,
unpublish_draft,
update_draft,
)
from resolvers.editor import create_shout, delete_shout, update_shout
from resolvers.feed import (
load_shouts_coauthored,
@@ -113,4 +121,12 @@ __all__ = [
"rate_author",
"get_my_rates_comments",
"get_my_rates_shouts",
# draft
"load_drafts",
"create_draft",
"update_draft",
"delete_draft",
"publish_draft",
"publish_shout",
"unpublish_shout",
]

230
resolvers/draft.py Normal file
View File

@@ -0,0 +1,230 @@
import time
from importlib import invalidate_caches
from sqlalchemy import select
from cache.cache import invalidate_shout_related_cache, invalidate_shouts_cache
from orm.author import Author
from orm.draft import Draft
from orm.shout import Shout
from services.auth import login_required
from services.db import local_session
from services.schema import mutation, query
from utils.logger import root_logger as logger
@query.field("load_drafts")
@login_required
async def load_drafts(_, info):
user_id = info.context.get("user_id")
author_dict = info.context.get("author", {})
author_id = author_dict.get("id")
if not user_id or not author_id:
return {"error": "User ID and author ID are required"}
with local_session() as session:
drafts = session.query(Draft).filter(Draft.authors.any(Author.id == author_id)).all()
return {"drafts": drafts}
@mutation.field("create_draft")
@login_required
async def create_draft(_, info, shout_id: int = 0):
user_id = info.context.get("user_id")
author_dict = info.context.get("author", {})
author_id = author_dict.get("id")
if not user_id or not author_id:
return {"error": "User ID and author ID are required"}
with local_session() as session:
draft = Draft(created_by=author_id)
if shout_id:
draft.shout = shout_id
session.add(draft)
session.commit()
return {"draft": draft}
@mutation.field("update_draft")
@login_required
async def update_draft(_, info, draft_input):
user_id = info.context.get("user_id")
author_dict = info.context.get("author", {})
author_id = author_dict.get("id")
draft_id = draft_input.get("id")
if not user_id or not author_id:
return {"error": "User ID and author ID are required"}
with local_session() as session:
draft = session.query(Draft).filter(Draft.id == draft_id).first()
Draft.update(draft, {**draft_input})
if not draft:
return {"error": "Draft not found"}
draft.updated_at = int(time.time())
session.commit()
return {"draft": draft}
@mutation.field("delete_draft")
@login_required
async def delete_draft(_, info, draft_id: int):
user_id = info.context.get("user_id")
author_dict = info.context.get("author", {})
author_id = author_dict.get("id")
with local_session() as session:
draft = session.query(Draft).filter(Draft.id == draft_id).first()
if not draft:
return {"error": "Draft not found"}
session.delete(draft)
session.commit()
return {"draft": draft}
@mutation.field("publish_draft")
@login_required
async def publish_draft(_, info, draft_id: int):
user_id = info.context.get("user_id")
author_dict = info.context.get("author", {})
author_id = author_dict.get("id")
if not user_id or not author_id:
return {"error": "User ID and author ID are required"}
with local_session() as session:
draft = session.query(Draft).filter(Draft.id == draft_id).first()
if not draft:
return {"error": "Draft not found"}
return publish_shout(None, None, draft.shout, draft)
@mutation.field("unpublish_draft")
@login_required
async def unpublish_draft(_, info, draft_id: int):
user_id = info.context.get("user_id")
author_dict = info.context.get("author", {})
author_id = author_dict.get("id")
if not user_id or not author_id:
return {"error": "User ID and author ID are required"}
with local_session() as session:
draft = session.query(Draft).filter(Draft.id == draft_id).first()
shout_id = draft.shout
unpublish_shout(None, None, shout_id)
@mutation.field("publish_shout")
@login_required
async def publish_shout(_, info, shout_id: int, draft=None):
"""Publish draft as a shout or update existing shout.
Args:
session: SQLAlchemy session to use for database operations
"""
user_id = info.context.get("user_id")
author_dict = info.context.get("author", {})
author_id = author_dict.get("id")
if not user_id or not author_id:
return {"error": "User ID and author ID are required"}
try:
# Use proper SQLAlchemy query
with local_session() as session:
if not draft:
find_draft_stmt = select(Draft).where(Draft.shout == shout_id)
draft = session.execute(find_draft_stmt).scalar_one_or_none()
now = int(time.time())
if not shout:
# Create new shout from draft
shout = Shout(
body=draft.body,
slug=draft.slug,
cover=draft.cover,
cover_caption=draft.cover_caption,
lead=draft.lead,
description=draft.description,
title=draft.title,
subtitle=draft.subtitle,
layout=draft.layout,
media=draft.media,
lang=draft.lang,
seo=draft.seo,
created_by=author_id,
community=draft.community,
authors=draft.authors.copy(), # Create copies of relationships
topics=draft.topics.copy(),
draft=draft.id,
deleted_at=None,
)
else:
# Update existing shout
shout.authors = draft.authors.copy()
shout.topics = draft.topics.copy()
shout.draft = draft.id
shout.created_by = author_id
shout.title = draft.title
shout.subtitle = draft.subtitle
shout.body = draft.body
shout.cover = draft.cover
shout.cover_caption = draft.cover_caption
shout.lead = draft.lead
shout.description = draft.description
shout.layout = draft.layout
shout.media = draft.media
shout.lang = draft.lang
shout.seo = draft.seo
shout.updated_at = now
shout.published_at = now
draft.updated_at = now
draft.published_at = now
session.add(shout)
session.add(draft)
session.commit()
invalidate_shout_related_cache(shout)
invalidate_shouts_cache()
return {"shout": shout}
except Exception as e:
import traceback
logger.error(f"Failed to publish shout: {e}")
logger.error(traceback.format_exc())
session.rollback()
return {"error": "Failed to publish shout"}
@mutation.field("unpublish_shout")
@login_required
async def unpublish_shout(_, info, shout_id: int):
"""Unpublish a shout.
Args:
shout_id: The ID of the shout to unpublish
Returns:
dict: The unpublished shout or an error message
"""
author_dict = info.context.get("author", {})
author_id = author_dict.get("id")
if not author_id:
return {"error": "Author ID is required"}
shout = None
with local_session() as session:
try:
shout = session.query(Shout).filter(Shout.id == shout_id).first()
shout.published_at = None
session.commit()
invalidate_shout_related_cache(shout)
invalidate_shouts_cache()
except Exception:
session.rollback()
return {"error": "Failed to unpublish shout"}
return {"shout": shout}

View File

@@ -7,8 +7,10 @@ from sqlalchemy.sql.functions import coalesce
from cache.cache import cache_author, cache_topic, invalidate_shout_related_cache, invalidate_shouts_cache
from orm.author import Author
from orm.draft import Draft
from orm.shout import Shout, ShoutAuthor, ShoutTopic
from orm.topic import Topic
from resolvers.draft import create_draft, publish_draft
from resolvers.follower import follow, unfollow
from resolvers.stat import get_with_stat
from services.auth import login_required
@@ -20,6 +22,23 @@ from utils.logger import root_logger as logger
async def cache_by_id(entity, entity_id: int, cache_method):
"""Cache an entity by its ID using the provided cache method.
Args:
entity: The SQLAlchemy model class to query
entity_id (int): The ID of the entity to cache
cache_method: The caching function to use
Returns:
dict: The cached entity data if successful, None if entity not found
Example:
>>> async def test_cache():
... author = await cache_by_id(Author, 1, cache_author)
... assert author['id'] == 1
... assert 'name' in author
... return author
"""
caching_query = select(entity).filter(entity.id == entity_id)
result = get_with_stat(caching_query)
if not result or not result[0]:
@@ -34,7 +53,34 @@ async def cache_by_id(entity, entity_id: int, cache_method):
@query.field("get_my_shout")
@login_required
async def get_my_shout(_, info, shout_id: int):
# logger.debug(info)
"""Get a shout by ID if the requesting user has permission to view it.
DEPRECATED: use `load_drafts` instead
Args:
info: GraphQL resolver info containing context
shout_id (int): ID of the shout to retrieve
Returns:
dict: Contains either:
- error (str): Error message if retrieval failed
- shout (Shout): The requested shout if found and accessible
Permissions:
User must be:
- The shout creator
- Listed as an author
- Have editor role
Example:
>>> async def test_get_my_shout():
... context = {'user_id': '123', 'author': {'id': 1}, 'roles': []}
... info = type('Info', (), {'context': context})()
... result = await get_my_shout(None, info, 1)
... assert result['error'] is None
... assert result['shout'].id == 1
... return result
"""
user_id = info.context.get("user_id", "")
author_dict = info.context.get("author", {})
author_id = author_dict.get("id")
@@ -105,8 +151,8 @@ async def get_shouts_drafts(_, info):
return {"shouts": shouts}
@mutation.field("create_shout")
@login_required
# @mutation.field("create_shout")
# @login_required
async def create_shout(_, info, inp):
logger.info(f"Starting create_shout with input: {inp}")
user_id = info.context.get("user_id")
@@ -214,6 +260,27 @@ async def create_shout(_, info, inp):
def patch_main_topic(session, main_topic_slug, shout):
"""Update the main topic for a shout.
Args:
session: SQLAlchemy session
main_topic_slug (str): Slug of the topic to set as main
shout (Shout): The shout to update
Side Effects:
- Updates ShoutTopic.main flags in database
- Only one topic can be main at a time
Example:
>>> def test_patch_main_topic():
... with local_session() as session:
... shout = session.query(Shout).first()
... patch_main_topic(session, 'tech', shout)
... main_topic = session.query(ShoutTopic).filter_by(
... shout=shout.id, main=True).first()
... assert main_topic.topic.slug == 'tech'
... return main_topic
"""
logger.info(f"Starting patch_main_topic for shout#{shout.id} with slug '{main_topic_slug}'")
with session.begin():
@@ -252,6 +319,34 @@ def patch_main_topic(session, main_topic_slug, shout):
def patch_topics(session, shout, topics_input):
"""Update the topics associated with a shout.
Args:
session: SQLAlchemy session
shout (Shout): The shout to update
topics_input (list): List of topic dicts with fields:
- id (int): Topic ID (<0 for new topics)
- slug (str): Topic slug
- title (str): Topic title (for new topics)
Side Effects:
- Creates new topics if needed
- Updates shout-topic associations
- Refreshes shout object with new topics
Example:
>>> def test_patch_topics():
... topics = [
... {'id': -1, 'slug': 'new-topic', 'title': 'New Topic'},
... {'id': 1, 'slug': 'existing-topic'}
... ]
... with local_session() as session:
... shout = session.query(Shout).first()
... patch_topics(session, shout, topics)
... assert len(shout.topics) == 2
... assert any(t.slug == 'new-topic' for t in shout.topics)
... return shout.topics
"""
logger.info(f"Starting patch_topics for shout#{shout.id}")
logger.info(f"Received topics_input: {topics_input}")
@@ -292,8 +387,8 @@ def patch_topics(session, shout, topics_input):
logger.info(f"Final shout topics: {[t.dict() for t in shout.topics]}")
@mutation.field("update_shout")
@login_required
# @mutation.field("update_shout")
# @login_required
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.debug(f"Full shout_input: {shout_input}")
@@ -505,8 +600,8 @@ async def update_shout(_, info, shout_id: int, shout_input=None, publish=False):
return {"error": "cant update shout"}
@mutation.field("delete_shout")
@login_required
# @mutation.field("delete_shout")
# @login_required
async def delete_shout(_, info, shout_id: int):
user_id = info.context.get("user_id")
roles = info.context.get("roles", [])