feat: Add publish and mark-published endpoints with validation
- Add /api/posts/{id}/publish endpoint for API-based publishing
- Add /api/posts/{id}/mark-published endpoint for manual workflow
- Add content length validation before publishing
- Update modal with "Ya lo publiqué" and "Publicar (API)" buttons
- Fix retry_count handling for None values
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -220,6 +220,62 @@ async def reject_post(post_id: int, db: Session = Depends(get_db)):
|
|||||||
return {"message": "Post rechazado", "post_id": post_id}
|
return {"message": "Post rechazado", "post_id": post_id}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{post_id}/publish")
|
||||||
|
async def publish_post_now(post_id: int, db: Session = Depends(get_db)):
|
||||||
|
"""Publicar un post inmediatamente."""
|
||||||
|
from worker.tasks.publish_post import publish_to_platform
|
||||||
|
|
||||||
|
post = db.query(Post).filter(Post.id == post_id).first()
|
||||||
|
if not post:
|
||||||
|
raise HTTPException(status_code=404, detail="Post no encontrado")
|
||||||
|
|
||||||
|
if post.status not in ["draft", "pending_approval", "scheduled", "failed"]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"No se puede publicar un post con status '{post.status}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cambiar estado a publishing
|
||||||
|
post.status = "publishing"
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Encolar tarea de publicación para cada plataforma
|
||||||
|
for platform in post.platforms:
|
||||||
|
publish_to_platform.delay(post.id, platform)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Post enviado a publicación",
|
||||||
|
"post_id": post_id,
|
||||||
|
"platforms": post.platforms
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{post_id}/mark-published")
|
||||||
|
async def mark_post_as_published(post_id: int, db: Session = Depends(get_db)):
|
||||||
|
"""Marcar un post como publicado manualmente (sin usar API)."""
|
||||||
|
post = db.query(Post).filter(Post.id == post_id).first()
|
||||||
|
if not post:
|
||||||
|
raise HTTPException(status_code=404, detail="Post no encontrado")
|
||||||
|
|
||||||
|
if post.status not in ["draft", "pending_approval", "scheduled", "failed"]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail=f"No se puede marcar un post con status '{post.status}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
post.status = "published"
|
||||||
|
post.published_at = datetime.utcnow()
|
||||||
|
post.error_message = None
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Post marcado como publicado",
|
||||||
|
"post_id": post_id
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{post_id}/regenerate")
|
@router.post("/{post_id}/regenerate")
|
||||||
async def regenerate_post(post_id: int, db: Session = Depends(get_db)):
|
async def regenerate_post(post_id: int, db: Session = Depends(get_db)):
|
||||||
"""Regenerar contenido de un post con IA."""
|
"""Regenerar contenido de un post con IA."""
|
||||||
|
|||||||
@@ -60,10 +60,32 @@ def publish_post(self, post_id: int):
|
|||||||
for platform_name in post.platforms:
|
for platform_name in post.platforms:
|
||||||
try:
|
try:
|
||||||
platform = Platform(platform_name)
|
platform = Platform(platform_name)
|
||||||
|
|
||||||
|
# Get platform-specific content
|
||||||
|
platform_content = post.get_content_for_platform(platform_name)
|
||||||
|
|
||||||
|
# Pre-validate content length with descriptive error
|
||||||
|
publisher = publisher_manager.get_publisher(platform)
|
||||||
|
if publisher and hasattr(publisher, 'char_limit'):
|
||||||
|
content_length = len(platform_content)
|
||||||
|
if content_length > publisher.char_limit:
|
||||||
|
error_msg = (
|
||||||
|
f"Contenido excede límite: {content_length}/{publisher.char_limit} "
|
||||||
|
f"caracteres (sobra {content_length - publisher.char_limit})"
|
||||||
|
)
|
||||||
|
logger.error(f"Validation failed for {platform_name}: {error_msg}")
|
||||||
|
results[platform_name] = {
|
||||||
|
"success": False,
|
||||||
|
"post_id": None,
|
||||||
|
"url": None,
|
||||||
|
"error": error_msg
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
|
||||||
result = run_async(
|
result = run_async(
|
||||||
publisher_manager.publish(
|
publisher_manager.publish(
|
||||||
platform=platform,
|
platform=platform,
|
||||||
content=post.content,
|
content=platform_content,
|
||||||
image_path=post.image_url
|
image_path=post.image_url
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -90,10 +112,14 @@ def publish_post(self, post_id: int):
|
|||||||
if any_success:
|
if any_success:
|
||||||
post.status = "published"
|
post.status = "published"
|
||||||
post.published_at = datetime.utcnow()
|
post.published_at = datetime.utcnow()
|
||||||
post.publish_results = results
|
post.platform_post_ids = {
|
||||||
|
k: v.get("post_id") for k, v in results.items() if v.get("success")
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
post.status = "failed"
|
post.status = "failed"
|
||||||
post.publish_results = results
|
# Collect all error messages
|
||||||
|
errors = [f"{k}: {v.get('error')}" for k, v in results.items() if v.get("error")]
|
||||||
|
post.error_message = "\n".join(errors) if errors else "Unknown error"
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|||||||
@@ -184,10 +184,13 @@
|
|||||||
|
|
||||||
${platformContents}
|
${platformContents}
|
||||||
|
|
||||||
<div class="flex gap-2 mt-6 pt-4 border-t border-dark-700">
|
<div class="flex flex-wrap gap-2 mt-6 pt-4 border-t border-dark-700">
|
||||||
${post.status === 'draft' || post.status === 'pending_approval' ?
|
${post.status === 'draft' || post.status === 'pending_approval' || post.status === 'failed' ?
|
||||||
`<button onclick="closePostModal(); publishPost(${post.id})" class="flex-1 bg-green-500/20 text-green-400 px-4 py-2 rounded-lg hover:bg-green-500/30 transition-colors">
|
`<button onclick="markAsPublished(${post.id})" class="flex-1 bg-green-500/20 text-green-400 px-4 py-2 rounded-lg hover:bg-green-500/30 transition-colors">
|
||||||
Publicar ahora
|
✓ Ya lo publiqué
|
||||||
|
</button>
|
||||||
|
<button onclick="closePostModal(); publishPost(${post.id})" class="flex-1 bg-blue-500/20 text-blue-400 px-4 py-2 rounded-lg hover:bg-blue-500/30 transition-colors">
|
||||||
|
🚀 Publicar (API)
|
||||||
</button>` : ''}
|
</button>` : ''}
|
||||||
<button onclick="closePostModal(); deletePost(${post.id})" class="flex-1 bg-red-500/20 text-red-400 px-4 py-2 rounded-lg hover:bg-red-500/30 transition-colors">
|
<button onclick="closePostModal(); deletePost(${post.id})" class="flex-1 bg-red-500/20 text-red-400 px-4 py-2 rounded-lg hover:bg-red-500/30 transition-colors">
|
||||||
Eliminar
|
Eliminar
|
||||||
@@ -263,6 +266,23 @@
|
|||||||
modal.classList.remove('flex');
|
modal.classList.remove('flex');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function markAsPublished(id) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/posts/${id}/mark-published`, { method: 'POST' });
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
closePostModal();
|
||||||
|
showModal('<div class="text-center"><span class="text-4xl mb-4 block">✅</span><p>Marcado como publicado</p></div>');
|
||||||
|
setTimeout(() => { closeModal(); location.reload(); }, 1500);
|
||||||
|
} else {
|
||||||
|
showModal(`<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>${data.detail || 'Error'}</p></div>`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showModal('<div class="text-center"><span class="text-4xl mb-4 block">❌</span><p>Error de conexión</p></div>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
if (!text) return '';
|
if (!text) return '';
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
|
|||||||
@@ -99,6 +99,18 @@ def publish_to_platform(self, post_id: int, platform: str):
|
|||||||
else:
|
else:
|
||||||
# Publicación normal
|
# Publicación normal
|
||||||
content = post.get_content_for_platform(platform)
|
content = post.get_content_for_platform(platform)
|
||||||
|
|
||||||
|
# Validar longitud antes de publicar
|
||||||
|
if hasattr(publisher, 'char_limit') and len(content) > publisher.char_limit:
|
||||||
|
error_msg = (
|
||||||
|
f"Contenido excede límite: {len(content)}/{publisher.char_limit} "
|
||||||
|
f"caracteres (sobran {len(content) - publisher.char_limit})"
|
||||||
|
)
|
||||||
|
post.error_message = f"{platform}: {error_msg}"
|
||||||
|
post.status = "failed"
|
||||||
|
db.commit()
|
||||||
|
return f"Error en {platform}: {error_msg}"
|
||||||
|
|
||||||
result = run_async(
|
result = run_async(
|
||||||
publisher.publish(content, post.image_url)
|
publisher.publish(content, post.image_url)
|
||||||
)
|
)
|
||||||
@@ -125,7 +137,7 @@ def publish_to_platform(self, post_id: int, platform: str):
|
|||||||
else:
|
else:
|
||||||
# Error en publicación
|
# Error en publicación
|
||||||
post.error_message = f"{platform}: {result.error_message}"
|
post.error_message = f"{platform}: {result.error_message}"
|
||||||
post.retry_count += 1
|
post.retry_count = (post.retry_count or 0) + 1
|
||||||
|
|
||||||
if post.retry_count >= 3:
|
if post.retry_count >= 3:
|
||||||
post.status = "failed"
|
post.status = "failed"
|
||||||
|
|||||||
Reference in New Issue
Block a user