Forum: one SEOSoft forum at forum.seosoft.dk, SQLInsight Pro as a category
Some checks are pending
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Blocked by required conditions
CI / Build og unit tests (push) Successful in 1m0s
deploy community / deploy (push) Successful in 0s
deploy hub / deploy (push) Successful in 23s
CI / MSI-installer (push) Successful in 36s
Some checks are pending
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Blocked by required conditions
CI / Build og unit tests (push) Successful in 1m0s
deploy community / deploy (push) Successful in 0s
deploy hub / deploy (push) Successful in 23s
CI / MSI-installer (push) Successful in 36s
The forum is shared by SEOSoft's products, so SQLInsight Pro becomes the
top-level category "sqlinsight" with the existing categories beneath it.
- render.py/install.sh: default hostname forum.seosoft.dk
- configure.py: forum-wide settings ("SEOSoft Forum") kept apart from the
SQLInsight Pro category tree; subcategories found via /site.json; the
welcome topic is pinned in the category, not on the shared front page
- Community site: ForumCategory option. The front page lists only that
category's topics (c/<slug>/l/latest.json); forum links go to the
category, topic links stay at the forum root
- Web: default ForumUrl https://forum.seosoft.dk/c/sqlinsight. The old
forum.sqlinsight.seosoft.dk stays as a redirect for installed versions
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c520919a76
commit
ce49866d2b
16 changed files with 134 additions and 67 deletions
|
|
@ -1,5 +1,5 @@
|
|||
# =============================================================================
|
||||
# Installér eller genopbyg forummet (Discourse, forum.sqlinsight.seosoft.dk) på appserveren.
|
||||
# Installér eller genopbyg SEOSofts fælles forum (Discourse, forum.seosoft.dk) på appserveren. SQLInsight Pro er én kategori.
|
||||
#
|
||||
# Kun manuelt (Actions → deploy forum → Run workflow): en rebuild tager 10-15 minutter, og forummet er nede imens.
|
||||
# Discourse opdateres ellers fra /admin/upgrade.
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ SQLInsight Pro vil gøre det anderledes:
|
|||
| Pris | Gratis | Abonnement | Abonnement |
|
||||
| Al overvågning og analyse | ✅ | ✅ | ✅ |
|
||||
| Opdateringer af værktøj og regelpakker | ✅ | ✅ | ✅ |
|
||||
| Community support ([forum](https://forum.sqlinsight.seosoft.dk/)) | ✅ | ✅ | ✅ |
|
||||
| Community support ([forum](https://forum.seosoft.dk/c/sqlinsight)) | ✅ | ✅ | ✅ |
|
||||
| Prioriteret mailsupport med svartidsgaranti | – | ✅ | ✅ |
|
||||
| Telefonsupport | – | – | ✅ |
|
||||
| Findings-sync til SQLInsight Hub (proaktiv opfølgning) | – | Valgfrit | ✅ |
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ services:
|
|||
environment:
|
||||
SQLInsight__Community__Feed: /feed
|
||||
SQLInsight__Community__DownloadUrl: https://hub.sqlinsight.seosoft.dk/updates/
|
||||
SQLInsight__Community__ForumUrl: https://forum.sqlinsight.seosoft.dk/
|
||||
# SEOSofts fælles forum; forsiden viser kun SQLInsight-kategorien.
|
||||
SQLInsight__Community__ForumUrl: https://forum.seosoft.dk/
|
||||
SQLInsight__Community__ForumCategory: sqlinsight
|
||||
volumes:
|
||||
- /opt/sqlinsight-hub/data/feed:/feed:ro
|
||||
ports:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
## SQLInsight Pro community forum (forum.sqlinsight.seosoft.dk): Discourse with the official discourse_docker launcher.
|
||||
## SEOSoft forum (forum.seosoft.dk), shared by SEOSoft's products (SQLInsight Pro is one category): Discourse with the official discourse_docker launcher.
|
||||
## See docs/community.md.
|
||||
##
|
||||
## This is a template: the double-braced names are filled in by render.py from environment variables (Forgejo secrets in
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Sets up the SQLInsight Pro forum on a fresh Discourse through its API. Safe to run again: it only changes what
|
||||
differs from the wanted state.
|
||||
"""Sets up SEOSoft's shared forum and SQLInsight Pro's category in it through the Discourse API. Safe to run again:
|
||||
it only changes what differs from the wanted state.
|
||||
|
||||
DISCOURSE_URL=https://forum.sqlinsight.seosoft.dk DISCOURSE_API_KEY=... python3 deploy/forum/configure.py
|
||||
DISCOURSE_URL=https://forum.seosoft.dk DISCOURSE_API_KEY=... python3 deploy/forum/configure.py
|
||||
|
||||
Create the key inside the container: ./launcher enter app then
|
||||
cd /var/www/discourse && su discourse -c 'RAILS_ENV=production bundle exec rake api_key:create_master[forum-setup]'
|
||||
Optional: COMMUNITY_URL (the community site, linked from the forum), CONTACT_EMAIL.
|
||||
Optional: COMMUNITY_URL (SQLInsight Pro's site, linked from its category), CONTACT_EMAIL.
|
||||
|
||||
The forum is shared by SEOSoft's products. SQLInsight Pro is one top-level category ("sqlinsight") with its own
|
||||
subcategories; other products get their own top-level categories and are not touched here.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
|
|
@ -21,22 +24,26 @@ KEY = os.environ.get("DISCOURSE_API_KEY", "")
|
|||
COMMUNITY = os.environ.get("COMMUNITY_URL", "https://sqlinsight.seosoft.dk").rstrip("/")
|
||||
CONTACT = os.environ.get("CONTACT_EMAIL", "")
|
||||
|
||||
# The whole forum (shared by all products).
|
||||
SETTINGS = {
|
||||
"title": "SQLInsight Pro Forum",
|
||||
"site_description": "Spørgsmål, svar og erfaringer om SQL Server-overvågning med SQLInsight Pro.",
|
||||
"short_site_description": "SQL Server-overvågning med dyb ekspertise",
|
||||
"title": "SEOSoft Forum",
|
||||
"site_description": "Spørgsmål, svar og erfaringer om SEOSofts produkter.",
|
||||
"short_site_description": "Community for SEOSofts produkter",
|
||||
"company_name": "SEOSoft",
|
||||
"default_locale": "da",
|
||||
"login_required": "false",
|
||||
"solved_enabled": "true",
|
||||
"tagging_enabled": "true",
|
||||
"top_menu": "latest|categories|new|top",
|
||||
"contact_url": f"{COMMUNITY}/support",
|
||||
"top_menu": "categories|latest|new|top",
|
||||
}
|
||||
if CONTACT:
|
||||
SETTINGS["contact_email"] = CONTACT
|
||||
|
||||
# name, slug, colour, description, accepted answers (solved), only staff may start topics
|
||||
# SQLInsight Pro's top-level category: name, slug, colour, description
|
||||
PRODUCT = ("SQLInsight Pro", "sqlinsight", "0E7490",
|
||||
f"Gratis SQL Server-overvågning med dyb ekspertise. Dokumentation og download: {COMMUNITY}")
|
||||
|
||||
# Its subcategories: name, slug, colour, description, accepted answers (solved), only staff may start topics
|
||||
CATEGORIES = [
|
||||
("Annonceringer", "annonceringer", "0E7490",
|
||||
"Nye versioner, regelpakker og vigtige SQL Server-opdateringer. Kun SEOSoft opretter emner her; alle kan svare.", False, True),
|
||||
|
|
@ -52,13 +59,13 @@ CATEGORIES = [
|
|||
"Hvad mangler SQLInsight Pro? Fejl oprettes som issues på Forgejo.", False, False),
|
||||
]
|
||||
|
||||
WELCOME_TITLE = "Velkommen til SQLInsight Pro-forummet"
|
||||
WELCOME_TITLE = "Velkommen til SQLInsight Pro på SEOSoft Forum"
|
||||
WELCOME = f"""SQLInsight Pro er gratis SQL Server-overvågning med dyb ekspertise: hvert fund har en forklaring, målingerne bag og en konkret anbefaling.
|
||||
|
||||
**Her kan du**
|
||||
- stille spørgsmål om installation, fund og tuning i [Spørgsmål og svar](/c/spoergsmaal)
|
||||
- dele erfaringer og foreslå nye [regler](/c/regler)
|
||||
- følge nye versioner og regelpakker i [Annonceringer](/c/annonceringer)
|
||||
- stille spørgsmål om installation, fund og tuning i [Spørgsmål og svar](/c/sqlinsight/spoergsmaal)
|
||||
- dele erfaringer og foreslå nye [regler](/c/sqlinsight/regler)
|
||||
- følge nye versioner og regelpakker i [Annonceringer](/c/sqlinsight/annonceringer)
|
||||
|
||||
**Gode råd til et hurtigt svar**
|
||||
- Skriv SQL Server-version og CU (står på instansens side i SQLInsight Pro).
|
||||
|
|
@ -89,6 +96,41 @@ def call(method, path, data=None):
|
|||
sys.exit(f"{method} {path}: stadig begrænset efter flere forsøg")
|
||||
|
||||
|
||||
def all_categories():
|
||||
"""Every category including subcategories (/categories.json leaves subcategories out)."""
|
||||
return call("GET", "/site.json")["categories"]
|
||||
|
||||
|
||||
def find(slug, parent_id):
|
||||
return next((c for c in all_categories() if c["slug"] == slug and c.get("parent_category_id") == parent_id), None)
|
||||
|
||||
|
||||
def ensure_category(name, slug, colour, description, parent_id=None, solved=False, staff_only=False):
|
||||
fields = {
|
||||
"name": name, "slug": slug, "color": colour, "text_color": "FFFFFF",
|
||||
"custom_fields[enable_accepted_answers]": "true" if solved else "false",
|
||||
}
|
||||
if parent_id is not None:
|
||||
fields["parent_category_id"] = str(parent_id)
|
||||
if staff_only:
|
||||
fields.update({"permissions[everyone]": "2", "permissions[staff]": "1"}) # everyone: reply; staff: create
|
||||
existing = find(slug, parent_id)
|
||||
if existing:
|
||||
call("PUT", f"/categories/{existing['id']}.json", fields)
|
||||
category_id = existing["id"]
|
||||
print(f"kategori {name} (opdateret)")
|
||||
else:
|
||||
category_id = call("POST", "/categories.json", fields)["category"]["id"]
|
||||
print(f"kategori {name} (oprettet)")
|
||||
# The description is the first post of the category's "About" topic.
|
||||
about = call("GET", f"/c/{category_id}/show.json")["category"].get("topic_url")
|
||||
if about:
|
||||
post = call("GET", about + ".json")["post_stream"]["posts"][0]
|
||||
if post.get("raw") != description and description not in (post.get("cooked") or ""):
|
||||
call("PUT", f"/posts/{post['id']}.json", {"post[raw]": description})
|
||||
return category_id
|
||||
|
||||
|
||||
def main():
|
||||
if not BASE or not KEY:
|
||||
sys.exit(__doc__)
|
||||
|
|
@ -101,43 +143,26 @@ def main():
|
|||
call("PUT", f"/admin/site_settings/{name}.json", {name: value})
|
||||
print(f"indstilling {name} = {value}")
|
||||
|
||||
categories = {c["slug"]: c for c in call("GET", "/categories.json")["category_list"]["categories"]}
|
||||
name, slug, colour, description = PRODUCT
|
||||
product_id = ensure_category(name, slug, colour, description)
|
||||
ids = {}
|
||||
for name, slug, colour, description, solved, staff_only in CATEGORIES:
|
||||
fields = {
|
||||
"name": name, "slug": slug, "color": colour, "text_color": "FFFFFF",
|
||||
"custom_fields[enable_accepted_answers]": "true" if solved else "false",
|
||||
}
|
||||
if staff_only:
|
||||
fields.update({"permissions[everyone]": "2", "permissions[staff]": "1"}) # everyone: reply; staff: create
|
||||
if slug in categories:
|
||||
call("PUT", f"/categories/{categories[slug]['id']}.json", fields)
|
||||
category_id = categories[slug]["id"]
|
||||
print(f"kategori {name} (opdateret)")
|
||||
else:
|
||||
category_id = call("POST", "/categories.json", fields)["category"]["id"]
|
||||
print(f"kategori {name} (oprettet)")
|
||||
# The description is the first post of the category's "About" topic.
|
||||
about = call("GET", f"/c/{category_id}/show.json")["category"].get("topic_url")
|
||||
if about:
|
||||
topic = call("GET", about + ".json")
|
||||
post = topic["post_stream"]["posts"][0]
|
||||
if post.get("raw") != description and description not in (post.get("cooked") or ""):
|
||||
call("PUT", f"/posts/{post['id']}.json", {"post[raw]": description})
|
||||
ids[slug] = ensure_category(name, slug, colour, description, product_id, solved, staff_only)
|
||||
|
||||
category_id = next(c["id"] for c in call("GET", "/categories.json")["category_list"]["categories"] if c["slug"] == "annonceringer")
|
||||
announcements = call("GET", f"/c/annonceringer/{category_id}/l/latest.json")["topic_list"]["topics"]
|
||||
announcements = call("GET", f"/c/{ids['annonceringer']}/l/latest.json")["topic_list"]["topics"]
|
||||
if not any(t["title"] == WELCOME_TITLE for t in announcements):
|
||||
topic = call("POST", "/posts.json", {"title": WELCOME_TITLE, "raw": WELCOME, "category": category_id})
|
||||
call("PUT", f"/t/{topic['topic_id']}/status.json", {"status": "pinned_globally", "enabled": "true"})
|
||||
print("emne velkomst (oprettet og fastgjort)")
|
||||
# Discourse's own English welcome topic: no longer pinned or listed; ours replaces it.
|
||||
topic = call("POST", "/posts.json", {"title": WELCOME_TITLE, "raw": WELCOME, "category": ids["annonceringer"]})
|
||||
# Pinned in its category only: the forum's front page belongs to all products.
|
||||
call("PUT", f"/t/{topic['topic_id']}/status.json", {"status": "pinned", "enabled": "true"})
|
||||
print("emne velkomst (oprettet og fastgjort i SQLInsight Pro)")
|
||||
# Discourse's own English welcome topic: no longer pinned or listed.
|
||||
for topic in call("GET", "/latest.json")["topic_list"]["topics"]:
|
||||
if topic.get("pinned_globally") and topic["title"] != WELCOME_TITLE and topic["title"].startswith("Welcome to"):
|
||||
if topic.get("pinned_globally") and topic["title"].startswith("Welcome to"):
|
||||
call("PUT", f"/t/{topic['id']}/status.json", {"status": "pinned_globally", "enabled": "false"})
|
||||
call("PUT", f"/t/{topic['id']}/status.json", {"status": "visible", "enabled": "false"})
|
||||
print("emne Discourses engelske velkomst skjult")
|
||||
|
||||
feedback = next((c for c in call("GET", "/categories.json")["category_list"]["categories"] if c["slug"] == "site-feedback"), None)
|
||||
feedback = find("site-feedback", None)
|
||||
if feedback and feedback["name"] != "Om forummet":
|
||||
call("PUT", f"/categories/{feedback['id']}.json", {"name": "Om forummet", "slug": "site-feedback", "color": feedback["color"]})
|
||||
print("kategori Site Feedback omdøbt til Om forummet")
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ cd "$TARGET"
|
|||
# Skip the interactive prerequisite checks (they cannot be answered from a workflow).
|
||||
./launcher rebuild app --skip-prereqs
|
||||
|
||||
host="${FORUM_HOSTNAME:-forum.sqlinsight.seosoft.dk}"
|
||||
host="${FORUM_HOSTNAME:-forum.seosoft.dk}"
|
||||
cat <<NEXT
|
||||
|
||||
Discourse kører på ${FORUM_BIND}:${FORUM_PORT:-8082}. Første gang:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
render.py <template> <output>
|
||||
|
||||
Required: FORUM_BIND KNOWN_PROXY FORUM_ADMIN_EMAIL FORUM_SMTP_ADDRESS FORUM_SMTP_USER FORUM_SMTP_PASSWORD
|
||||
Optional: FORUM_HOSTNAME (forum.sqlinsight.seosoft.dk), FORUM_PORT (8082), FORUM_SMTP_PORT (587), FORUM_NOTIFICATION_EMAIL (FORUM_SMTP_USER),
|
||||
Optional: FORUM_HOSTNAME (forum.seosoft.dk), FORUM_PORT (8082), FORUM_SMTP_PORT (587), FORUM_NOTIFICATION_EMAIL (FORUM_SMTP_USER),
|
||||
FORUM_WORKERS (4), FORUM_DB_SHARED_BUFFERS (512MB)
|
||||
"""
|
||||
import ipaddress
|
||||
|
|
@ -13,7 +13,7 @@ import re
|
|||
import sys
|
||||
|
||||
DEFAULTS = {
|
||||
"FORUM_HOSTNAME": "forum.sqlinsight.seosoft.dk",
|
||||
"FORUM_HOSTNAME": "forum.seosoft.dk",
|
||||
"FORUM_PORT": "8082",
|
||||
"FORUM_SMTP_PORT": "587",
|
||||
"FORUM_WORKERS": "4",
|
||||
|
|
|
|||
|
|
@ -5,20 +5,23 @@ SQLInsight Pro har to offentlige websteder. Begge drives af SEOSoft:
|
|||
| Websted | Adresse | Hvad |
|
||||
|---|---|---|
|
||||
| Community-webstedet | `https://sqlinsight.seosoft.dk` | Forside, funktioner, download af signerede udgivelser, dokumentation, SQL Server-versioner og supportdatoer, regler og supportaftaler. Projektet `src/SQLInsight.Community`. |
|
||||
| Forummet | `https://forum.sqlinsight.seosoft.dk` | Discourse på dansk: spørgsmål og svar (med løste svar), installation og drift, performance, regler, idéer og annonceringer. |
|
||||
| Forummet | `https://forum.seosoft.dk/c/sqlinsight` | SEOSofts fælles Discourse-forum på dansk. SQLInsight Pro er kategorien `sqlinsight` med underkategorier: spørgsmål og svar (med løste svar), installation og drift, performance, regler, idéer og annonceringer. |
|
||||
|
||||
Alle tre navne (også `hub.sqlinsight.seosoft.dk`) peger på SEOSofts offentlige IP. Reverse proxyen dér terminerer TLS og sender videre til appserverens DMZ-adresse (`HOST_BIND`), på hver sin port:
|
||||
Alle navne peger på SEOSofts offentlige IP. Reverse proxyen dér terminerer TLS og sender videre til appserverens DMZ-adresse (`HOST_BIND`), på hver sin port:
|
||||
|
||||
| Navn | Container | Port (secret) | Deploy |
|
||||
|---|---|---|---|
|
||||
| `hub.sqlinsight.seosoft.dk` | `sqlinsight-hub` | `HOST_PORT` | `deploy-hub.yml`, ved push |
|
||||
| `sqlinsight.seosoft.dk` | `sqlinsight-community` | `COMMUNITY_PORT` | `deploy-community.yml`, ved push |
|
||||
| `forum.sqlinsight.seosoft.dk` | `app` (Discourse) | `FORUM_PORT` | `deploy-forum.yml`, kun manuelt |
|
||||
| `forum.seosoft.dk` | `app` (Discourse, fælles for SEOSofts produkter) | `FORUM_PORT` | `deploy-forum.yml`, kun manuelt |
|
||||
| `forum.sqlinsight.seosoft.dk` | – | – | 301-redirect til `forum.seosoft.dk` (`/` → `/c/sqlinsight`). Ældre installationer linker hertil |
|
||||
|
||||
Proxyen skal sende `Host`, `X-Forwarded-For` og `X-Forwarded-Proto: https`. For forummet skal den også tillade WebSockets og lange forespørgsler (Discourse bruger long polling på `/message-bus/`, så timeout skal være mindst 60 sekunder).
|
||||
|
||||
## Forummet (Discourse)
|
||||
|
||||
Forummet er fælles for SEOSofts produkter. Hvert produkt har sin egen hovedkategori; SQLInsight Pro har `sqlinsight`. Webstedet sætter `ForumCategory: sqlinsight`, så forsiden kun viser SQLInsight-emner.
|
||||
|
||||
Discourse installeres med den officielle `discourse_docker`-launcher, som er den eneste måde, Discourse understøtter. Workflowet `deploy-forum.yml` kører launcheren på appserveren i en helper-container, med `/var/discourse` og docker.sock fra værten. `deploy/forum/app.yml` er en skabelon, som `render.py` udfylder fra secrets. Intet hemmeligt ligger i repoet.
|
||||
|
||||
1. **Krav:** Discourse bruger 2-4 GB RAM og 20 GB disk under `/var/discourse` på appserveren.
|
||||
|
|
@ -35,7 +38,7 @@ Discourse installeres med den officielle `discourse_docker`-launcher, som er den
|
|||
| `FORUM_NOTIFICATION_EMAIL` | `forum@seosoft.dk` | Valgfri, ellers `FORUM_SMTP_USER` |
|
||||
| `FORUM_WORKERS`, `FORUM_DB_SHARED_BUFFERS` | `4`, `512MB` | Valgfri: ca. 2 pr. CPU-kerne og 25 % af den RAM, Discourse må bruge |
|
||||
|
||||
4. **Reverse proxy:** Peg `forum.sqlinsight.seosoft.dk` på `http://HOST_BIND:FORUM_PORT`.
|
||||
4. **Reverse proxy:** Peg `forum.seosoft.dk` på `http://HOST_BIND:FORUM_PORT`. Hostnavnet kan ændres med `FORUM_HOSTNAME`.
|
||||
5. **Installation:** Kør *Actions → deploy forum → Run workflow*. Første gang tager det 10-15 minutter. Det samme workflow bruges, når `app.yml` eller secrets ændres. Selve Discourse opdateres fra */admin/upgrade*.
|
||||
6. **Administrator:** Åbn forummet, og opret en bruger med `FORUM_ADMIN_EMAIL`. Den bliver administrator.
|
||||
7. **Opsætning som kode:** Lav en midlertidig API-nøgle på appserveren:
|
||||
|
|
@ -47,14 +50,14 @@ Discourse installeres med den officielle `discourse_docker`-launcher, som er den
|
|||
Kør derefter:
|
||||
|
||||
```
|
||||
DISCOURSE_URL=https://forum.sqlinsight.seosoft.dk DISCOURSE_API_KEY=... \
|
||||
DISCOURSE_URL=https://forum.seosoft.dk DISCOURSE_API_KEY=... \
|
||||
CONTACT_EMAIL=support@... python3 deploy/forum/configure.py
|
||||
```
|
||||
|
||||
`configure.py` gør følgende:
|
||||
- sætter titel, beskrivelser, dansk sprog, tags og løste svar (pluginnet `solved` følger med Discourse);
|
||||
- opretter kategorierne med beskrivelser. Kun medarbejdere kan skrive i *Annonceringer*;
|
||||
- opretter og fastgør et velkomstemne;
|
||||
- sætter forummets titel ("SEOSoft Forum"), beskrivelser, dansk sprog, tags og løste svar (pluginnet `solved` følger med Discourse);
|
||||
- opretter hovedkategorien *SQLInsight Pro* (`sqlinsight`) og underkategorierne med beskrivelser. Kun medarbejdere kan oprette emner i *Annonceringer*. Andre produkters kategorier røres ikke;
|
||||
- opretter et velkomstemne og fastgør det i SQLInsight Pro (ikke på hele forummets forside);
|
||||
- skjuler Discourses engelske standardemne.
|
||||
|
||||
Scriptet kan køres igen. Det ændrer kun det, der afviger. Slet API-nøglen bagefter under *Admin → API*.
|
||||
|
|
|
|||
|
|
@ -13,9 +13,20 @@ public sealed class CommunityOptions
|
|||
/// </summary>
|
||||
public Uri? DownloadUrl { get; set; }
|
||||
|
||||
/// <summary>The Discourse forum, e.g. https://forum.sqlinsight.seosoft.dk/.</summary>
|
||||
/// <summary>The Discourse forum's root, e.g. https://forum.seosoft.dk/. Topic links are built from it.</summary>
|
||||
public Uri? ForumUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Slug of SQLInsight Pro's category when the forum is shared with other products (e.g. "sqlinsight"). The front
|
||||
/// page then only lists that category's topics, and the forum links go to the category instead of the root.
|
||||
/// </summary>
|
||||
public string? ForumCategory { get; set; }
|
||||
|
||||
/// <summary>Where "Forum" links go: the category when <see cref="ForumCategory"/> is set, otherwise the root.</summary>
|
||||
public Uri? ForumHome => ForumUrl is { } forum && !string.IsNullOrWhiteSpace(ForumCategory)
|
||||
? new Uri(forum, $"c/{Uri.EscapeDataString(ForumCategory)}")
|
||||
: ForumUrl;
|
||||
|
||||
public Uri SourceUrl { get; set; } = new("https://git.seosoft.dk/seo/sqlinsight-pro");
|
||||
|
||||
/// <summary>Where Professional and Enterprise enquiries go.</summary>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<NavLink href="/sql-server-versioner">SQL Server-versioner</NavLink>
|
||||
<NavLink href="/regler">Regler</NavLink>
|
||||
<NavLink href="/support">Support</NavLink>
|
||||
@if (Options.Value.ForumUrl is { } forum)
|
||||
@if (Options.Value.ForumHome is { } forum)
|
||||
{
|
||||
<a class="button small" href="@forum" rel="noopener">Forum</a>
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
Gratis og open source (MIT). Udviklet af SEOSoft.
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
@if (Options.Value.ForumUrl is { } forumLink)
|
||||
@if (Options.Value.ForumHome is { } forumLink)
|
||||
{
|
||||
<a href="@forumLink" rel="noopener">Forum</a>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@
|
|||
}
|
||||
</ul>
|
||||
}
|
||||
@if (Options.Value.ForumUrl is { } forum)
|
||||
@if (Options.Value.ForumHome is { } forum)
|
||||
{
|
||||
<p><a href="@forum" rel="noopener">Gå til forummet →</a></p>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
<div class="card">
|
||||
<h3>Forummet</h3>
|
||||
<p class="muted">Spørgsmål om installation, fund og tuning. Andre brugere og SEOSoft svarer; gode svar markeres som løsningen.</p>
|
||||
@if (Options.Value.ForumUrl is { } forum) { <a href="@forum" rel="noopener">Gå til forummet →</a> }
|
||||
@if (Options.Value.ForumHome is { } forum) { <a href="@forum" rel="noopener">Gå til forummet →</a> }
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Fejl og forslag</h3>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ app.UseAntiforgery();
|
|||
app.MapStaticAssets();
|
||||
app.MapGet("/health", () => Results.Ok(new { status = "ok" }));
|
||||
app.MapGet("/forum", (Microsoft.Extensions.Options.IOptions<CommunityOptions> options) =>
|
||||
options.Value.ForumUrl is { } forum ? Results.Redirect(forum.ToString()) : Results.NotFound());
|
||||
options.Value.ForumHome is { } forum ? Results.Redirect(forum.ToString()) : Results.NotFound());
|
||||
app.MapRazorComponents<App>();
|
||||
app.Run();
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,11 @@ public sealed partial class ForumService(IOptions<CommunityOptions> options, IHt
|
|||
try
|
||||
{
|
||||
var client = http.CreateClient("forum");
|
||||
await using var stream = await client.GetStreamAsync(new Uri(forum, "latest.json"), cancellationToken).ConfigureAwait(false);
|
||||
// A shared forum: only our category's topics (Discourse includes its subcategories).
|
||||
var latestPath = string.IsNullOrWhiteSpace(options.Value.ForumCategory)
|
||||
? "latest.json"
|
||||
: $"c/{Uri.EscapeDataString(options.Value.ForumCategory)}/l/latest.json";
|
||||
await using var stream = await client.GetStreamAsync(new Uri(forum, latestPath), cancellationToken).ConfigureAwait(false);
|
||||
var latest = await JsonSerializer.DeserializeAsync<Latest>(stream, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
return (latest?.TopicList?.Topics ?? [])
|
||||
.Where(t => !t.Pinned && !string.IsNullOrWhiteSpace(t.Title))
|
||||
|
|
|
|||
|
|
@ -28,5 +28,5 @@ public sealed class WebOptions
|
|||
public Uri CommunityUrl { get; set; } = new("https://sqlinsight.seosoft.dk/");
|
||||
|
||||
/// <summary>The community forum, linked from the menu.</summary>
|
||||
public Uri ForumUrl { get; set; } = new("https://forum.sqlinsight.seosoft.dk/");
|
||||
public Uri ForumUrl { get; set; } = new("https://forum.seosoft.dk/c/sqlinsight");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,7 +192,24 @@ public sealed class SiteTests : IDisposable
|
|||
Assert.Equal(new Uri("https://forum.example/"), response.Headers.Location);
|
||||
}
|
||||
|
||||
private WebApplicationFactory<Program> Site(bool trustKey = true, string? downloadUrl = null)
|
||||
[Fact]
|
||||
public async Task Shared_forum_shows_only_our_category_and_links_to_it()
|
||||
{
|
||||
using var factory = Site(forumCategory: "sqlinsight");
|
||||
|
||||
var html = await Get(factory, "/");
|
||||
|
||||
Assert.Equal("/c/sqlinsight/l/latest.json", _forum.Requests.Single().AbsolutePath);
|
||||
// Topics live at the forum's root, not under the category.
|
||||
Assert.Contains("https://forum.example/t/maxdop/7", html, StringComparison.Ordinal);
|
||||
Assert.Contains("href=\"https://forum.example/c/sqlinsight\"", html, StringComparison.Ordinal);
|
||||
|
||||
using var client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
|
||||
using var response = await client.GetAsync(new Uri("/forum", UriKind.Relative), CancellationToken.None);
|
||||
Assert.Equal(new Uri("https://forum.example/c/sqlinsight"), response.Headers.Location);
|
||||
}
|
||||
|
||||
private WebApplicationFactory<Program> Site(bool trustKey = true, string? downloadUrl = null, string? forumCategory = null)
|
||||
{
|
||||
var settings = new Dictionary<string, string?>
|
||||
{
|
||||
|
|
@ -205,6 +222,11 @@ public sealed class SiteTests : IDisposable
|
|||
settings["SQLInsight:Community:DownloadUrl"] = downloadUrl;
|
||||
}
|
||||
|
||||
if (forumCategory is not null)
|
||||
{
|
||||
settings["SQLInsight:Community:ForumCategory"] = forumCategory;
|
||||
}
|
||||
|
||||
if (trustKey)
|
||||
{
|
||||
settings[$"SQLInsight:Community:TrustedKeys:{TestFeed.KeyId}"] = _feed.PublicKey;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue