Coverage for webapp/publisher/snaps/views.py: 85%

185 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-15 22:43 +0000

1# Packages 

2import flask 

3from canonicalwebteam.store_api.dashboard import Dashboard 

4from canonicalwebteam.store_api.publishergw import PublisherGW 

5from canonicalwebteam.exceptions import ( 

6 StoreApiError, 

7 StoreApiResponseErrorList, 

8) 

9from flask.json import jsonify 

10 

11# Local 

12from webapp import authentication 

13from webapp.helpers import api_publisher_session, launchpad 

14from webapp.api.exceptions import ApiError 

15from webapp.decorators import ( 

16 exchange_required, 

17 gate_unreleased_snap_pages, 

18 login_required, 

19) 

20from webapp.endpoints.publisher import listing as listing_endpoint 

21from webapp.endpoints import cves 

22from webapp.publisher.snaps import ( 

23 build_views, 

24 listing_views, 

25 logic, 

26 metrics_views, 

27 publicise_views, 

28 release_views, 

29 settings_views, 

30 collaboration_views, 

31) 

32from webapp.endpoints.publisher.builds import ( 

33 get_snap_build_page, 

34 get_validate_repo, 

35 post_build, 

36 post_disconnect_repo, 

37) 

38from webapp.endpoints.publisher.settings import ( 

39 get_settings_data, 

40 post_settings_data, 

41) 

42from webapp.endpoints.publisher.publicise import get_publicise_data 

43from webapp.endpoints.publisher.packages import get_package_metadata 

44from webapp.endpoints.publisher.register import ( 

45 post_register_name, 

46 post_register_name_dispute, 

47) 

48from webapp.endpoints import releases, builds 

49from webapp.publisher.snaps.builds import map_snap_build_status 

50 

51dashboard = Dashboard(api_publisher_session) 

52publisher_gateway = PublisherGW("snap", api_publisher_session) 

53 

54 

55publisher_snaps = flask.Blueprint( 

56 "publisher_snaps", 

57 __name__, 

58 template_folder="/templates", 

59 static_folder="/static", 

60) 

61publisher_snaps.before_request(gate_unreleased_snap_pages) 

62 

63# Listing views 

64publisher_snaps.add_url_rule( 

65 "/account/snaps/<snap_name>/market", 

66 view_func=listing_views.get_market_snap, 

67) 

68publisher_snaps.add_url_rule( 

69 "/account/snaps/<snap_name>/listing", 

70 view_func=listing_views.get_market_snap, 

71 methods=["GET"], 

72) 

73publisher_snaps.add_url_rule( 

74 "/account/snaps/<snap_name>/listing", 

75 view_func=listing_views.redirect_post_market_snap, 

76 methods=["POST"], 

77) 

78publisher_snaps.add_url_rule( 

79 "/<snap_name>/listing", 

80 view_func=listing_views.get_listing_snap, 

81 methods=["GET"], 

82) 

83publisher_snaps.add_url_rule( 

84 "/api/<snap_name>/listing", 

85 view_func=listing_endpoint.get_listing_data, 

86 methods=["GET"], 

87) 

88publisher_snaps.add_url_rule( 

89 "/api/<snap_name>/listing", 

90 view_func=listing_endpoint.post_listing_data, 

91 methods=["POST"], 

92) 

93publisher_snaps.add_url_rule( 

94 "/<snap_name>/preview", 

95 view_func=listing_views.post_preview, 

96 methods=["POST"], 

97) 

98publisher_snaps.add_url_rule( 

99 "/<snap_name>/collaboration", 

100 view_func=collaboration_views.get_collaboration_snap, 

101 methods=["GET"], 

102) 

103 

104# Build views 

105publisher_snaps.add_url_rule( 

106 "/<snap_name>/builds", 

107 view_func=build_views.get_snap_builds_page, 

108 methods=["GET"], 

109), 

110 

111publisher_snaps.add_url_rule( 

112 "/<snap_name>/builds/<build_id>", 

113 view_func=get_snap_build_page, 

114 methods=["GET"], 

115), 

116 

117publisher_snaps.add_url_rule( 

118 "/api/<snap_name>/repo", 

119 view_func=builds.get_snap_repo, 

120 methods=["GET"], 

121) 

122publisher_snaps.add_url_rule( 

123 "/api/<snap_name>/builds", 

124 view_func=build_views.get_snap_builds, 

125 methods=["GET"], 

126) 

127publisher_snaps.add_url_rule( 

128 "/api/<snap_name>/builds", 

129 view_func=build_views.post_snap_builds, 

130 methods=["POST"], 

131) 

