diff options
| author | Dennis Fink | 2026-08-21 11:08:53 +0200 |
|---|---|---|
| committer | Dennis Fink | 2026-08-21 11:08:53 +0200 |
| commit | 66ad7fefff6edc1d993f5c347430a28d63727988 (patch) | |
| tree | 3fd2d66d0505628480692d5c8ad86131c657be83 /webmentions_ssg | |
| parent | b07b61a354b32637acf84cec2c5b4f8019cb94b7 (diff) | |
| download | webmentions-ssg-66ad7fefff6edc1d993f5c347430a28d63727988.tar.gz webmentions-ssg-66ad7fefff6edc1d993f5c347430a28d63727988.zip | |
feat(sender): add manual Webmention resend action
Add an authenticated action to reopen a sent Webmention revision and
queue it for delivery again from the source detail view.
Clear the previous attempt state while preserving historical
successful-send data, and show pending Webmentions explicitly in the UI.
Diffstat (limited to 'webmentions_ssg')
| -rw-r--r-- | webmentions_ssg/templates/includes/confirmation_modal.html | 2 | ||||
| -rw-r--r-- | webmentions_ssg/templates/received.html | 2 | ||||
| -rw-r--r-- | webmentions_ssg/templates/sent.html | 4 | ||||
| -rw-r--r-- | webmentions_ssg/templates/sent_source.html | 27 | ||||
| -rw-r--r-- | webmentions_ssg/views.py | 58 |
5 files changed, 79 insertions, 14 deletions
diff --git a/webmentions_ssg/templates/includes/confirmation_modal.html b/webmentions_ssg/templates/includes/confirmation_modal.html index 5c0a5a1..e6a4c10 100644 --- a/webmentions_ssg/templates/includes/confirmation_modal.html +++ b/webmentions_ssg/templates/includes/confirmation_modal.html @@ -3,7 +3,7 @@ SPDX-FileCopyrightText: 2026 Dennis Fink <me+coding@dennisfink.me> SPDX-License-Identifier: BSD-3-Clause --> - +{% from "bootstrap5/form.html" import render_form %} <div class="modal fade" id="confirmation-modal" tabindex="-1" diff --git a/webmentions_ssg/templates/received.html b/webmentions_ssg/templates/received.html index 1c5a216..7cfdce8 100644 --- a/webmentions_ssg/templates/received.html +++ b/webmentions_ssg/templates/received.html @@ -3,9 +3,7 @@ SPDX-FileCopyrightText: 2026 Dennis Fink <me+coding@dennisfink.me> SPDX-License-Identifier: BSD-3-Clause --> - {% extends "base.html" %} -{% from "bootstrap5/form.html" import render_form %} {% from "bootstrap5/pagination.html" import render_pagination %} {% block content %} <div class="d-flex justify-content-between align-items-center mb-3"> diff --git a/webmentions_ssg/templates/sent.html b/webmentions_ssg/templates/sent.html index 1d311a0..4ca75ce 100644 --- a/webmentions_ssg/templates/sent.html +++ b/webmentions_ssg/templates/sent.html @@ -3,9 +3,7 @@ SPDX-FileCopyrightText: 2026 Dennis Fink <me+coding@dennisfink.me> SPDX-License-Identifier: BSD-3-Clause --> - {% extends "base.html" %} -{% from "bootstrap5/form.html" import render_form %} {% from "bootstrap5/pagination.html" import render_pagination %} {% block content %} <div class="d-flex justify-content-between align-items-center mb-3"> @@ -50,7 +48,7 @@ SPDX-License-Identifier: BSD-3-Clause {% endif %} <tr class="{{ row_class }}"> <td class="text-nowrap"> - <a href="{{ url_for('root.sent_source', identifier=source.uuid) }}">{{ source.path }}</a> + <a href="{{ url_for('root.sent_source', source_identifier=source.uuid) }}">{{ source.path }}</a> </td> <td class="text-break"> <a href="{{ source.url }}">{{ source.url }}</a> diff --git a/webmentions_ssg/templates/sent_source.html b/webmentions_ssg/templates/sent_source.html index 6bb2ac5..77376b5 100644 --- a/webmentions_ssg/templates/sent_source.html +++ b/webmentions_ssg/templates/sent_source.html @@ -3,7 +3,6 @@ SPDX-FileCopyrightText: 2026 Dennis Fink <me+coding@dennisfink.me> SPDX-License-Identifier: BSD-3-Clause --> - {% extends "base.html" %} {% block content %} <div class="mb-3"> @@ -85,11 +84,15 @@ SPDX-License-Identifier: BSD-3-Clause <th scope="col">Status URL</th> <th scope="col">Last attempted at</th> <th scope="col">Last sent at</th> + <th scope="col">Actions</th> </tr> </thead> <tbody> {% for webmention in webmentions %} - {% if webmention.status.value == "sent" %} + {% if webmention.status is none %} + {% set row_class = "table-info" %} + {% set badge_class = "text-bg-info" %} + {% elif webmention.status.value == "sent" %} {% set row_class = "table-success" %} {% set badge_class = "text-bg-success" %} {% elif webmention.status.value == "unsupported" %} @@ -107,7 +110,9 @@ SPDX-License-Identifier: BSD-3-Clause <a href="{{ webmention.target }}">{{ webmention.target }}</a> </td> <td> - {% if webmention.status is not none %} + {% if webmention.status is none %} + <span class="badge {{ badge_class }}">Pending</span> + {% else %} <span class="badge {{ badge_class }}">{{ webmention.status.value.title() }}</span> {% endif %} </td> @@ -171,12 +176,28 @@ SPDX-License-Identifier: BSD-3-Clause — {% endif %} </td> + <td> + <button type="button" + class="btn btn-sm btn-primary" + data-bs-toggle="modal" + data-bs-target="#confirmation-modal" + data-action="{{ url_for("root.resend_sent_webmention", source_identifier=source.uuid, webmention_identifier=webmention.uuid) }}" + data-title="Resend Webmention?" + data-message="This Webmention will be queued again for delivery to {{ webmention.target }}." + data-confirm-label="Resend" + data-confirm-class="btn-primary">Resend</button> + </td> </tr> {% endfor %} </tbody> </table> </div> + {% include "includes/confirmation_modal.html" %} {% else %} <p class="text-body-secondary">No Webmentions have been discovered for this source.</p> {% endif %} {% endblock %} +{% block scripts %} + {{ super() }} + <script src="{{ url_for('static', filename='js/confirmation-modal.js') }}"></script> +{% endblock %} diff --git a/webmentions_ssg/views.py b/webmentions_ssg/views.py index 3430cea..2235735 100644 --- a/webmentions_ssg/views.py +++ b/webmentions_ssg/views.py @@ -24,9 +24,10 @@ from sqlalchemy.orm import selectinload from . import CSRF, forms from . import DATABASE as db -from .models import ReceivedWebmention, Source, User +from .models import ReceivedWebmention, SentWebmention, Source, User from .tasks.receiver import verify_webmention from .tasks.scanner import manual_scan_sources +from .tasks.sender import send_webmention root_page = Blueprint("root", __name__) @@ -209,19 +210,19 @@ def rescan_sent_sources() -> ResponseReturnValue: return redirect(url_for("root.sent")) -@root_page.route("/sent/<uuid:identifier>") +@root_page.route("/sent/<uuid:source_identifier>") @login_required -def sent_source(identifier: uuid.UUID) -> ResponseReturnValue: +def sent_source(source_identifier: uuid.UUID) -> ResponseReturnValue: """ Display the sent Webmentions associated with a source. - :param identifier: Identifier of the source to display. + :param source_identifier: Identifier of the source to display. :return: Rendered source details page. """ source = db.session.scalar( sa.select(Source) .options(selectinload(Source.sent_webmentions)) - .where(Source.uuid == identifier) + .where(Source.uuid == source_identifier) ) if source is None: return abort(404) @@ -234,9 +235,56 @@ def sent_source(identifier: uuid.UUID) -> ResponseReturnValue: title="Sent Webmentions", source=source, webmentions=webmentions, + action_form=forms.AdminActionForm(), ) +@root_page.post("/sent/<uuid:source_identifier>/<uuid:webmention_identifier>/resend") +@login_required +def resend_sent_webmention( + source_identifier: uuid.UUID, webmention_identifier: uuid.UUID +) -> ResponseReturnValue: + """ + Queue an outgoing Webmention to be sent again. + + The current desired source revision is reopened for processing so the + sender task does not treat the Webmention as already completed. Historical + successful-send fields are preserved, while result fields from the previous + attempt are cleared. + + :param source_identifier: Identifier of the source owning the Webmention. + :param webmention_identifier: Identifier of the sent Webmention to resend. + :return: Redirect response to the source details view. + """ + form = forms.AdminActionForm() + + if not form.validate_on_submit(): + return abort(400) + + webmention = db.session.get(SentWebmention, webmention_identifier) + + if webmention is None or webmention.source_id != source_identifier: + return abort(404) + + # Reopen the existing desired revision instead of inventing a new source + # revision. send_webmention() otherwise exits when this revision has already + # been processed. + webmention.processed_revision = None + webmention.status = None + webmention.failure_reason = None + webmention.endpoint = None + webmention.response_status = None + webmention.status_url = None + + db.session.commit() + + send_webmention(webmention_identifier) + + flash(f"Webmention {webmention.uuid} queued for resend.", "success") + + return redirect(url_for("root.sent_source", identifier=source_identifier)) + + @root_page.post("/endpoint") @CSRF.exempt def endpoint() -> ResponseReturnValue: |
