Coverage for webapp/solutions/views.py: 21%
97 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-18 22:11 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-18 22:11 +0000
1from flask import Blueprint, abort, render_template, request
2from webapp.decorators import redirect_uppercase_to_lowercase
3from webapp.store_api import publisher_gateway
4from webapp.helpers import markdown_to_html
5from webapp.solutions.logic import get_solution_from_backend
6from concurrent.futures import ThreadPoolExecutor, as_completed
7from webapp.solutions.logic import get_published_solution_by_name
8from webapp.solutions.logic import map_category_slugs_to_display
9from webapp.publisher.views import preview_cache
12solutions = Blueprint(
13 "solutions",
14 __name__,
15 template_folder="../templates/solutions",
16 static_folder="../static",
17)
19FIELDS = [
20 "result.media",
21 "result.title",
22 "result.summary",
23 "result.publisher.display-name",
24 "result.categories",
25 "result.deployable-on",
26]
29def get_charm_data(charm_name):
30 """Fetch charm details from Charmhub API."""
31 try:
32 charm = publisher_gateway.get_item_details(charm_name, fields=FIELDS)
33 except Exception:
34 return None
36 if not charm or "result" not in charm:
37 return None
39 icon = None
40 if charm["result"].get("media"):
41 icon = charm["result"]["media"][0]["url"]
42 else:
43 icon = (
44 "https://assets.ubuntu.com/v1/"
45 "be6eb412-snapcraft-missing-icon.svg"
46 )
48 return {
49 "name": charm_name,
50 "title": charm["result"].get("title", charm_name),
51 "summary": charm["result"].get("summary", ""),
52 "publisher": charm["result"].get("publisher"),
53 "icon": icon,
54 "url": f"https://charmhub.io/{charm_name}",
55 "categories": charm["result"].get("categories"),
56 "deployable-on": charm["result"].get("deployable-on"),
57 }
60def render_solution(solution):
61 solution["description_html"] = markdown_to_html(
62 solution.get("description", "")
63 )
65 solution["categories"] = map_category_slugs_to_display(
66 solution.get("categories")
67 )
69 architecture_explanation = ""
70 if solution.get("documentation", {}).get("architecture_explanation"):
71 architecture_explanation = solution["documentation"][
72 "architecture_explanation"
73 ]
74 solution["architecture_explanation_html"] = markdown_to_html(
75 architecture_explanation
76 )
78 charm_names = []
79 for charm in solution.get("charms", []):
80 if isinstance(charm, dict):
81 if "name" in charm:
82 charm_names.append(charm["name"])
83 elif "charm_name" in charm:
84 charm_names.append(charm["charm_name"])
85 elif "title" in charm:
86 charm_names.append(charm["title"])
87 elif isinstance(charm, str):
88 charm_names.append(charm)
90 def fetch(name):
91 return name, get_charm_data(name)
93 charm_data_map = {}
94 if charm_names:
95 with ThreadPoolExecutor(max_workers=8) as executor:
96 futures = {
97 executor.submit(fetch, name): name for name in charm_names
98 }
99 for future in as_completed(futures):
100 name, data = future.result()
101 if data:
102 charm_data_map[name] = data
104 # Preserve charm order
105 solution_charms = [
106 charm_data_map[name] for name in charm_names if name in charm_data_map
107 ]
109 solution["charms"] = solution_charms
110 return solution
113@solutions.route("/solutions/preview/<hash>")
114def solution_preview(hash):
115 solution = get_solution_from_backend(hash)
116 if not solution:
117 abort(404)
119 solution = render_solution(solution)
121 return render_template(
122 "solutions/solutions_base_layout.html",
123 solution=solution,
124 is_preview=True,
125 )
128@solutions.route("/solutions/preview-draft/<preview_key>")
129def solution_preview_draft(preview_key):
131 cache_entry = preview_cache.get(preview_key)
132 if not cache_entry:
133 abort(404)
135 preview_data, _timestamp = cache_entry
137 solution = preview_data["solution"]
138 form_data = preview_data["form_data"]
140 # Merge form data into solution for preview
141 preview_solution = {**solution}
143 preview_solution.update(
144 {
145 "title": form_data.get("title", solution.get("title")),
146 "summary": form_data.get("summary", solution.get("summary")),
147 "description": form_data.get(
148 "description", solution.get("description")
149 ),
150 "terraform_modules": form_data.get("terraform_modules"),
151 }
152 )
154 preview_solution["documentation"] = {
155 "main": form_data.get("documentation_main"),
156 "source": form_data.get("documentation_source"),
157 "get_started": form_data.get("get_started_url"),
158 "architecture_explanation": form_data.get("architecture_explanation"),
159 "submit_a_bug": form_data.get("submit_bug_url"),
160 "community_discussion": form_data.get("community_discussion_url"),
161 }
163 preview_solution["media"] = {
164 "icon": form_data.get("icon"),
165 "architecture_diagram": form_data.get("architecture_diagram_url"),
166 }
168 preview_solution["deployable-on"] = [
169 {
170 "platform": form_data.get("platform"),
171 "version": form_data.get("platform_version", []),
172 "prerequisites": form_data.get("platform_prerequisites", []),
173 }
174 ]
176 preview_solution["compatibility"] = {
177 "juju_versions": form_data.get("juju_versions", []),
178 }
180 preview_solution["categories"] = form_data.get("categories", [])
182 charms_list = form_data.get("charms", [])
183 preview_solution["charms"] = [
184 {"charm_name": charm} for charm in charms_list
185 ]
187 preview_solution["use_cases"] = form_data.get("use_cases", [])
188 preview_solution["useful_links"] = form_data.get("useful_links", [])
190 maintainers_emails = form_data.get("maintainers", [])
191 preview_solution["maintainers"] = [
192 {"email": email, "display_name": email.split("@")[0]}
193 for email in maintainers_emails
194 ]
196 preview_solution = render_solution(preview_solution)
198 return render_template(
199 "solutions/solutions_base_layout.html",
200 solution=preview_solution,
201 is_preview=True,
202 )
205@solutions.route("/solutions/<name>")
206@redirect_uppercase_to_lowercase
207def solution_details(name):
208 preview_id = request.args.get("preview")
210 if preview_id:
211 solution = get_solution_from_backend(preview_id)
212 is_preview = True
213 else:
214 solution = get_published_solution_by_name(name)
215 is_preview = False
217 if not solution:
218 abort(404)
220 solution = render_solution(solution)
222 return render_template(
223 "solutions/solutions_base_layout.html",
224 solution=solution,
225 is_preview=is_preview,
226 )