132publisher_snaps.add_url_rule( 

133 "/api/<snap_name>/builds/<build_id>", 

134 view_func=build_views.get_snap_build, 

135 methods=["GET"], 

136) 

137publisher_snaps.add_url_rule( 

138 "/api/<snap_name>/builds/validate-repo", 

139 view_func=get_validate_repo, 

140 methods=["GET"], 

141) 

142publisher_snaps.add_url_rule( 

143 "/api/<snap_name>/builds/trigger-build", 

144 view_func=post_build, 

145 methods=["POST"], 

146) 

147publisher_snaps.add_url_rule( 

148 "/api/<snap_name>/builds/check-build-request/<build_id>", 

149 view_func=build_views.check_build_request, 

150 methods=["GET"], 

151) 

152publisher_snaps.add_url_rule( 

153 "/api/<snap_name>/webhook/notify", 

154 view_func=build_views.post_github_webhook, 

155 methods=["POST"], 

156) 

157# This route is to support previous webhooks from build.snapcraft.io 

158publisher_snaps.add_url_rule( 

159 "/api/<github_owner>/<github_repo>/webhook/notify", 

160 view_func=build_views.post_github_webhook, 

161 methods=["POST"], 

162) 

163publisher_snaps.add_url_rule( 

164 "/api/<snap_name>/builds/update-webhook", 

165 view_func=build_views.get_update_gh_webhooks, 

166 methods=["GET"], 

167) 

168publisher_snaps.add_url_rule( 

169 "/api/<snap_name>/builds/disconnect/", 

170 view_func=post_disconnect_repo, 

171 methods=["POST"], 

172) 

173 

174# Release views 

175publisher_snaps.add_url_rule( 

176 "/account/snaps/<snap_name>/release", 

177 view_func=release_views.redirect_get_release_history, 

178) 

179publisher_snaps.add_url_rule( 

180 "/<snap_name>/release", 

181 view_func=release_views.redirect_get_release_history, 

182) 

183publisher_snaps.add_url_rule( 

184 "/<snap_name>/releases", 

185 view_func=release_views.get_releases, 

186 methods=["GET"], 

187) 

188publisher_snaps.add_url_rule( 

189 "/api/<snap_name>/releases", 

190 view_func=releases.get_release_history_data, 

191 methods=["GET"], 

192) 

193publisher_snaps.add_url_rule( 

194 "/account/snaps/<snap_name>/release", 

195 view_func=release_views.redirect_post_release, 

196 methods=["POST"], 

197) 

198publisher_snaps.add_url_rule( 

199 "/<snap_name>/release", 

200 view_func=release_views.redirect_post_release, 

201 methods=["POST"], 

202) 

203publisher_snaps.add_url_rule( 

204 "/<snap_name>/releases/json", 

205 view_func=release_views.get_release_history_json, 

206) 

207publisher_snaps.add_url_rule( 

208 "/<snap_name>/releases", 

209 view_func=release_views.post_release, 

210 methods=["POST"], 

211) 

212publisher_snaps.add_url_rule( 

213 "/<snap_name>/release/close-channel", 

214 view_func=release_views.redirect_post_close_channel, 

215 methods=["POST"], 

216) 

217publisher_snaps.add_url_rule( 

218 "/<snap_name>/releases/close-channel", 

219 view_func=release_views.post_close_channel, 

220 methods=["POST"], 

221) 

222publisher_snaps.add_url_rule( 

223 "/<snap_name>/releases/default-track", 

224 view_func=release_views.post_default_track, 

225 methods=["POST"], 

226) 

227publisher_snaps.add_url_rule( 

228 "/<snap_name>/releases/revision/<revision>", 

229 view_func=release_views.get_snap_revision_json, 

230) 

231publisher_snaps.add_url_rule( 

232 "/api/<snap_name>/release-status", 

233 view_func=release_views.get_release_status, 

234 methods=["GET"], 

235) 

236 

237# Metrics views 

238publisher_snaps.add_url_rule( 

239 "/snaps/metrics/json", 

240 view_func=metrics_views.get_account_snaps_metrics, 

241 methods=["POST"], 

242) 

243publisher_snaps.add_url_rule( 

244 "/account/snaps/<snap_name>/measure", 

245 view_func=metrics_views.get_measure_snap, 

246) 

247publisher_snaps.add_url_rule( 

248 "/account/snaps/<snap_name>/metrics", 

249 view_func=metrics_views.get_measure_snap, 

250) 

