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

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 

10 

11 

12solutions = Blueprint( 

13 "solutions", 

14 __name__, 

15 template_folder="../templates/solutions", 

16 static_folder="../static", 

17) 

18 

19FIELDS = [ 

20 "result.media", 

21 "result.title", 

22 "result.summary", 

23 "result.publisher.display-name", 

24 "result.categories", 

25 "result.deployable-on", 

26] 

27 

28 

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 

35 

36 if not charm or "result" not in charm: 

37 return None 

38 

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 ) 

47 

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 } 

58 

59 

60def render_solution(solution): 

61 solution["description_html"] = markdown_to_html( 

62 solution.get("description", "") 

63 ) 

64 

65 solution["categories"] = map_category_slugs_to_display( 

66 solution.get("categories") 

67 ) 

68 

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 ) 

77 

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) 

89 

90 def fetch(name): 

91 return name, get_charm_data(name) 

92 

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 

103 

104 # Preserve charm order 

105 solution_charms = [ 

106 charm_data_map[name] for name in charm_names if name in charm_data_map 

107 ] 

108 

109 solution["charms"] = solution_charms 

110 return solution 

111 

112 

113@solutions.route("/solutions/preview/<hash>") 

114def solution_preview(hash): 

115 solution = get_solution_from_backend(hash) 

116 if not solution: 

117 abort(404) 

118 

119 solution = render_solution(solution) 

120 

121 return render_template( 

122 "solutions/solutions_base_layout.html", 

123 solution=solution, 

124 is_preview=True, 

125 ) 

126 

127 

128@solutions.route("/solutions/preview-draft/<preview_key>") 

129def solution_preview_draft(preview_key): 

130 

131 cache_entry = preview_cache.get(preview_key) 

132 if not cache_entry: 

133 abort(404) 

134 

135 preview_data, _timestamp = cache_entry 

136 

137 solution = preview_data["solution"] 

138 form_data = preview_data["form_data"] 

139 

140 # Merge form data into solution for preview 

141 preview_solution = {**solution} 

142 

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 ) 

153 

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 } 

162 

163 preview_solution["media"] = { 

164 "icon": form_data.get("icon"), 

165 "architecture_diagram": form_data.get("architecture_diagram_url"), 

166 } 

167 

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 ] 

175 

176 preview_solution["compatibility"] = { 

177 "juju_versions": form_data.get("juju_versions", []), 

178 } 

179 

180 preview_solution["categories"] = form_data.get("categories", []) 

181 

182 charms_list = form_data.get("charms", []) 

183 preview_solution["charms"] = [ 

184 {"charm_name": charm} for charm in charms_list 

185 ] 

186 

187 preview_solution["use_cases"] = form_data.get("use_cases", []) 

188 preview_solution["useful_links"] = form_data.get("useful_links", []) 

189 

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 ] 

195 

196 preview_solution = render_solution(preview_solution) 

197 

198 return render_template( 

199 "solutions/solutions_base_layout.html", 

200 solution=preview_solution, 

201 is_preview=True, 

202 ) 

203 

204 

205@solutions.route("/solutions/<name>") 

206@redirect_uppercase_to_lowercase 

207def solution_details(name): 

208 preview_id = request.args.get("preview") 

209 

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 

216 

217 if not solution: 

218 abort(404) 

219 

220 solution = render_solution(solution) 

221 

222 return render_template( 

223 "solutions/solutions_base_layout.html", 

224 solution=solution, 

225 is_preview=is_preview, 

226 )