diff options
| author | Dennis Fink | 2026-08-21 13:13:23 +0200 |
|---|---|---|
| committer | Dennis Fink | 2026-08-21 13:13:23 +0200 |
| commit | 75d22f618b22de6d26ed66bd9a15bf7a66e97af9 (patch) | |
| tree | be4b29d9c761b79d6ac476adda00dfb9f31b415e | |
| parent | 66ad7fefff6edc1d993f5c347430a28d63727988 (diff) | |
| download | webmentions-ssg-75d22f618b22de6d26ed66bd9a15bf7a66e97af9.tar.gz webmentions-ssg-75d22f618b22de6d26ed66bd9a15bf7a66e97af9.zip | |
feat(sender): paginate sent webmention details
Load sent Webmentions with database-backed pagination instead of eagerly
loading the full relationship for each source.
Condense delivery, revision, and activity metadata in the source detail
table and add pagination controls for large Webmention sets.
| -rw-r--r-- | webmentions_ssg/models.py | 2 | ||||
| -rw-r--r-- | webmentions_ssg/templates/sent_source.html | 220 | ||||
| -rw-r--r-- | webmentions_ssg/views.py | 13 |
3 files changed, 123 insertions, 112 deletions
diff --git a/webmentions_ssg/models.py b/webmentions_ssg/models.py index 5d22395..73805de 100644 --- a/webmentions_ssg/models.py +++ b/webmentions_ssg/models.py @@ -167,7 +167,7 @@ class Source(Base): ) sent_webmentions: Mapped[list[SentWebmention]] = relationship( - back_populates="source", cascade="all, delete-orphan", lazy="selectin" + back_populates="source", cascade="all, delete-orphan" ) def __repr__(self) -> str: diff --git a/webmentions_ssg/templates/sent_source.html b/webmentions_ssg/templates/sent_source.html index 77376b5..db8f898 100644 --- a/webmentions_ssg/templates/sent_source.html +++ b/webmentions_ssg/templates/sent_source.html @@ -4,6 +4,7 @@ SPDX-FileCopyrightText: 2026 Dennis Fink <me+coding@dennisfink.me> SPDX-License-Identifier: BSD-3-Clause --> {% extends "base.html" %} +{% from "bootstrap5/pagination.html" import render_pagination %} {% block content %} <div class="mb-3"> <a href="{{ url_for("root.sent") }}">← Back to sources</a> @@ -65,100 +66,102 @@ SPDX-License-Identifier: BSD-3-Clause </div> <div class="d-flex justify-content-between align-items-start mb-3"> <h2 class="mb-0">Sent Webmentions</h2> - <span class="text-body-secondary">{{ webmentions | length }} webmentions</span> + <span class="text-body-secondary">{{ webmentions.item | length }} webmentions</span> </div> - {% if webmentions %} - <div class="table-responsive"> - <table class="table table-bordered table-striped table-hover align-middle"> - <thead> - <tr> - <th scope="col">Target</th> - <th scope="col">Status</th> - <th scope="col">Active</th> - <th scope="col">Desired revision</th> - <th scope="col">Processed revision</th> - <th scope="col">Sent revision</th> - <th scope="col">Failure Reason</th> - <th scope="col">Endpoint</th> - <th scope="col">Response Status</th> - <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 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" %} - {% set row_class = "table-warning" %} - {% set badge_class = "text-bg-warning" %} - {% elif webmention.status.value == "failed" %} - {% set row_class = "table-danger" %} - {% set badge_class = "text-bg-danger" %} - {% else %} - {% set row_class = "table-info" %} - {% set badge_class = "text-bg-info" %} - {% endif %} - <tr class="{{ row_class }}"> - <td class="text-break"> - <a href="{{ webmention.target }}">{{ webmention.target }}</a> - </td> - <td> - {% 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> - <td> - {% if webmention.active %} - <span class="badge text-bg-success">Active</span> - {% else %} - <span class="badge text-bg-danger">Removed</span> - {% endif %} - </td> - <td>{{ webmention.desired_revision }}</td> - <td> + {% if webmentions.items %} + <table class="table table-bordered table-striped table-hover align-middle"> + <thead> + <tr> + <th scope="col" class="w-25">Target</th> + <th scope="col">Status</th> + <th scope="col">Active</th> + <th scope="col">Revisions</th> + <th scope="col">Delivery</th> + <th scope="col">Last activity</th> + <th scope="col">Actions</th> + </tr> + </thead> + <tbody> + {% for webmention in webmentions %} + {% 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" %} + {% set row_class = "table-warning" %} + {% set badge_class = "text-bg-warning" %} + {% elif webmention.status.value == "failed" %} + {% set row_class = "table-danger" %} + {% set badge_class = "text-bg-danger" %} + {% else %} + {% set row_class = "table-info" %} + {% set badge_class = "text-bg-info" %} + {% endif %} + <tr class="{{ row_class }}"> + <td class="text-break"> + <a href="{{ webmention.target }}">{{ webmention.target }}</a> + </td> + <td> + {% if webmention.status is none %} + <span class="badge {{ badge_class }}">Pending</span> + {% else %} + <span class="badge {{ badge_class }}">{{ webmention.status.value.title() }}</span> + {% endif %} + {% if webmention.failure_reason is not none %} + <div class="small mt-2">{{ webmention.failure_reason }}</div> + {% endif %} + </td> + <td> + {% if webmention.active %} + <span class="badge text-bg-success">Active</span> + {% else %} + <span class="badge text-bg-danger">Removed</span> + {% endif %} + </td> + <td class="small text-nowrap"> + <div> + <strong>Desired:</strong> + {{ webmention.desired_revision }} + </div> + <div> + <strong>Processed:</strong> {{ webmention.processed_revision if webmention.processed_revision is not none else "—" }} - </td> - <td> + </div> + <div> + <strong>Sent:</strong> {{ webmention.sent_revision if webmention.sent_revision is not none else "—" }} - </td> - <td> - {{ webmention.failure_reason - if webmention.failure_reason is not none - else "—" }} - </td> - <td class="text-break"> - {% if webmention.endpoint is not none %} - <a href="{{ webmention.endpoint }}">{{ webmention.endpoint }}</a> - {% else %} - — - {% endif %} - </td> - <td> - {{ webmention.response_status - if webmention.response_status is not none - else "—" }} - </td> - <td class="text-break"> - {% if webmention.status_url is not none %} - <a href="{{ webmention.status_url }}">{{ webmention.status_url }}</a> - {% else %} - — - {% endif %} - </td> - <td> + </div> + </td> + <td class="small"> + {% if webmention.endpoint is not none %} + <div class="mb-1"> + <strong>Endpoint:</strong> + <a href="{{ webmention.endpoint }}" class="text-break">{{ webmention.endpoint }}</a> + </div> + {% endif %} + {% if webmention.response_status is not none %} + <div class="mb-1"> + <strong>HTTP:</strong> + {{ webmention.response_status }} + </div> + {% endif %} + {% if webmention.status_url is not none %} + <div> + <strong>Status:</strong> + <a href="{{ webmention.status_url }}" class="text-break">{{ webmention.status_url }}</a> + </div> + {% endif %} + </td> + <td class="small text-nowrap"> + <div class="mb-1"> + <strong>Attempted:</strong> + <br> {% if webmention.last_attempted_at is not none %} <time datetime="{{ webmention.last_attempted_at.isoformat() }}"> {{ webmention.last_attempted_at.strftime("%Y-%m-%d %H:%M:%S") }} @@ -166,8 +169,10 @@ SPDX-License-Identifier: BSD-3-Clause {% else %} — {% endif %} - </td> - <td> + </div> + <div> + <strong>Sent:</strong> + <br> {% if webmention.last_sent_at is not none %} <time datetime="{{ webmention.last_sent_at.isoformat() }}"> {{ webmention.last_sent_at.strftime("%Y-%m-%d %H:%M:%S") }} @@ -175,23 +180,26 @@ SPDX-License-Identifier: BSD-3-Clause {% else %} — {% 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> + </div> + </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> + {% if webmentions.pages > 1 %} + <div class="mt-3">{{ render_pagination(webmentions) }}</div> + {% endif %} {% include "includes/confirmation_modal.html" %} {% else %} <p class="text-body-secondary">No Webmentions have been discovered for this source.</p> diff --git a/webmentions_ssg/views.py b/webmentions_ssg/views.py index 2235735..543a44c 100644 --- a/webmentions_ssg/views.py +++ b/webmentions_ssg/views.py @@ -220,16 +220,19 @@ def sent_source(source_identifier: uuid.UUID) -> ResponseReturnValue: :return: Rendered source details page. """ source = db.session.scalar( - sa.select(Source) - .options(selectinload(Source.sent_webmentions)) - .where(Source.uuid == source_identifier) + sa.select(Source).where(Source.uuid == source_identifier) ) + if source is None: return abort(404) - webmentions = sorted( - source.sent_webmentions, key=lambda webmention: webmention.target + webmentions = db.paginate( + sa.select(SentWebmention) + .where(SentWebmention.source_id == source.uuid) + .order_by(SentWebmention.target, SentWebmention.uuid), + per_page=25, ) + return render_template( "sent_source.html", title="Sent Webmentions", |
