From 8bfd351582c1a16a22cbb69db4793bb5bd993645 Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Sun, 19 Apr 2026 20:20:20 +0200 Subject: [PATCH] =?UTF-8?q?en=20mikrolagring=20f=C3=B8r=20app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-04-19 13.30.00 teeoff.no 44994b2e2831.jpg | Bin 46759 -> 0 bytes backend/import_social_golfamore_csv.py | 270 ++++++++++++++++++ frontend/src/app/admin/[alias]/page.tsx | 17 ++ .../golfbaner/[slug]/FacilityDetailView.tsx | 17 +- frontend/src/app/klubbnummer/page.tsx | 6 +- 5 files changed, 303 insertions(+), 7 deletions(-) delete mode 100644 2026-04-19 13.30.00 teeoff.no 44994b2e2831.jpg create mode 100644 backend/import_social_golfamore_csv.py create mode 100644 frontend/src/app/admin/[alias]/page.tsx diff --git a/2026-04-19 13.30.00 teeoff.no 44994b2e2831.jpg b/2026-04-19 13.30.00 teeoff.no 44994b2e2831.jpg deleted file mode 100644 index 3519f9238449572bdd826385cb038cc5ed8fc171..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46759 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 E0HmE}hX4Qo diff --git a/backend/import_social_golfamore_csv.py b/backend/import_social_golfamore_csv.py new file mode 100644 index 0000000..50910fb --- /dev/null +++ b/backend/import_social_golfamore_csv.py @@ -0,0 +1,270 @@ +import argparse +import asyncio +import csv +import json +import re +import unicodedata +from dataclasses import dataclass +from pathlib import Path + +import asyncpg + +from env_config import get_database_url + + +DEFAULT_CSV_PATH = Path("/opt/teeoff/Regneark uten navn - Ark 1.csv") +DB_URL = get_database_url() + +SLUG_OVERRIDES = { + "Bodø Golfpark": "salten-golfklubb-bodo-golfpark", + "Hemsedal (IKKE ALPIN)": "hemsedal-golfklubb", + "Randsfjorden": "land-golfklubb", + "Romerike": "aurskog-golfpark", + "Steinkjær": "steinkjer-golfklubb", + "Tønsberg Re": "re-golfklubb", +} + + +@dataclass +class FacilityMatch: + id: int + slug: str + name: str + normalized_blob: str + tokens: set[str] + + +def normalize_text(value: str) -> str: + source = (value or "").translate( + str.maketrans( + { + "æ": "ae", + "ø": "o", + "å": "a", + "Æ": "Ae", + "Ø": "O", + "Å": "A", + } + ) + ) + normalized = unicodedata.normalize("NFKD", source) + ascii_text = normalized.encode("ascii", "ignore").decode("ascii").lower() + ascii_text = re.sub(r"\([^)]*\)", " ", ascii_text) + ascii_text = ascii_text.replace("&", " and ") + ascii_text = re.sub(r"[^a-z0-9]+", " ", ascii_text) + return re.sub(r"\s+", " ", ascii_text).strip() + + +def tokenize(value: str) -> set[str]: + return {token for token in normalize_text(value).split() if token} + + +def clean_cell(value: str | None) -> str | None: + if value is None: + return None + cleaned = value.strip() + return cleaned or None + + +def build_social_links(facebook_url: str | None, instagram_url: str | None) -> list[dict[str, str]]: + social_links: list[dict[str, str]] = [] + if facebook_url: + social_links.append({"platform": "Facebook", "url": facebook_url}) + if instagram_url: + social_links.append({"platform": "Instagram", "url": instagram_url}) + return social_links + + +def build_golfamore_data(description: str | None) -> dict[str, str]: + if not description: + return {} + return {"terms": description} + + +def select_facility(row_name: str, facilities: list[FacilityMatch]) -> FacilityMatch: + override_slug = SLUG_OVERRIDES.get(row_name) + if override_slug: + for facility in facilities: + if facility.slug == override_slug: + return facility + raise ValueError(f"Fant ikke override-slug '{override_slug}' for '{row_name}'.") + + query_tokens = tokenize(row_name) + query_norm = normalize_text(row_name) + if not query_tokens: + raise ValueError(f"Tom eller ugyldig anleggsidentifikator: '{row_name}'.") + + scored: list[tuple[int, FacilityMatch]] = [] + for facility in facilities: + if not query_tokens.issubset(facility.tokens): + continue + + score = 0 + if query_norm == normalize_text(facility.slug): + score += 100 + if query_norm == normalize_text(facility.name): + score += 100 + if query_norm and query_norm in facility.normalized_blob: + score += 25 + score += max(0, 20 - (len(facility.tokens) - len(query_tokens))) + scored.append((score, facility)) + + if not scored: + raise ValueError(f"Fant ingen facility-match for '{row_name}'.") + + scored.sort(key=lambda item: (-item[0], item[1].name)) + best_score = scored[0][0] + best_matches = [facility for score, facility in scored if score == best_score] + + if len(best_matches) != 1: + options = ", ".join(f"{facility.name} ({facility.slug})" for facility in best_matches) + raise ValueError(f"Flertydig match for '{row_name}': {options}") + + return best_matches[0] + + +async def fetch_facilities(conn: asyncpg.Connection) -> list[FacilityMatch]: + rows = await conn.fetch("SELECT id, slug, name FROM facilities") + facilities: list[FacilityMatch] = [] + for row in rows: + blob = f"{row['name']} {row['slug']}" + facilities.append( + FacilityMatch( + id=row["id"], + slug=row["slug"], + name=row["name"], + normalized_blob=normalize_text(blob), + tokens=tokenize(blob), + ) + ) + return facilities + + +async def run_import(csv_path: Path, apply_changes: bool) -> None: + if not csv_path.exists(): + raise FileNotFoundError(f"Fant ikke CSV-fil: {csv_path}") + + with csv_path.open("r", encoding="utf-8-sig", newline="") as handle: + reader = csv.DictReader(handle) + rows = list(reader) + + conn = await asyncpg.connect(DB_URL) + try: + facilities = await fetch_facilities(conn) + updates: list[dict[str, object]] = [] + warnings: list[str] = [] + + for row in rows: + row_name = clean_cell(row.get("Anlegg")) + if not row_name: + continue + + facility = select_facility(row_name, facilities) + facebook_url = clean_cell(row.get("Facebook")) + instagram_url = clean_cell(row.get("Instagram")) + golfamore_description = clean_cell(row.get("Golfamore beskrivelse")) + golfamore_url = clean_cell(row.get("Golfamore url")) + social_links = build_social_links(facebook_url, instagram_url) + golfamore_data = build_golfamore_data(golfamore_description) + + if facebook_url and "facebook.com" not in facebook_url.lower(): + warnings.append(f"{row_name}: Facebook-kolonnen peker ikke til facebook.com -> {facebook_url}") + if instagram_url and "instagram.com" not in instagram_url.lower(): + warnings.append(f"{row_name}: Instagram-kolonnen peker ikke til instagram.com -> {instagram_url}") + + updates.append( + { + "row_name": row_name, + "facility_id": facility.id, + "slug": facility.slug, + "name": facility.name, + "facebook_url": facebook_url, + "instagram_url": instagram_url, + "social_links": social_links, + "golfamore": bool(golfamore_description or golfamore_url), + "golfamore_url": golfamore_url, + "golfamore_data": golfamore_data, + } + ) + + print(f"Klar til å oppdatere {len(updates)} anlegg fra {csv_path}.") + for update in updates: + print(f"- {update['row_name']} -> {update['name']} ({update['slug']})") + + if warnings: + print("\nAdvarsler:") + for warning in warnings: + print(f"- {warning}") + + if not apply_changes: + print("\nDry-run fullført. Ingen data ble skrevet.") + return + + async with conn.transaction(): + for update in updates: + await conn.execute( + """ + UPDATE facilities + SET + facebook_url = $1, + instagram_url = $2, + social_links = $3::jsonb, + golfamore = $4, + golfamore_url = $5, + golfamore_data = $6::jsonb + WHERE id = $7 + """, + update["facebook_url"], + update["instagram_url"], + json.dumps(update["social_links"]), + update["golfamore"], + update["golfamore_url"], + json.dumps(update["golfamore_data"]), + update["facility_id"], + ) + + await conn.execute( + """ + WITH cleaned AS ( + SELECT + id, + COALESCE( + ( + SELECT jsonb_agg(entry) + FROM jsonb_array_elements(COALESCE(social_links, '[]'::jsonb)) AS entry + WHERE NULLIF(BTRIM(entry->>'url'), '') IS NOT NULL + AND NULLIF(BTRIM(entry->>'platform'), '') IS NOT NULL + ), + '[]'::jsonb + ) AS social_links + FROM facilities + ) + UPDATE facilities AS target + SET + facebook_url = NULLIF(BTRIM(target.facebook_url), ''), + instagram_url = NULLIF(BTRIM(target.instagram_url), ''), + social_links = cleaned.social_links + FROM cleaned + WHERE target.id = cleaned.id + """ + ) + + print("\nImport fullført.") + finally: + await conn.close() + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Importer sosiale medier og Golfamore-felter fra CSV.") + parser.add_argument("--csv", type=Path, default=DEFAULT_CSV_PATH, help="Sti til CSV-filen som skal importeres.") + parser.add_argument( + "--apply", + action="store_true", + help="Skriver endringene til databasen. Uten dette kjøres bare dry-run.", + ) + return parser.parse_args() + + +if __name__ == "__main__": + arguments = parse_args() + asyncio.run(run_import(arguments.csv, arguments.apply)) diff --git a/frontend/src/app/admin/[alias]/page.tsx b/frontend/src/app/admin/[alias]/page.tsx new file mode 100644 index 0000000..dd76bec --- /dev/null +++ b/frontend/src/app/admin/[alias]/page.tsx @@ -0,0 +1,17 @@ +import { notFound, redirect } from "next/navigation"; +import { resolveFacilityAlias } from "@/app/facilityAliases"; + +type AdminFacilityAliasPageProps = { + params: Promise<{ alias: string }>; +}; + +export default async function AdminFacilityAliasPage({ params }: AdminFacilityAliasPageProps) { + const { alias } = await params; + const facilitySlug = await resolveFacilityAlias(alias); + + if (!facilitySlug) { + notFound(); + } + + redirect(`/admin/rediger/${facilitySlug}`); +} diff --git a/frontend/src/app/golfbaner/[slug]/FacilityDetailView.tsx b/frontend/src/app/golfbaner/[slug]/FacilityDetailView.tsx index 9d75962..cfcd86a 100644 --- a/frontend/src/app/golfbaner/[slug]/FacilityDetailView.tsx +++ b/frontend/src/app/golfbaner/[slug]/FacilityDetailView.tsx @@ -198,7 +198,11 @@ export default function FacilityDetailView({ facility }: { facility: any }) { const golfamoreData = parseJson(facility.golfamore_data, {}); const nsgData = parseJson(facility.nsg_data, {}); const socialLinksRaw = parseJson(facility.social_links, []); - const socialLinks = Array.isArray(socialLinksRaw) ? socialLinksRaw : []; + const socialLinks = (Array.isArray(socialLinksRaw) ? socialLinksRaw : []).filter((social: any) => { + const platform = typeof social?.platform === 'string' ? social.platform.trim() : ''; + const url = typeof social?.url === 'string' ? social.url.trim() : ''; + return Boolean(platform && url); + }); const coopClubsRaw = parseJson(facility.cooperating_clubs, []); const cooperatingClubs = Array.isArray(coopClubsRaw) ? coopClubsRaw : []; @@ -483,7 +487,7 @@ export default function FacilityDetailView({ facility }: { facility: any }) { href={facility.golfamore_url} target="_blank" rel="noopener noreferrer" - className="font-black text-[#ff5722] transition-colors hover:underline" + className="font-black !text-[#ff5722] visited:!text-[#ff5722] hover:!text-[#ff5722] hover:underline" > {golfamoreData.terms || golfamoreData.gyldighet || "Ja"} @@ -496,7 +500,7 @@ export default function FacilityDetailView({ facility }: { facility: any }) { Seniorgolf (NSG): {hasNSG && facility.nsg_url - ? Ja (Vis avtale) + ? Ja (Vis avtale) : (hasNSG ? Ja : "Nei") } @@ -511,7 +515,12 @@ export default function FacilityDetailView({ facility }: { facility: any }) {

{pakke.navn || 'Golfpakke'}

- {pakke.beskrivelse &&

{pakke.beskrivelse}

} + {pakke.beskrivelse && ( +
+ )}
diff --git a/frontend/src/app/klubbnummer/page.tsx b/frontend/src/app/klubbnummer/page.tsx index 743bfc1..986aee2 100644 --- a/frontend/src/app/klubbnummer/page.tsx +++ b/frontend/src/app/klubbnummer/page.tsx @@ -8,7 +8,7 @@ import { createPageMetadata, } from "@/app/seo"; -const pageTitle = "Klubbnummer"; +const pageTitle = "Klubbnummer i Golfbox"; const pageDescription = "Sorterbar oversikt over NGF-nummer og klubbnavn for norske golfanlegg på TeeOff."; @@ -62,8 +62,8 @@ export default async function ClubNumbersPage() { />