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 editor textarea must show only clean editable text.
|
||||||
- The list preview in `/editor` should show the publication preview, including composed hashtags.
|
- 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.
|
- `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
|
## 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
|
FROM raw_posts
|
||||||
WHERE id=ANY($1::bigint[])
|
WHERE id=ANY($1::bigint[])
|
||||||
AND status='storage_ready'
|
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
|
FOR UPDATE
|
||||||
)
|
)
|
||||||
UPDATE raw_posts rp
|
UPDATE raw_posts rp
|
||||||
@@ -2737,8 +2741,19 @@ async def raw_send_to_rewrite(
|
|||||||
qualification_reject_tag=NULL,
|
qualification_reject_tag=NULL,
|
||||||
qualified_at=NOW(),
|
qualified_at=NOW(),
|
||||||
rewrite_status='pending',
|
rewrite_status='pending',
|
||||||
|
rewritten_text=NULL,
|
||||||
rewrite_notes=NULL,
|
rewrite_notes=NULL,
|
||||||
rewrite_batch_id=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,
|
editorial_status=NULL,
|
||||||
updated_at=NOW()
|
updated_at=NOW()
|
||||||
FROM selected
|
FROM selected
|
||||||
|
|||||||
@@ -84,7 +84,11 @@
|
|||||||
<i data-lucide="layers" class="w-4 h-4"></i>
|
<i data-lucide="layers" class="w-4 h-4"></i>
|
||||||
Всего: <span class="font-bold text-white">{{ pagination.total }}</span>
|
Всего: <span class="font-bold text-white">{{ pagination.total }}</span>
|
||||||
</div>
|
</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>
|
<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}))">
|
<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] %}
|
{% for n in [25,50,100,200] %}
|
||||||
@@ -95,10 +99,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card overflow-hidden">
|
<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">
|
<div class="md:hidden divide-y divide-app-border">
|
||||||
{% for p in posts %}
|
{% 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">
|
<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="min-w-0">
|
||||||
<div class="font-mono text-xs text-app-primary">#{{ p.id }}</div>
|
<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>
|
<div class="mt-1 font-semibold text-app-textMain truncate">{{ p.source_name }}</div>
|
||||||
@@ -120,9 +129,25 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<div class="mt-2 text-[10px] text-app-textMuted">{{ p.posted_at_fmt or p.created_at_fmt }}</div>
|
||||||
</a>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="px-4 py-12">
|
<div class="px-4 py-12">
|
||||||
<div class="flex flex-col items-center justify-center text-app-textMuted">
|
<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">
|
<table class="w-full text-left text-sm whitespace-nowrap">
|
||||||
<thead class="bg-app-bg border-b border-app-border">
|
<thead class="bg-app-bg border-b border-app-border">
|
||||||
<tr class="text-app-textMuted font-semibold tracking-wide uppercase text-xs">
|
<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>
|
<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">
|
<tbody class="divide-y divide-app-border">
|
||||||
{% for p in posts %}
|
{% for p in posts %}
|
||||||
<tr class="hover:bg-app-surfaceHover transition-colors group">
|
<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">
|
<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>
|
||||||
<td class="px-4 py-3 whitespace-normal min-w-[200px]">
|
<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>
|
<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 %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 whitespace-normal w-full max-w-sm lg:max-w-xl xl:max-w-2xl">
|
<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">
|
<details class="group">
|
||||||
{{ p.raw_text }}
|
<summary class="list-none cursor-pointer text-xs text-app-textMain/80 leading-relaxed font-sans">
|
||||||
</div>
|
<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 %}
|
{% if p.posted_at_fmt %}
|
||||||
<div class="text-[10px] text-app-textMuted mt-1">Опубликован: {{ p.posted_at_fmt }}</div>
|
<div class="text-[10px] text-app-textMuted mt-1">Опубликован: {{ p.posted_at_fmt }}</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -190,7 +233,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr>
|
<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">
|
<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>
|
<i data-lucide="inbox" class="w-12 h-12 mb-3 text-app-borderFocus"></i>
|
||||||
<p class="text-sm">Нет данных по заданным фильтрам</p>
|
<p class="text-sm">Нет данных по заданным фильтрам</p>
|
||||||
|
|||||||
@@ -661,6 +661,40 @@ class MAXPoster:
|
|||||||
json.dumps(attachments or [], ensure_ascii=False),
|
json.dumps(attachments or [], ensure_ascii=False),
|
||||||
PUBLICATION_STATUS_PUBLISHED,
|
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:
|
async def process_slot(self, slot: dict[str, Any]) -> int:
|
||||||
count = int(slot["count"])
|
count = int(slot["count"])
|
||||||
|
|||||||
@@ -381,6 +381,40 @@ class VKPoster:
|
|||||||
json.dumps(attachments or [], ensure_ascii=False),
|
json.dumps(attachments or [], ensure_ascii=False),
|
||||||
PUBLICATION_STATUS_PUBLISHED,
|
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:
|
async def process_slot(self, slot: dict[str, Any]) -> int:
|
||||||
count = int(slot["count"])
|
count = int(slot["count"])
|
||||||
|
|||||||
Reference in New Issue
Block a user