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

186 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-20 22:09 +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/<build_id>/logs", 

139 view_func=build_views.get_snap_build_logs, 

140 methods=["GET"], 

141) 

142publisher_snaps.add_url_rule( 

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

144 view_func=get_validate_repo, 

145 methods=["GET"], 

146) 

147publisher_snaps.add_url_rule( 

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

149 view_func=post_build, 

150 methods=["POST"], 

151) 

152publisher_snaps.add_url_rule( 

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

154 view_func=build_views.check_build_request, 

155 methods=["GET"], 

156) 

157publisher_snaps.add_url_rule( 

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

159 view_func=build_views.post_github_webhook, 

160 methods=["POST"], 

161) 

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

163publisher_snaps.add_url_rule( 

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

165 view_func=build_views.post_github_webhook, 

166 methods=["POST"], 

167) 

168publisher_snaps.add_url_rule( 

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

170 view_func=build_views.get_update_gh_webhooks, 

171 methods=["GET"], 

172) 

173publisher_snaps.add_url_rule( 

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

175 view_func=post_disconnect_repo, 

176 methods=["POST"], 

177) 

178 

179# Release views 

180publisher_snaps.add_url_rule( 

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

182 view_func=release_views.redirect_get_release_history, 

183) 

184publisher_snaps.add_url_rule( 

185 "/<snap_name>/release", 

186 view_func=release_views.redirect_get_release_history, 

187) 

188publisher_snaps.add_url_rule( 

189 "/<snap_name>/releases", 

190 view_func=release_views.get_releases, 

191 methods=["GET"], 

192) 

193publisher_snaps.add_url_rule( 

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

195 view_func=releases.get_release_history_data, 

196 methods=["GET"], 

197) 

198publisher_snaps.add_url_rule( 

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

200 view_func=release_views.redirect_post_release, 

201 methods=["POST"], 

202) 

203publisher_snaps.add_url_rule( 

204 "/<snap_name>/release", 

205 view_func=release_views.redirect_post_release, 

206 methods=["POST"], 

207) 

208publisher_snaps.add_url_rule( 

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

210 view_func=release_views.get_release_history_json, 

211) 

212publisher_snaps.add_url_rule( 

213 "/<snap_name>/releases", 

214 view_func=release_views.post_release, 

215 methods=["POST"], 

216) 

217publisher_snaps.add_url_rule( 

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

219 view_func=release_views.redirect_post_close_channel, 

220 methods=["POST"], 

221) 

222publisher_snaps.add_url_rule( 

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

224 view_func=release_views.post_close_channel, 

225 methods=["POST"], 

226) 

227publisher_snaps.add_url_rule( 

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

229 view_func=release_views.post_default_track, 

230 methods=["POST"], 

231) 

232publisher_snaps.add_url_rule( 

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

234 view_func=release_views.get_snap_revision_json, 

235) 

236publisher_snaps.add_url_rule( 

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

238 view_func=release_views.get_release_status, 

239 methods=["GET"], 

240) 

241 

242# Metrics views 

243publisher_snaps.add_url_rule( 

244 "/snaps/metrics/json", 

245 view_func=metrics_views.get_account_snaps_metrics, 

246 methods=["POST"], 

247) 

248publisher_snaps.add_url_rule( 

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

250 view_func=metrics_views.get_measure_snap, 

251) 

252publisher_snaps.add_url_rule( 

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

254 view_func=metrics_views.get_measure_snap, 

255) 

256publisher_snaps.add_url_rule( 

257 "/<snap_name>/metrics", 

258 view_func=metrics_views.publisher_snap_metrics, 

259) 

260 

261publisher_snaps.add_url_rule( 

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

263 view_func=metrics_views.get_active_devices, 

264) 

265 

266publisher_snaps.add_url_rule( 

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

268 view_func=metrics_views.get_latest_active_devices, 

269) 

270 

271publisher_snaps.add_url_rule( 

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

273 view_func=metrics_views.get_metric_annotaion, 

274) 

275 

276publisher_snaps.add_url_rule( 

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

278 view_func=metrics_views.get_country_metric, 

279) 

280 

281# Publice views 

282publisher_snaps.add_url_rule( 

283 "/<snap_name>/publicise", 

284 view_func=publicise_views.get_publicise, 

285) 

286publisher_snaps.add_url_rule( 

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

288 view_func=publicise_views.get_publicise, 

289) 

290publisher_snaps.add_url_rule( 

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

292 view_func=publicise_views.get_publicise, 

293) 

294publisher_snaps.add_url_rule( 

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

296 view_func=get_publicise_data, 

297) 

298 

299# Settings views 

300publisher_snaps.add_url_rule( 

301 "/<snap_name>/settings", 

302 view_func=settings_views.get_settings, 

303) 

304publisher_snaps.add_url_rule( 

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

306 view_func=post_settings_data, 

307 methods=["POST"], 

308) 