251publisher_snaps.add_url_rule( 

252 "/<snap_name>/metrics", 

253 view_func=metrics_views.publisher_snap_metrics, 

254) 

255 

256publisher_snaps.add_url_rule( 

257 "/<snap_name>/metrics/active-devices", 

258 view_func=metrics_views.get_active_devices, 

259) 

260 

261publisher_snaps.add_url_rule( 

262 "/<snap_name>/metrics/active-latest-devices", 

263 view_func=metrics_views.get_latest_active_devices, 

264) 

265 

266publisher_snaps.add_url_rule( 

267 "/<snap_name>/metrics/active-device-annotation", 

268 view_func=metrics_views.get_metric_annotaion, 

269) 

270 

271publisher_snaps.add_url_rule( 

272 "/<snap_name>/metrics/country-metric", 

273 view_func=metrics_views.get_country_metric, 

274) 

275 

276# Publice views 

277publisher_snaps.add_url_rule( 

278 "/<snap_name>/publicise", 

279 view_func=publicise_views.get_publicise, 

280) 

281publisher_snaps.add_url_rule( 

282 "/<snap_name>/publicise/badges", 

283 view_func=publicise_views.get_publicise, 

284) 

285publisher_snaps.add_url_rule( 

286 "/<snap_name>/publicise/cards", 

287 view_func=publicise_views.get_publicise, 

288) 

289publisher_snaps.add_url_rule( 

290 "/api/<snap_name>/publicise", 

291 view_func=get_publicise_data, 

292) 

293 

294# Settings views 

295publisher_snaps.add_url_rule( 

296 "/<snap_name>/settings", 

297 view_func=settings_views.get_settings, 

298) 

299publisher_snaps.add_url_rule( 

300 "/api/<snap_name>/settings", 

301 view_func=post_settings_data, 

302 methods=["POST"], 

303) 

304publisher_snaps.add_url_rule( 

305 "/api/<snap_name>/settings", 

306 view_func=get_settings_data, 

307) 

308 

309# CVE API 

310publisher_snaps.add_url_rule( 

311 "/api/<snap_name>/<revision>/cves", 

312 view_func=cves.get_cves, 

313) 

314 

315publisher_snaps.add_url_rule( 

316 "/api/<snap_name>/cves", 

317 view_func=cves.get_revisions_with_cves, 

318) 

319 

320 

321@publisher_snaps.route("/account/snaps") 

322@login_required 

323def redirect_get_account_snaps(): 

324 return flask.redirect(flask.url_for(".get_account_snaps")) 

325 

326 

327@publisher_snaps.route("/snaps") 

328@login_required 

329def get_account_snaps(): 

330 account_info = dashboard.get_account(flask.session) 

331 

332 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info) 

333 

334 flask_user = flask.session["publisher"] 

335 

336 context = { 

337 "snaps": user_snaps, 

338 "current_user": flask_user["nickname"], 

339 "registered_snaps": registered_snaps, 

340 } 

341 

342 return flask.render_template("store/publisher.html", **context) 

343 

344 

345@publisher_snaps.route("/account-keys.json") 

346@login_required 

347def get_user_keys(): 

348 account_keys = dashboard.get_account_keys(flask.session) 

349 

350 return flask.jsonify(account_keys) 

351 

352 

353@publisher_snaps.route("/snaps.json") 

354@login_required 

355def get_user_snaps(): 

356 account_info = dashboard.get_account(flask.session) 

357 

358 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info) 

359 

360 flask_user = flask.session["publisher"] 

361 

362 return flask.jsonify( 

363 { 

364 "snaps": user_snaps, 

365 "current_user": flask_user["nickname"], 

366 "registered_snaps": registered_snaps, 

367 } 

368 ) 

369 

370 

371@publisher_snaps.route("/snap-builds.json") 

372@login_required 

373def get_snap_build_status(): 

374 try: 

375 account_info = dashboard.get_account(flask.session) 

376 except (StoreApiError, ApiError): 

377 return flask.jsonify({"error": "An unexpected error occurred"}), 400 

378 

379 response = [] 

380 user_snaps, _ = logic.get_snaps_account_info(account_info) 

381 

382 for snap_name in user_snaps: 

383 snap_build_statuses = launchpad.get_snap_build_status(snap_name) 

384 status = map_snap_build_status(snap_build_statuses) 

385 

386 response.append({"name": snap_name, "status": status}) 

387 

388 return flask.jsonify(response) 

389 

390 

391@publisher_snaps.route("/account/register-snap") 

392def redirect_get_register_name(): 

