Coverage for webapp/topics/views.py: 35%
97 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-27 22:07 +0000
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-27 22:07 +0000
1import json
2from os import getenv
4from canonicalwebteam.discourse import DocParser
5from canonicalwebteam.discourse.exceptions import (
6 PathNotFoundError,
7 RedirectFoundError,
8)
9from flask import Blueprint, abort, jsonify, render_template, request, redirect
10from webapp.helpers import discourse_api
11from jinja2 import Template
12from bs4 import BeautifulSoup
13from urllib.parse import urlparse
15from webapp.config import CATEGORIES
16from webapp.observability.utils import trace_function
18DISCOURSE_API_KEY = getenv("DISCOURSE_API_KEY")
19DISCOURSE_API_USERNAME = getenv("DISCOURSE_API_USERNAME")
20ALLOWED_HOST = "charmhub.io"
22topics = Blueprint(
23 "topics", __name__, template_folder="/templates", static_folder="/static"
24)
26with open("webapp/topics/topics.json") as f:
27 topic_list = json.load(f)
30class TopicParser(DocParser):
31 @trace_function
32 def parse_topic(self, topic, docs_version=""):
33 result = super().parse_topic(topic, docs_version)
35 soup = BeautifulSoup(result["body_html"], features="html.parser")
36 self._parse_packages(soup)
37 result["body_html"] = soup
38 return result
40 @trace_function
41 def _parse_packages(self, soup):
42 """
43 Get a list of packages from all the
44 packages tables in a topic
46 Example:
47 | Charms |
48 | -- |
49 | https://charmhub.io/hello-kubecon |
50 """
51 package_tables = []
53 tables = soup.select("table:has(th:-soup-contains('Charms'))")
55 for table in tables:
56 table_rows = table.select("tr:has(td)")
58 if table_rows:
59 packages_set = {"soup_table": table, "packages": []}
61 # Get all packages URLs in this table
62 for row in table_rows:
63 navlink_href = row.find("a", href=True)
65 if navlink_href:
66 navlink_href = navlink_href.get("href")
67 parsed_url = urlparse(navlink_href)
69 if parsed_url.netloc != ALLOWED_HOST:
70 self.warnings.append("Invalid tutorial URL")
71 continue
73 # To avoid iframe issues with local development, demos
74 navlink_href = navlink_href.replace(
75 "https://charmhub.io/", request.url_root
76 )
78 packages_set["packages"].append(navlink_href)
80 package_tables.append(packages_set)
82 if package_tables:
83 # Remplace tables with cards
84 self._replace_packages(package_tables)
86 @trace_function
87 def _replace_packages(self, package_tables):
88 """
89 Replace charm tables to cards
90 """
91 card_template = Template(
92 (
93 '<div class="row">'
94 "{% for package in packages %}"
95 '<div class="col-small-5 col-medium-3 col-3">'
96 '<iframe src="{{ package }}/embedded?store_design=true" '
97 'frameborder="0" width="100%" height="266px" '
98 'style="border: 0"></iframe>'
99 "</div>"
100 "{% endfor %}"
101 "</div>"
102 )
103 )
105 for table in package_tables:
106 card = card_template.render(
107 packages=table["packages"],
108 )
109 table["soup_table"].replace_with(
110 BeautifulSoup(card, features="html.parser")
111 )
114@trace_function
115@topics.route("/topics.json")
116def topics_json():
117 query = request.args.get("q", default=None, type=str)
119 if query:
120 query = query.lower()
121 matched = []
122 unmatched = []
124 for t in topic_list:
125 if query in t["name"].lower() or query in t["categories"]:
126 matched.append(t)
127 else:
128 unmatched.append(t)
129 results = matched + unmatched
130 else:
131 results = topic_list
133 return jsonify(
134 {
135 "topics": results,
136 "q": query,
137 "size": len(results),
138 }
139 )
142@trace_function
143@topics.route("/topics")
144def all_topics():
145 context = {}
146 context["topics"] = topic_list
147 context["categories"] = CATEGORIES
148 return render_template("topics/index.html", **context)
151@trace_function
152@topics.route("/topics/<string:topic_slug>")
153@topics.route("/topics/<string:topic_slug>/<path:path>")
154def topic_page(topic_slug, path=None):
155 topic = next((t for t in topic_list if t["slug"] == topic_slug), None)
157 if not topic:
158 return abort(404)
160 topic_id = topic["topic_id"]
161 docs_url_prefix = f"/topics/{topic_slug}"
163 docs = TopicParser(
164 api=discourse_api,
165 index_topic_id=topic_id,
166 url_prefix=docs_url_prefix,
167 tutorials_index_topic_id=2628,
168 tutorials_url_prefix="https://juju.is/tutorials",
169 )
170 docs.parse()
172 if path:
173 try:
174 topic_id = docs.resolve_path(path)[0]
175 except PathNotFoundError:
176 abort(404)
177 except RedirectFoundError as path_redirect:
178 return redirect(path_redirect.target_url)
180 topic = docs.api.get_topic(topic_id)
181 else:
182 topic = docs.index_topic
184 document = docs.parse_topic(topic)
186 context = {
187 "navigation": docs.navigation,
188 "forum_url": docs.api.base_url,
189 "document": document,
190 }
192 return render_template("topics/document.html", **context)