309publisher_snaps.add_url_rule( 

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

311 view_func=get_settings_data, 

312) 

313 

314# CVE API 

315publisher_snaps.add_url_rule( 

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

317 view_func=cves.get_cves, 

318) 

319 

320publisher_snaps.add_url_rule( 

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

322 view_func=cves.get_revisions_with_cves, 

323) 

324 

325 

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

327@login_required 

328def redirect_get_account_snaps(): 

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

330 

331 

332@publisher_snaps.route("/snaps") 

333@login_required 

334def get_account_snaps(): 

335 account_info = dashboard.get_account(flask.session) 

336 

337 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info) 

338 

339 flask_user = flask.session["publisher"] 

340 

341 context = { 

342 "snaps": user_snaps, 

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

344 "registered_snaps": registered_snaps, 

345 } 

346 

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

348 

349 

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

351@login_required 

352def get_user_keys(): 

353 account_keys = dashboard.get_account_keys(flask.session) 

354 

355 return flask.jsonify(account_keys) 

356 

357 

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

359@login_required 

360def get_user_snaps(): 

361 account_info = dashboard.get_account(flask.session) 

362 

363 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info) 

364 

365 flask_user = flask.session["publisher"] 

366 

367 return flask.jsonify( 

368 { 

369 "snaps": user_snaps, 

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

371 "registered_snaps": registered_snaps, 

372 } 

373 ) 

374 

375 

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

377@login_required 

378def get_snap_build_status(): 

379 try: 

380 account_info = dashboard.get_account(flask.session) 

381 except (StoreApiError, ApiError): 

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

383 

384 response = [] 

385 user_snaps, _ = logic.get_snaps_account_info(account_info) 

386 

387 for snap_name in user_snaps: 

388 snap_build_statuses = launchpad.get_snap_build_status(snap_name) 

389 status = map_snap_build_status(snap_build_statuses) 

390 

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

392 

393 return flask.jsonify(response) 

394 

395 

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

397def redirect_get_register_name(): 

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

399 

400 

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

402@login_required 

403def get_register_name(): 

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

405 

406 

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

408def redirect_post_register_name(): 

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

410 

411 

412publisher_snaps.add_url_rule( 

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

414 view_func=get_package_metadata, 

415 methods=["GET"], 

416) 

417 

418publisher_snaps.add_url_rule( 

419 "/api/register-snap", 

420 view_func=post_register_name, 

421 methods=["POST"], 

422) 

423 

424publisher_snaps.add_url_rule( 

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

426 view_func=post_register_name_dispute, 

427 methods=["POST"], 

428) 

429 

430 

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

432@login_required 

433@exchange_required 

434def delete_package(package_name): 

435 response = publisher_gateway.unregister_package_name( 

436 flask.session, package_name 

437 ) 

438 

439 if response.status_code == 200: 

440 return ("", 200) 

441 return ( 

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

443 response.status_code, 

444 ) 

445 

446 

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

448@login_required 

449def get_is_user_snap(snap_name): 

450 is_users_snap = False 

451 try: 

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

453 except (StoreApiError, ApiError): 

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

455 

456 if authentication.is_authenticated(flask.session): 

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

458 if ( 

459 publisher_info.get("nickname") 

460 == snap_info["publisher"]["username"] 

461 ): 

462 is_users_snap = True 

463 

464 return {"is_users_snap": is_users_snap} 

465 

466 

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

468@login_required 

469def post_register_name_json(): 

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

471 

472 if not snap_name: 

473 return ( 

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

475 400, 

476 ) 

477 

478 try: 

479 response = dashboard.post_register_name( 

480 session=flask.session, snap_name=snap_name 

481 ) 

482 except StoreApiResponseErrorList as api_response_error_list: 

483 for error in api_response_error_list.errors: 

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

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

486 return flask.jsonify( 

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

488 ) 

489 return ( 

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

491 api_response_error_list.status_code, 

492 ) 

493 

494 response["code"] = "created" 

495 

496 return flask.jsonify(response) 

497 

498 

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

500@login_required 

501def get_register_name_dispute(): 

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

503 

504 if not snap_name: 

505 return flask.redirect( 

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

507 ) 

508 return flask.render_template( 

509 "store/publisher.html", 

510 ) 

511 

512 

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

514@login_required 

515def get_request_reserved_name(): 

516 stores = dashboard.get_stores(flask.session) 

517 

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

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

520 store_name = logic.get_store_name(store_id, stores) 

521 

522 if not snap_name: 

523 return flask.redirect( 

524 flask.url_for( 

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

526 ) 

527 ) 

528 return flask.render_template( 

529 "store/publisher.html", 

530 snap_name=snap_name, 

531 store=store_name, 

532 ) 

533 

534 

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

536@login_required 

537def snap_count(): 

538 account_info = dashboard.get_account(flask.session) 

539 

540 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info) 

541 

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

543 

544 return flask.jsonify(context)