393 return flask.redirect(flask.url_for(".get_register_name")) 

394 

395 

396@publisher_snaps.route("/register-snap") 

397@login_required 

398def get_register_name(): 

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

400 

401 

402@publisher_snaps.route("/account/register-snap", methods=["POST"]) 

403def redirect_post_register_name(): 

404 return flask.redirect(flask.url_for(".post_register_name"), 307) 

405 

406 

407publisher_snaps.add_url_rule( 

408 "/api/packages/<snap_name>", 

409 view_func=get_package_metadata, 

410 methods=["GET"], 

411) 

412 

413publisher_snaps.add_url_rule( 

414 "/api/register-snap", 

415 view_func=post_register_name, 

416 methods=["POST"], 

417) 

418 

419publisher_snaps.add_url_rule( 

420 "/api/register-name-dispute", 

421 view_func=post_register_name_dispute, 

422 methods=["POST"], 

423) 

424 

425 

426@publisher_snaps.route("/packages/<package_name>", methods=["DELETE"]) 

427@login_required 

428@exchange_required 

429def delete_package(package_name): 

430 response = publisher_gateway.unregister_package_name( 

431 flask.session, package_name 

432 ) 

433 

434 if response.status_code == 200: 

435 return ("", 200) 

436 return ( 

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

438 response.status_code, 

439 ) 

440 

441 

442@publisher_snaps.route("/snap_info/user_snap/<snap_name>", methods=["GET"]) 

443@login_required 

444def get_is_user_snap(snap_name): 

445 is_users_snap = False 

446 try: 

447 snap_info = dashboard.get_snap_info(flask.session, snap_name) 

448 except (StoreApiError, ApiError): 

449 return flask.jsonify({"error": "An unexpected error occurred"}), 400 

450 

451 if authentication.is_authenticated(flask.session): 

452 publisher_info = flask.session.get("publisher", {}) 

453 if ( 

454 publisher_info.get("nickname") 

455 == snap_info["publisher"]["username"] 

456 ): 

457 is_users_snap = True 

458 

459 return {"is_users_snap": is_users_snap} 

460 

461 

462@publisher_snaps.route("/register-snap/json", methods=["POST"]) 

463@login_required 

464def post_register_name_json(): 

465 snap_name = flask.request.form.get("snap-name") 

466 

467 if not snap_name: 

468 return ( 

469 flask.jsonify({"errors": [{"message": "Snap name is required"}]}), 

470 400, 

471 ) 

472 

473 try: 

474 response = dashboard.post_register_name( 

475 session=flask.session, snap_name=snap_name 

476 ) 

477 except StoreApiResponseErrorList as api_response_error_list: 

478 for error in api_response_error_list.errors: 

479 # if snap name is already owned treat it as success 

480 if error["code"] == "already_owned": 

481 return flask.jsonify( 

482 {"code": error["code"], "snap_name": snap_name} 

483 ) 

484 return ( 

485 flask.jsonify({"errors": api_response_error_list.errors}), 

486 api_response_error_list.status_code, 

487 ) 

488 

489 response["code"] = "created" 

490 

491 return flask.jsonify(response) 

492 

493 

494@publisher_snaps.route("/register-name-dispute") 

495@login_required 

496def get_register_name_dispute(): 

497 snap_name = flask.request.args.get("snap-name") 

498 

499 if not snap_name: 

500 return flask.redirect( 

501 flask.url_for(".get_register_name", snap_name=snap_name) 

502 ) 

503 return flask.render_template( 

504 "store/publisher.html", 

505 ) 

506 

507 

508@publisher_snaps.route("/request-reserved-name") 

509@login_required 

510def get_request_reserved_name(): 

511 stores = dashboard.get_stores(flask.session) 

512 

513 snap_name = flask.request.args.get("snap_name") 

514 store_id = flask.request.args.get("store") 

515 store_name = logic.get_store_name(store_id, stores) 

516 

517 if not snap_name: 

518 return flask.redirect( 

519 flask.url_for( 

520 ".get_register_name", snap_name=snap_name, store=store_id 

521 ) 

522 ) 

523 return flask.render_template( 

524 "store/publisher.html", 

525 snap_name=snap_name, 

526 store=store_name, 

527 ) 

528 

529 

530@publisher_snaps.route("/snaps/api/snap-count") 

531@login_required 

532def snap_count(): 

533 account_info = dashboard.get_account(flask.session) 

534 

535 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info) 

536 

537 context = {"count": len(user_snaps), "snaps": list(user_snaps.keys())} 

538 

539 return flask.jsonify(context)