Add new webhook detail view
This commit is contained in:
parent
94d5935b78
commit
bf94f3f3e4
4 changed files with 214 additions and 11 deletions
|
|
@ -1473,12 +1473,14 @@ def modify_webhook(
|
||||||
old_hook: Annotated[str, Form()],
|
old_hook: Annotated[str, Form()],
|
||||||
new_hook: Annotated[str, Form()],
|
new_hook: Annotated[str, Form()],
|
||||||
reader: Annotated[Reader, Depends(get_reader_dependency)],
|
reader: Annotated[Reader, Depends(get_reader_dependency)],
|
||||||
|
redirect_to: Annotated[str, Form()] = "",
|
||||||
):
|
):
|
||||||
"""Modify a webhook.
|
"""Modify a webhook.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
old_hook: The webhook to modify.
|
old_hook: The webhook to modify.
|
||||||
new_hook: The new webhook.
|
new_hook: The new webhook.
|
||||||
|
redirect_to: Optional redirect URL after the update.
|
||||||
reader: The Reader instance.
|
reader: The Reader instance.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
@ -1515,8 +1517,13 @@ def modify_webhook(
|
||||||
if webhook == old_hook.strip():
|
if webhook == old_hook.strip():
|
||||||
reader.set_tag(feed.url, "webhook", new_hook.strip()) # pyright: ignore[reportArgumentType]
|
reader.set_tag(feed.url, "webhook", new_hook.strip()) # pyright: ignore[reportArgumentType]
|
||||||
|
|
||||||
# Redirect to the webhook page.
|
redirect_url: str = redirect_to.strip() or "/webhooks"
|
||||||
return RedirectResponse(url="/webhooks", status_code=303)
|
if redirect_to:
|
||||||
|
redirect_url = redirect_url.replace(urllib.parse.quote(old_hook.strip()), urllib.parse.quote(new_hook.strip()))
|
||||||
|
redirect_url = redirect_url.replace(old_hook.strip(), new_hook.strip())
|
||||||
|
|
||||||
|
# Redirect to the requested page.
|
||||||
|
return RedirectResponse(url=redirect_url, status_code=303)
|
||||||
|
|
||||||
|
|
||||||
def extract_youtube_video_id(url: str) -> str | None:
|
def extract_youtube_video_id(url: str) -> str | None:
|
||||||
|
|
@ -1577,6 +1584,8 @@ async def get_webhook_entries( # noqa: C901, PLR0914
|
||||||
if not webhook_name:
|
if not webhook_name:
|
||||||
raise HTTPException(status_code=404, detail=f"Webhook not found: {clean_webhook_url}")
|
raise HTTPException(status_code=404, detail=f"Webhook not found: {clean_webhook_url}")
|
||||||
|
|
||||||
|
hook_info: WebhookInfo = get_data_from_hook_url(hook_name=webhook_name, hook_url=clean_webhook_url)
|
||||||
|
|
||||||
# Get all feeds associated with this webhook
|
# Get all feeds associated with this webhook
|
||||||
all_feeds: list[Feed] = list(reader.get_feeds())
|
all_feeds: list[Feed] = list(reader.get_feeds())
|
||||||
webhook_feeds: list[Feed] = []
|
webhook_feeds: list[Feed] = []
|
||||||
|
|
@ -1632,8 +1641,10 @@ async def get_webhook_entries( # noqa: C901, PLR0914
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
"request": request,
|
"request": request,
|
||||||
|
"hook_info": hook_info,
|
||||||
"webhook_name": webhook_name,
|
"webhook_name": webhook_name,
|
||||||
"webhook_url": clean_webhook_url,
|
"webhook_url": clean_webhook_url,
|
||||||
|
"webhook_feeds": webhook_feeds,
|
||||||
"entries": paginated_entries,
|
"entries": paginated_entries,
|
||||||
"html": html,
|
"html": html,
|
||||||
"last_entry": last_entry,
|
"last_entry": last_entry,
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,8 @@
|
||||||
<div class="p-2 mb-3 border border-dark">
|
<div class="p-2 mb-3 border border-dark">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<h2 class="h5 mb-0">
|
<h2 class="h5 mb-0">
|
||||||
<a class="text-muted" href="/webhooks">{{ hook_from_context.name }}</a>
|
<a class="text-muted"
|
||||||
|
href="/webhook_entries?webhook_url={{ hook_from_context.url|encode_url }}">{{ hook_from_context.name }}</a>
|
||||||
</h2>
|
</h2>
|
||||||
<a class="text-muted"
|
<a class="text-muted"
|
||||||
href="/webhook_entries?webhook_url={{ hook_from_context.url|encode_url }}">View Latest Entries</a>
|
href="/webhook_entries?webhook_url={{ hook_from_context.url|encode_url }}">View Latest Entries</a>
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,96 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}
|
{% block title %}
|
||||||
| {{ webhook_name }} - Latest Entries
|
| {{ webhook_name }}
|
||||||
{% endblock title %}
|
{% endblock title %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="card mb-3 border border-dark p-3 text-light">
|
<div class="card mb-3 border border-dark p-3 text-light">
|
||||||
<!-- Webhook Title -->
|
<div class="d-flex flex-column flex-md-row justify-content-between gap-3">
|
||||||
<h2>{{ webhook_name }} - Latest Entries ({{ total_entries }} total from {{ feeds_count }} feeds)</h2>
|
<div>
|
||||||
<!-- Webhook Info -->
|
<h2 class="mb-2">{{ webhook_name }}</h2>
|
||||||
<div class="mt-3">
|
<p class="text-muted mb-1">
|
||||||
<p class="text-muted">
|
{{ total_entries }} total from {{ feeds_count }} feed{{ 's' if feeds_count != 1 else '' }}
|
||||||
<code>{{ webhook_url }}</code>
|
</p>
|
||||||
</p>
|
<p class="text-muted mb-0">
|
||||||
|
<code>{{ webhook_url }}</code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2 align-items-start">
|
||||||
|
<a class="btn btn-outline-light btn-sm" href="/">Back to dashboard</a>
|
||||||
|
<a class="btn btn-outline-info btn-sm" href="/webhooks">All webhooks</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<div class="card border border-dark p-3 text-light h-100">
|
||||||
|
<h3 class="h5">Settings</h3>
|
||||||
|
<ul class="list-unstyled text-muted mb-3">
|
||||||
|
<li>
|
||||||
|
<strong>Custom name:</strong> {{ hook_info.custom_name }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Discord name:</strong> {{ hook_info.name or 'Unavailable' }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Webhook:</strong>
|
||||||
|
<a class="text-muted" href="{{ hook_info.url }}">{{ hook_info.url | replace('https://discord.com/api/webhooks', '') }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<form action="/modify_webhook" method="post" class="row g-3 mb-3">
|
||||||
|
<input type="hidden" name="old_hook" value="{{ webhook_url }}" />
|
||||||
|
<input type="hidden"
|
||||||
|
name="redirect_to"
|
||||||
|
value="/webhook_entries?webhook_url={{ webhook_url|encode_url }}" />
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="new_hook" class="form-label">Modify Webhook</label>
|
||||||
|
<input type="text"
|
||||||
|
name="new_hook"
|
||||||
|
id="new_hook"
|
||||||
|
class="form-control border text-muted bg-dark"
|
||||||
|
placeholder="Enter new webhook URL" />
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Save Webhook URL</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<form action="/delete_webhook" method="post">
|
||||||
|
<input type="hidden" name="webhook_url" value="{{ webhook_url }}" />
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-danger w-100"
|
||||||
|
onclick="return confirm('Are you sure you want to delete this webhook?');">
|
||||||
|
Delete Webhook
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<div class="card border border-dark p-3 text-light h-100">
|
||||||
|
<h3 class="h5">Attached feeds</h3>
|
||||||
|
{% if webhook_feeds %}
|
||||||
|
<ul class="list-group list-unstyled mb-0">
|
||||||
|
{% for feed in webhook_feeds %}
|
||||||
|
<li class="mb-2">
|
||||||
|
<a class="text-muted" href="/feed?feed_url={{ feed.url|encode_url }}">
|
||||||
|
{% if feed.title %}
|
||||||
|
{{ feed.title }}
|
||||||
|
{% else %}
|
||||||
|
{{ feed.url }}
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% if not feed.updates_enabled %}<span class="text-warning">Disabled</span>{% endif %}
|
||||||
|
{% if feed.last_exception %}<span class="text-danger">({{ feed.last_exception.value_str }})</span>{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted mb-0">No feeds are attached to this webhook yet.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{# Rendered HTML content #}
|
{# Rendered HTML content #}
|
||||||
{% if entries %}
|
{% if entries %}
|
||||||
|
<h3 class="h5 text-light">Latest entries</h3>
|
||||||
<pre>{{ html|safe }}</pre>
|
<pre>{{ html|safe }}</pre>
|
||||||
{% if is_show_more_entries_button_visible and last_entry %}
|
{% if is_show_more_entries_button_visible and last_entry %}
|
||||||
<a class="btn btn-dark mt-3"
|
<a class="btn btn-dark mt-3"
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,9 @@ def test_get() -> None:
|
||||||
response: Response = client.get(url="/webhooks")
|
response: Response = client.get(url="/webhooks")
|
||||||
assert response.status_code == 200, f"/webhooks failed: {response.text}"
|
assert response.status_code == 200, f"/webhooks failed: {response.text}"
|
||||||
|
|
||||||
|
response = client.get(url="/webhook_entries", params={"webhook_url": webhook_url})
|
||||||
|
assert response.status_code == 200, f"/webhook_entries failed: {response.text}"
|
||||||
|
|
||||||
response: Response = client.get(url="/whitelist", params={"feed_url": encoded_feed_url(feed_url)})
|
response: Response = client.get(url="/whitelist", params={"feed_url": encoded_feed_url(feed_url)})
|
||||||
assert response.status_code == 200, f"/whitelist failed: {response.text}"
|
assert response.status_code == 200, f"/whitelist failed: {response.text}"
|
||||||
|
|
||||||
|
|
@ -882,6 +885,32 @@ def test_webhook_entries_no_feeds() -> None:
|
||||||
assert "No feeds found" in response.text or "Add feeds" in response.text, "Expected message about no feeds"
|
assert "No feeds found" in response.text or "Add feeds" in response.text, "Expected message about no feeds"
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_entries_no_feeds_still_shows_webhook_settings() -> None:
|
||||||
|
"""The webhook detail view should show settings/actions even with no attached feeds."""
|
||||||
|
client.post(url="/delete_webhook", data={"webhook_url": webhook_url})
|
||||||
|
response: Response = client.post(
|
||||||
|
url="/add_webhook",
|
||||||
|
data={"webhook_name": webhook_name, "webhook_url": webhook_url},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, f"Failed to add webhook: {response.text}"
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
url="/webhook_entries",
|
||||||
|
params={"webhook_url": webhook_url},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200, f"Failed to get /webhook_entries: {response.text}"
|
||||||
|
assert "Settings" in response.text, "Expected settings card on webhook detail view"
|
||||||
|
assert "Modify Webhook" in response.text, "Expected modify form on webhook detail view"
|
||||||
|
assert "Delete Webhook" in response.text, "Expected delete action on webhook detail view"
|
||||||
|
assert "Back to dashboard" in response.text, "Expected dashboard navigation link"
|
||||||
|
assert "All webhooks" in response.text, "Expected all webhooks navigation link"
|
||||||
|
assert f'name="old_hook" value="{webhook_url}"' in response.text, "Expected old_hook hidden input"
|
||||||
|
assert f'value="/webhook_entries?webhook_url={urllib.parse.quote(webhook_url)}"' in response.text, (
|
||||||
|
"Expected modify form to redirect back to the current webhook detail view"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_webhook_entries_with_feeds_no_entries() -> None:
|
def test_webhook_entries_with_feeds_no_entries() -> None:
|
||||||
"""Test webhook_entries endpoint when webhook has feeds but no entries yet."""
|
"""Test webhook_entries endpoint when webhook has feeds but no entries yet."""
|
||||||
# Clean up and create fresh webhook
|
# Clean up and create fresh webhook
|
||||||
|
|
@ -943,6 +972,38 @@ def test_webhook_entries_with_entries() -> None:
|
||||||
assert webhook_name in response.text, "Webhook name not found in response"
|
assert webhook_name in response.text, "Webhook name not found in response"
|
||||||
# Should show entries (the feed has entries)
|
# Should show entries (the feed has entries)
|
||||||
assert "total from" in response.text, "Expected to see entry count"
|
assert "total from" in response.text, "Expected to see entry count"
|
||||||
|
assert "Modify Webhook" in response.text, "Expected webhook settings to be visible"
|
||||||
|
assert "Attached feeds" in response.text, "Expected attached feeds section to be visible"
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_entries_shows_attached_feed_link() -> None:
|
||||||
|
"""The webhook detail view should list attached feeds linking to their feed pages."""
|
||||||
|
client.post(url="/delete_webhook", data={"webhook_url": webhook_url})
|
||||||
|
response: Response = client.post(
|
||||||
|
url="/add_webhook",
|
||||||
|
data={"webhook_name": webhook_name, "webhook_url": webhook_url},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, f"Failed to add webhook: {response.text}"
|
||||||
|
|
||||||
|
client.post(url="/remove", data={"feed_url": feed_url})
|
||||||
|
response = client.post(
|
||||||
|
url="/add",
|
||||||
|
data={"feed_url": feed_url, "webhook_dropdown": webhook_name},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, f"Failed to add feed: {response.text}"
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
url="/webhook_entries",
|
||||||
|
params={"webhook_url": webhook_url},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200, f"Failed to get /webhook_entries: {response.text}"
|
||||||
|
assert f"/feed?feed_url={urllib.parse.quote(feed_url)}" in response.text, (
|
||||||
|
"Expected attached feed to link to its feed detail page"
|
||||||
|
)
|
||||||
|
assert "Latest entries" in response.text, "Expected latest entries heading on webhook detail view"
|
||||||
|
|
||||||
|
client.post(url="/remove", data={"feed_url": feed_url})
|
||||||
|
|
||||||
|
|
||||||
def test_webhook_entries_multiple_feeds() -> None:
|
def test_webhook_entries_multiple_feeds() -> None:
|
||||||
|
|
@ -1047,6 +1108,60 @@ def test_webhook_entries_url_encoding() -> None:
|
||||||
client.post(url="/remove", data={"feed_url": feed_url})
|
client.post(url="/remove", data={"feed_url": feed_url})
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboard_webhook_name_links_to_webhook_detail() -> None:
|
||||||
|
"""Webhook names on the dashboard should open the webhook detail view."""
|
||||||
|
client.post(url="/delete_webhook", data={"webhook_url": webhook_url})
|
||||||
|
response: Response = client.post(
|
||||||
|
url="/add_webhook",
|
||||||
|
data={"webhook_name": webhook_name, "webhook_url": webhook_url},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, f"Failed to add webhook: {response.text}"
|
||||||
|
|
||||||
|
client.post(url="/remove", data={"feed_url": feed_url})
|
||||||
|
response = client.post(url="/add", data={"feed_url": feed_url, "webhook_dropdown": webhook_name})
|
||||||
|
assert response.status_code == 200, f"Failed to add feed: {response.text}"
|
||||||
|
|
||||||
|
response = client.get(url="/")
|
||||||
|
assert response.status_code == 200, f"Failed to get /: {response.text}"
|
||||||
|
|
||||||
|
expected_link = f"/webhook_entries?webhook_url={urllib.parse.quote(webhook_url)}"
|
||||||
|
assert expected_link in response.text, "Expected dashboard webhook link to point to the webhook detail view"
|
||||||
|
|
||||||
|
client.post(url="/remove", data={"feed_url": feed_url})
|
||||||
|
|
||||||
|
|
||||||
|
def test_modify_webhook_redirects_back_to_webhook_detail() -> None:
|
||||||
|
"""Webhook updates from the detail view should redirect back to that view with the new URL."""
|
||||||
|
original_webhook_url = "https://discord.com/api/webhooks/1234567890/abcdefghijklmnopqrstuvwxyz"
|
||||||
|
new_webhook_url = "https://discord.com/api/webhooks/1234567890/updated-token"
|
||||||
|
|
||||||
|
client.post(url="/delete_webhook", data={"webhook_url": original_webhook_url})
|
||||||
|
client.post(url="/delete_webhook", data={"webhook_url": new_webhook_url})
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
url="/add_webhook",
|
||||||
|
data={"webhook_name": webhook_name, "webhook_url": original_webhook_url},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, f"Failed to add webhook: {response.text}"
|
||||||
|
|
||||||
|
no_redirect_client = TestClient(app, follow_redirects=False)
|
||||||
|
response = no_redirect_client.post(
|
||||||
|
url="/modify_webhook",
|
||||||
|
data={
|
||||||
|
"old_hook": original_webhook_url,
|
||||||
|
"new_hook": new_webhook_url,
|
||||||
|
"redirect_to": f"/webhook_entries?webhook_url={urllib.parse.quote(original_webhook_url)}",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 303, f"Expected 303 redirect, got {response.status_code}: {response.text}"
|
||||||
|
assert response.headers["location"] == (f"/webhook_entries?webhook_url={urllib.parse.quote(new_webhook_url)}"), (
|
||||||
|
f"Unexpected redirect location: {response.headers['location']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
client.post(url="/delete_webhook", data={"webhook_url": new_webhook_url})
|
||||||
|
|
||||||
|
|
||||||
def test_reader_dependency_override_is_used() -> None:
|
def test_reader_dependency_override_is_used() -> None:
|
||||||
"""Reader should be injectable and overridable via FastAPI dependency overrides."""
|
"""Reader should be injectable and overridable via FastAPI dependency overrides."""
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue