Coverage for webapp/store/views.py: 49%

193 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-22 22:07 +0000

1from math import floor 

2import flask 

3from dateutil import parser 

4from webapp.decorators import exchange_required, login_required 

5import webapp.helpers as helpers 

6import webapp.store.logic as logic 

7from webapp.api import requests 

8 

9from canonicalwebteam.exceptions import StoreApiError 

10from canonicalwebteam.store_api.dashboard import Dashboard 

11from canonicalwebteam.store_api.publishergw import PublisherGW 

12from canonicalwebteam.store_api.devicegw import DeviceGW 

13 

14from webapp.api.exceptions import ApiError 

15from webapp.store.snap_details_views import snap_details_views 

16from webapp.helpers import api_publisher_session, api_session 

17from flask.json import jsonify 

18import os 

19from webapp.extensions import csrf 

20 

21session = requests.Session() 

22 

23YOUTUBE_API_KEY = os.getenv("YOUTUBE_API_KEY") 

24dashboard = Dashboard(api_session) 

25publisher_gateway = PublisherGW("snap", api_publisher_session) 

26device_gateway = DeviceGW("snap", api_session) 

27 

28 

29def store_blueprint(store_query=None): 

30 store = flask.Blueprint( 

31 "store", 

32 __name__, 

33 template_folder="/templates", 

34 static_folder="/static", 

35 ) 

36 snap_details_views(store) 

37 

38 @store.route("/validation-sets", defaults={"path": ""}) 

39 @store.route("/validation-sets/<path:path>") 

40 @login_required 

41 def validation_sets(path): 

42 return flask.render_template("store/publisher.html") 

43 

44 @store.route("/discover") 

45 def discover(): 

46 return flask.redirect(flask.url_for(".homepage")) 

47 

48 def brand_store_view(): 

49 error_info = {} 

50 status_code = 200 

51 

52 try: 

53 snaps = device_gateway.get_all_items(size=16)["results"] 

54 except (StoreApiError, ApiError): 

55 snaps = [] 

56 

57 for snap in snaps: 

58 if "media" in snap: 

59 snap["icon_url"] = helpers.get_icon(snap["media"]) 

60 

61 return ( 

62 flask.render_template( 

63 "brand-store/store.html", snaps=snaps, error_info=error_info 

64 ), 

65 status_code, 

66 ) 

67 

68 def brand_search_snap(): 

69 status_code = 200 

70 snap_searched = flask.request.args.get("q", default="", type=str) 

71 

72 if not snap_searched: 

73 return flask.redirect(flask.url_for(".homepage")) 

74 

75 size = flask.request.args.get("limit", default=25, type=int) 

76 offset = flask.request.args.get("offset", default=0, type=int) 

77 

78 try: 

79 page = floor(offset / size) + 1 

80 except ZeroDivisionError: 

81 size = 10 

82 page = floor(offset / size) + 1 

83 

84 error_info = {} 

85 searched_results = [] 

86 

87 searched_results = device_gateway.search( 

88 snap_searched, size=size, page=page 

89 ) 

90 

91 snaps_results = searched_results["results"] 

92 

93 for snap in snaps_results: 

94 snap["icon_url"] = helpers.get_icon(snap["media"]) 

95 

96 links = logic.get_pages_details( 

97 flask.request.base_url, 

98 ( 

99 searched_results["_links"] 

100 if "_links" in searched_results 

101 else [] 

102 ), 

103 ) 

104 

105 context = { 

106 "query": snap_searched, 

107 "snaps": snaps_results, 

108 "links": links, 

109 "error_info": error_info, 

110 } 

111 

112 return ( 

113 flask.render_template("brand-store/search.html", **context), 

114 status_code, 

115 ) 

116 

117 @store.route("/store") 

118 def store_view(): 

119 return flask.render_template("store/store.html") 

120 

121 @store.route("/explore") 

122 def explore_view(): 

123 return flask.render_template("store/store.html") 

124 

125 @store.route("/youtube", methods=["POST"]) 

126 def get_video_thumbnail_data(): 

127 body = flask.request.form 

128 thumbnail_url = "https://www.googleapis.com/youtube/v3/videos" 

129 thumbnail_data = session.get( 

130 ( 

131 f"{thumbnail_url}?id={body['videoId']}" 

132 f"&part=snippet&key={YOUTUBE_API_KEY}" 

133 ) 

134 ) 

135 

136 if thumbnail_data: 

137 return thumbnail_data.json() 

138 

139 return {} 

140 

141 @store.route("/publisher/<regex('[a-z0-9-]*[a-z][a-z0-9-]*'):publisher>") 

142 def publisher_details(publisher): 

143 """ 

144 A view to display the publisher details page for specific publisher. 

145 """ 

146 

147 # 404 for the snap-quarantine publisher 

148 if publisher == "snap-quarantine": 

149 flask.abort(404) 

150 

151 publisher_content_path = flask.current_app.config["CONTENT_DIRECTORY"][ 

152 "PUBLISHER_PAGES" 

153 ] 

154 

155 if publisher in ["kde", "snapcrafters", "jetbrains"]: 

156 context = helpers.get_yaml( 

157 publisher_content_path + publisher + ".yaml", typ="safe" 

158 ) 

159 

160 if not context: 

161 flask.abort(404) 

162 

163 popular_snaps = helpers.get_yaml( 

164 publisher_content_path + publisher + "-snaps.yaml", 

165 typ="safe", 

166 ) 

167 

168 context["popular_snaps"] = ( 

169 popular_snaps["snaps"] if popular_snaps else [] 

170 ) 

171 

172 if "publishers" in context: 

173 context["snaps"] = [] 

174 for publisher in context["publishers"]: 

175 snaps_results = [] 

176 try: 

177 snaps_results = device_gateway.get_publisher_items( 

178 publisher, size=500, page=1 

179 )["_embedded"]["clickindex:package"] 

180 except StoreApiError: 

181 pass 

182 

183 for snap in snaps_results: 

184 snap["icon_url"] = helpers.get_icon(snap["media"]) 

185 

186 context["snaps"].extend( 

187 [snap for snap in snaps_results if snap["apps"]] 

188 ) 

189 

190 featured_snaps = [ 

191 snap["package_name"] for snap in context["featured_snaps"] 

192 ] 

193 

194 context["snaps"] = [ 

195 snap 

196 for snap in context["snaps"] 

197 if snap["package_name"] not in featured_snaps 

198 ] 

199 

200 context["snaps_count"] = len(context["snaps"]) + len( 

201 featured_snaps 

202 ) 

203 

204 return flask.render_template( 

205 "store/publisher-details.html", **context 

206 ) 

207 

208 status_code = 200 

209 error_info = {} 

210 snaps_results = [] 

211 snaps = [] 

212 snaps_count = 0 

213 publisher_details = {"display-name": publisher, "username": publisher} 

214 

215 snaps_results = device_gateway.find( 

216 publisher=publisher, 

217 fields=[ 

218 "title", 

219 "summary", 

220 "media", 

221 "publisher", 

222 ], 

223 )["results"] 

224 

225 for snap in snaps_results: 

226 item = snap["snap"] 

227 item["package_name"] = snap["name"] 

228 item["icon_url"] = helpers.get_icon(item["media"]) 

229 snaps.append(item) 

230 

231 snaps_count = len(snaps) 

232 

233 if snaps_count > 0: 

234 publisher_details = snaps[0]["publisher"] 

235 

236 context = { 

237 "snaps": snaps, 

238 "snaps_count": snaps_count, 

239 "publisher": publisher_details, 

240 "error_info": error_info, 

241 } 

242 

243 return ( 

244 flask.render_template( 

245 "store/community-publisher-details.html", **context 

246 ), 

247 status_code, 

248 ) 

249 

250 @store.route("/store/categories/<category>") 

251 def store_category(category): 

252 status_code = 200 

253 error_info = {} 

254 snaps_results = [] 

255 

256 snaps_results = device_gateway.get_category_items( 

257 category=category, size=10, page=1 

258 )["results"] 

259 for snap in snaps_results: 

260 snap["icon_url"] = helpers.get_icon(snap["media"]) 

261 

262 # if the first snap (banner snap) doesn't have an icon, remove the last 

263 # snap from the list to avoid a hanging snap (grid of 9) 

264 if len(snaps_results) == 10 and snaps_results[0]["icon_url"] == "": 

265 snaps_results = snaps_results[:-1] 

266 

267 for index in range(len(snaps_results)): 

268 snaps_results[index] = logic.get_snap_banner_url( 

269 snaps_results[index] 

270 ) 

271 

272 context = { 

273 "category": category, 

274 "has_featured": True, 

275 "snaps": snaps_results, 

276 "error_info": error_info, 

277 } 

278 

279 return ( 

280 flask.render_template("store/_category-partial.html", **context), 

281 status_code, 

282 ) 

283 

284 @store.route("/store/featured-snaps/<category>") 

285 def featured_snaps_in_category(category): 

286 snaps_results = [] 

287 

288 snaps_results = device_gateway.get_category_items( 

289 category=category, size=3, page=1 

290 )["_embedded"]["clickindex:package"] 

291 

292 for snap in snaps_results: 

293 snap["icon_url"] = helpers.get_icon(snap["media"]) 

294 

295 return flask.jsonify(snaps_results) 

296 

297 @store.route("/store/sitemap.xml") 

298 def sitemap(): 

299 base_url = "https://snapcraft.io/store" 

300 

301 snaps = [] 

302 page = 0 

303 url = f"https://api.snapcraft.io/api/v1/snaps/search?page={page}" 

304 while url: 

305 response = session.get(url) 

306 try: 

307 snaps_response = response.json() 

308 except Exception: 

309 continue 

310 

311 for snap in snaps_response["_embedded"]["clickindex:package"]: 

312 try: 

313 last_udpated = ( 

314 parser.parse(snap["last_updated"]) 

315 .replace(tzinfo=None) 

316 .strftime("%Y-%m-%d") 

317 ) 

318 snaps.append( 

319 { 

320 "url": "https://snapcraft.io/" 

321 + snap["package_name"], 

322 "last_udpated": last_udpated, 

323 } 

324 ) 

325 except Exception: 

326 continue 

327 if "next" in snaps_response["_links"]: 

328 url = snaps_response["_links"]["next"]["href"] 

329 else: 

330 url = None 

331 

332 xml_sitemap = flask.render_template( 

333 "sitemap/sitemap.xml", 

334 base_url=base_url, 

335 links=snaps, 

336 ) 

337 

338 response = flask.make_response(xml_sitemap) 

339 response.headers["Content-Type"] = "application/xml" 

340 response.headers["Cache-Control"] = "public, max-age=43200" 

341 

342 return response 

343 

344 if store_query: 

345 store.add_url_rule("/", "homepage", brand_store_view) 

346 store.add_url_rule("/search", "search", brand_search_snap) 

347 else: 

348 store.add_url_rule("/store", "homepage", store_view) 

349 

350 store.add_url_rule("/explore", "explore", explore_view) 

351 

352 @store.route("/<snap_name>/create-track", methods=["POST"]) 

353 @login_required 

354 @csrf.exempt 

355 @exchange_required 

356 def post_create_track(snap_name): 

357 track_name = flask.request.form["track-name"] 

358 version_pattern = flask.request.form.get("version-pattern") 

359 auto_phasing_percentage = flask.request.form.get( 

360 "automatic-phasing-percentage" 

361 ) 

362 

363 if auto_phasing_percentage is not None: 

364 auto_phasing_percentage = float(auto_phasing_percentage) 

365 

366 response = publisher_gateway.create_track( 

367 flask.session, 

368 snap_name, 

369 track_name, 

370 version_pattern, 

371 auto_phasing_percentage, 

372 ) 

373 if response.status_code == 201: 

374 return response.json(), response.status_code 

375 if response.status_code == 409: 

376 return ( 

377 jsonify({"error": "Track already exists."}), 

378 response.status_code, 

379 ) 

380 if "error-list" in response.json(): 

381 return ( 

382 jsonify( 

383 {"error": response.json()["error-list"][0]["message"]} 

384 ), 

385 response.status_code, 

386 ) 

387 return response.json(), response.status_code 

388 

389 return store