Improve raw rewrite flow and publication status sync

This commit is contained in:
Your Name
2026-08-05 14:20:04 +05:00
parent 8eadddba1f
commit d65d1498a9
6 changed files with 174 additions and 10 deletions
+5
View File
@@ -100,6 +100,11 @@ canonical git repository as FN-8. Code work for both projects must happen in
- The editor textarea must show only clean editable text.
- The list preview in `/editor` should show the publication preview, including composed hashtags.
- `build_publication_text()` in `src/vk_parser_app/text_utils.py` is display/composition helper only. Do not use it before writing `final_text` to DB.
- `/raw` has a manual checkbox action to send rejected posts back to rewrite.
It resets stale rewrite/final fields and sets `rewrite_status='pending'`.
- VK/MAX successful publications also sync `raw_posts.publication_status` and
`editorial_status` to `published`; this keeps VK-only projects from leaving
posts forever in the editor's accepted tab.
## Recent Hotfix: Hashtags
@@ -0,0 +1,33 @@
UPDATE raw_posts rp
SET publication_status='published',
editorial_status='published',
publication_error=NULL,
published_at=COALESCE(rp.published_at, pp.published_at, NOW()),
updated_at=NOW()
FROM (
SELECT raw_post_id, MIN(published_at) AS published_at
FROM post_publications
WHERE status='published'
GROUP BY raw_post_id
) pp
WHERE rp.id=pp.raw_post_id
AND COALESCE(rp.editorial_status, 'review') IN ('accepted', 'publish_failed');
UPDATE raw_posts rp
SET publication_status='publish_failed',
editorial_status='publish_failed',
publication_error=COALESCE(pp.error, rp.publication_error),
updated_at=NOW()
FROM (
SELECT DISTINCT ON (raw_post_id) raw_post_id, error
FROM post_publications
WHERE status='publish_failed'
ORDER BY raw_post_id, updated_at DESC NULLS LAST, id DESC
) pp
WHERE rp.id=pp.raw_post_id
AND COALESCE(rp.editorial_status, 'review')='accepted'
AND NOT EXISTS (
SELECT 1
FROM post_publications ok
WHERE ok.raw_post_id=rp.id AND ok.status='published'
);
+16 -1
View File
@@ -2726,7 +2726,11 @@ async def raw_send_to_rewrite(
FROM raw_posts
WHERE id=ANY($1::bigint[])
AND status='storage_ready'
AND COALESCE(rewrite_status, 'pending') IN ('pending', 'failed')
AND (
COALESCE(rewrite_status, 'pending') IN ('pending', 'failed')
OR COALESCE(qualification_status, 'pending')='rejected'
OR COALESCE(editorial_status, 'review')='rejected'
)
FOR UPDATE
)
UPDATE raw_posts rp
@@ -2737,8 +2741,19 @@ async def raw_send_to_rewrite(
qualification_reject_tag=NULL,
qualified_at=NOW(),
rewrite_status='pending',
rewritten_text=NULL,
rewrite_notes=NULL,
rewrite_batch_id=NULL,
rewrite_category=NULL,
rewrite_category_tag=NULL,
rewrite_source_tag=NULL,
rewrite_category_id=NULL,
final_text=NULL,
final_category=NULL,
final_category_tag=NULL,
final_source_tag=NULL,
final_category_id=NULL,
editor_notes=NULL,
editorial_status=NULL,
updated_at=NOW()
FROM selected
@@ -84,7 +84,11 @@
<i data-lucide="layers" class="w-4 h-4"></i>
Всего: <span class="font-bold text-white">{{ pagination.total }}</span>
</div>
<div class="flex items-center gap-2">
<div class="flex flex-wrap items-center gap-2">
<button class="btn btn-primary btn-sm" type="submit" form="raw-rewrite-form">
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
Отправить на рерайт
</button>
<span class="text-sm text-app-textMuted">На странице:</span>
<select name="per_page" class="select select-sm w-auto py-1 pr-8" form="filter-form" onchange="document.getElementById('filter-form').dispatchEvent(new Event('submit', {cancelable: true}))">
{% for n in [25,50,100,200] %}
@@ -95,10 +99,15 @@
</div>
<div class="card overflow-hidden">
<form id="raw-rewrite-form" method="post" action="/raw/send-to-rewrite">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<input type="hidden" name="next_url" value="{{ request.url.path }}{% if request.url.query %}?{{ request.url.query }}{% endif %}">
</form>
<div class="md:hidden divide-y divide-app-border">
{% for p in posts %}
<a href="/raw/{{ p.id }}" class="block p-4 hover:bg-app-surfaceHover transition-colors">
<div class="block p-4 hover:bg-app-surfaceHover transition-colors">
<div class="flex items-start justify-between gap-3">
<input type="checkbox" name="raw_post_id" value="{{ p.id }}" form="raw-rewrite-form" class="checkbox mt-1" aria-label="Выбрать raw #{{ p.id }}">
<div class="min-w-0">
<div class="font-mono text-xs text-app-primary">#{{ p.id }}</div>
<div class="mt-1 font-semibold text-app-textMain truncate">{{ p.source_name }}</div>
@@ -120,9 +129,25 @@
{% endif %}
</div>
</div>
<div class="mt-3 text-xs text-app-textMain/80 leading-relaxed line-clamp-3">{{ p.raw_text }}</div>
<details class="mt-3 group">
<summary class="list-none cursor-pointer text-xs text-app-textMain/80 leading-relaxed">
<span class="block group-open:hidden line-clamp-3">{{ p.raw_text }}</span>
<span class="hidden group-open:block whitespace-pre-wrap">{{ p.raw_text }}</span>
</summary>
{% if p.media_items %}
<div class="mt-3 grid grid-cols-2 gap-2">
{% for m in p.media_items %}
{% if m.type == "photo" and m.url %}
<img src="{{ m.url }}" alt="" class="w-full rounded border border-app-border object-cover">
{% elif m.url %}
<a href="{{ m.url }}" target="_blank" class="btn btn-surface btn-sm">Видео {{ loop.index }}</a>
{% endif %}
{% endfor %}
</div>
{% endif %}
</details>
<div class="mt-2 text-[10px] text-app-textMuted">{{ p.posted_at_fmt or p.created_at_fmt }}</div>
</a>
</div>
{% else %}
<div class="px-4 py-12">
<div class="flex flex-col items-center justify-center text-app-textMuted">
@@ -136,6 +161,7 @@
<table class="w-full text-left text-sm whitespace-nowrap">
<thead class="bg-app-bg border-b border-app-border">
<tr class="text-app-textMuted font-semibold tracking-wide uppercase text-xs">
<th class="px-4 py-3 w-10"></th>
<th class="px-4 py-3">#</th>
<th class="px-4 py-3">Источник</th>
<th class="px-4 py-3">Этап</th>
@@ -146,8 +172,11 @@
<tbody class="divide-y divide-app-border">
{% for p in posts %}
<tr class="hover:bg-app-surfaceHover transition-colors group">
<td class="px-4 py-3 align-top">
<input type="checkbox" name="raw_post_id" value="{{ p.id }}" form="raw-rewrite-form" class="checkbox" aria-label="Выбрать raw #{{ p.id }}">
</td>
<td class="px-4 py-3 font-mono text-xs">
<a href="/raw/{{ p.id }}" class="text-app-primary hover:text-white transition-colors">#{{ p.id }}</a>
<span class="text-app-primary">#{{ p.id }}</span>
</td>
<td class="px-4 py-3 whitespace-normal min-w-[200px]">
<a href="{{ p.original_url }}" target="_blank" class="font-semibold text-app-textMain hover:text-app-primary transition-colors">{{ p.source_name }}</a>
@@ -171,9 +200,23 @@
{% endif %}
</td>
<td class="px-4 py-3 whitespace-normal w-full max-w-sm lg:max-w-xl xl:max-w-2xl">
<div class="text-xs text-app-textMain/80 leading-relaxed font-sans line-clamp-3">
{{ p.raw_text }}
<details class="group">
<summary class="list-none cursor-pointer text-xs text-app-textMain/80 leading-relaxed font-sans">
<span class="block group-open:hidden line-clamp-3">{{ p.raw_text }}</span>
<span class="hidden group-open:block whitespace-pre-wrap">{{ p.raw_text }}</span>
</summary>
{% if p.media_items %}
<div class="mt-3 flex flex-wrap gap-2">
{% for m in p.media_items %}
{% if m.type == "photo" and m.url %}
<img src="{{ m.url }}" alt="" class="h-28 w-28 rounded border border-app-border object-cover">
{% elif m.url %}
<a href="{{ m.url }}" target="_blank" class="btn btn-surface btn-sm">Видео {{ loop.index }}</a>
{% endif %}
{% endfor %}
</div>
{% endif %}
</details>
{% if p.posted_at_fmt %}
<div class="text-[10px] text-app-textMuted mt-1">Опубликован: {{ p.posted_at_fmt }}</div>
{% else %}
@@ -190,7 +233,7 @@
</tr>
{% else %}
<tr>
<td colspan="5" class="px-4 py-12">
<td colspan="6" class="px-4 py-12">
<div class="flex flex-col items-center justify-center text-app-textMuted">
<i data-lucide="inbox" class="w-12 h-12 mb-3 text-app-borderFocus"></i>
<p class="text-sm">Нет данных по заданным фильтрам</p>
+34
View File
@@ -661,6 +661,40 @@ class MAXPoster:
json.dumps(attachments or [], ensure_ascii=False),
PUBLICATION_STATUS_PUBLISHED,
)
if status == PUBLICATION_STATUS_PUBLISHED:
await self.pool.execute(
"""
UPDATE raw_posts
SET publication_status=$2,
editorial_status='published',
publication_error=NULL,
published_at=COALESCE(published_at, NOW()),
updated_at=NOW()
WHERE id=$1
""",
post_id,
PUBLICATION_STATUS_PUBLISHED,
)
elif status == PUBLICATION_STATUS_FAILED:
await self.pool.execute(
"""
UPDATE raw_posts rp
SET publication_status=$2,
editorial_status='publish_failed',
publication_error=$3,
updated_at=NOW()
WHERE rp.id=$1
AND NOT EXISTS (
SELECT 1
FROM post_publications pp
WHERE pp.raw_post_id=rp.id AND pp.status=$4
)
""",
post_id,
PUBLICATION_STATUS_FAILED,
error[:1000] if error else None,
PUBLICATION_STATUS_PUBLISHED,
)
async def process_slot(self, slot: dict[str, Any]) -> int:
count = int(slot["count"])
+34
View File
@@ -381,6 +381,40 @@ class VKPoster:
json.dumps(attachments or [], ensure_ascii=False),
PUBLICATION_STATUS_PUBLISHED,
)
if status == PUBLICATION_STATUS_PUBLISHED:
await self.pool.execute(
"""
UPDATE raw_posts
SET publication_status=$2,
editorial_status='published',
publication_error=NULL,
published_at=COALESCE(published_at, NOW()),
updated_at=NOW()
WHERE id=$1
""",
post_id,
PUBLICATION_STATUS_PUBLISHED,
)
elif status == PUBLICATION_STATUS_FAILED:
await self.pool.execute(
"""
UPDATE raw_posts rp
SET publication_status=$2,
editorial_status='publish_failed',
publication_error=$3,
updated_at=NOW()
WHERE rp.id=$1
AND NOT EXISTS (
SELECT 1
FROM post_publications pp
WHERE pp.raw_post_id=rp.id AND pp.status=$4
)
""",
post_id,
PUBLICATION_STATUS_FAILED,
error[:1000] if error else None,
PUBLICATION_STATUS_PUBLISHED,
)
async def process_slot(self, slot: dict[str, Any]) -> int:
count = int(slot["count"])