Coverage for app / backend / src / couchers / resources.py: 91%
70 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-03 06:18 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-03 06:18 +0000
1import functools
2import json
3import logging
4from dataclasses import dataclass
5from pathlib import Path
6from typing import Any, cast
8from sqlalchemy import select
9from sqlalchemy.orm import Session
10from sqlalchemy.sql import delete, text
12from couchers.config import config
13from couchers.db import session_scope
14from couchers.models import Language, Region, TimezoneArea
16logger = logging.getLogger(__name__)
18resources_folder = Path(__file__).parent / ".." / ".." / "resources"
21@functools.cache
22def get_terms_of_service() -> str:
23 """
24 Get the latest terms of service
25 """
26 with open(resources_folder / "terms_of_service.md", "r") as f:
27 return f.read()
30@functools.cache
31def get_icon(name: str) -> str:
32 """
33 Get an icon SVG by name
34 """
35 return (resources_folder / "icons" / name).read_text()
38@functools.cache
39def get_region_dict() -> dict[str, str]:
40 """
41 Get a list of allowed regions as a dictionary of {alpha3: name}.
42 """
43 with session_scope() as session:
44 return {region.code: region.name for region in session.execute(select(Region)).scalars().all()}
47def region_is_allowed(code: str) -> bool:
48 """
49 Check a region code is valid
50 """
51 return code in get_region_dict()
54@functools.cache
55def get_language_dict() -> dict[str, str]:
56 """
57 Get a list of allowed languages as a dictionary of {code: name}.
58 """
59 with session_scope() as session:
60 return {language.code: language.name for language in session.execute(select(Language)).scalars().all()}
63@functools.cache
64def get_badge_data() -> dict[str, Any]:
65 """
66 Get a list of profile badges in form {id: Badge}
67 """
68 with open(resources_folder / "badges.json", "r") as f:
69 data = json.load(f)
70 return cast(dict[str, Any], data)
73@dataclass(frozen=True, slots=True, kw_only=True)
74class Badge:
75 """Defines a profile badge that can be awarded to users."""
77 id: str
78 color: str
79 admin_editable: bool
82@functools.cache
83def get_badge_dict() -> dict[str, Badge]:
84 """
85 Get a list of profile badges in form {id: Badge}
86 """
87 badges = [Badge(**b) for b in get_badge_data()["badges"]]
88 return {badge.id: badge for badge in badges}
91@functools.cache
92def get_static_badge_dict() -> dict[str, list[int]]:
93 """
94 Get a list of static badges in form {id: list(user_ids)}
95 """
96 data = get_badge_data()["static_badges"]
97 return cast(dict[str, list[int]], data)
100def language_is_allowed(code: str) -> bool:
101 """
102 Check a language code is valid
103 """
104 return code in get_language_dict()
107def copy_resources_to_database(session: Session) -> None:
108 """
109 Syncs the source-of-truth data from files into the database. Call this at the end of a migration.
111 Foreign key constraints that refer to resource tables need to be set to DEFERRABLE.
113 We sync as follows:
115 1. Lock the table to be updated fully
116 2. Defer all constraints
117 3. Truncate the table
118 4. Re-insert everything
120 Truncating and recreating guarantees the data is fully in sync.
121 """
122 with open(resources_folder / "regions.json", "r") as f:
123 regions = [(region["alpha3"], region["name"]) for region in json.load(f)]
125 with open(resources_folder / "languages.json", "r") as f:
126 languages = [(language["code"], language["name"]) for language in json.load(f)]
128 timezone_areas_file = resources_folder / "timezone_areas.sql"
130 if not timezone_areas_file.exists(): 130 ↛ 131line 130 didn't jump to line 131 because the condition on line 130 was never true
131 if not config["DEV"]:
132 raise Exception("Missing timezone_areas.sql and not running in dev")
134 timezone_areas_file = resources_folder / "timezone_areas.sql-fake"
135 logger.info("Using fake timezone areas")
137 with open(timezone_areas_file, "r") as f:
138 tz_sql = f.read()
140 # set all constraints marked as DEFERRABLE to be checked at the end of this transaction, not immediately
141 session.execute(text("SET CONSTRAINTS ALL DEFERRED"))
143 session.execute(delete(Region))
144 for code, name in regions:
145 session.add(Region(code=code, name=name))
147 session.execute(delete(Language))
148 for code, name in languages:
149 session.add(Language(code=code, name=name))
151 session.execute(delete(TimezoneArea))
152 session.execute(text(tz_sql))