Improve raw rewrite flow and publication status sync
This commit is contained in:
@@ -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'
|
||||
);
|
||||
@@ -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 }}
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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"])
|
||||
|
||||
Reference in New Issue
Block a user