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

217 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-28 22:05 +0000

1# Packages 

2import bleach 

3import flask 

4from canonicalwebteam.store_api.dashboard import Dashboard 

5from canonicalwebteam.store_api.publishergw import PublisherGW 

6from canonicalwebteam.exceptions import ( 

7 StoreApiError, 

8 StoreApiResponseErrorList, 

9) 

10from flask.json import jsonify 

11 

12# Local 

13from webapp import authentication 

14from webapp.helpers import api_publisher_session, launchpad 

15from webapp.api.exceptions import ApiError 

16from webapp.decorators import exchange_required, login_required 

17from webapp.publisher.cve import cve_views 

18from webapp.publisher.snaps import ( 

19 build_views, 

20 listing_views, 

21 logic, 

22 metrics_views, 

23 publicise_views, 

24 release_views, 

25 settings_views, 

26 collaboration_views, 

27) 

28from webapp.publisher.snaps.builds import map_snap_build_status 

29 

30dashboard = Dashboard(api_publisher_session) 

31publisher_gateway = PublisherGW("snap", api_publisher_session) 

32 

33 

34publisher_snaps = flask.Blueprint( 

35 "publisher_snaps", 

36 __name__, 

37 template_folder="/templates", 

38 static_folder="/static", 

39) 

40 

41# Listing views 

42publisher_snaps.add_url_rule( 

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

44 view_func=listing_views.get_market_snap, 

45) 

46publisher_snaps.add_url_rule( 

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

48 view_func=listing_views.get_market_snap, 

49 methods=["GET"], 

50) 

51publisher_snaps.add_url_rule( 

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

53 view_func=listing_views.redirect_post_market_snap, 

54 methods=["POST"], 

55) 

56publisher_snaps.add_url_rule( 

57 "/<snap_name>/listing", 

58 view_func=listing_views.get_listing_snap, 

59 methods=["GET"], 

60) 

61publisher_snaps.add_url_rule( 

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

63 view_func=listing_views.get_listing_data, 

64 methods=["GET"], 

65) 

66publisher_snaps.add_url_rule( 

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

68 view_func=listing_views.post_listing_data, 

69 methods=["POST"], 

70) 

71publisher_snaps.add_url_rule( 

72 "/<snap_name>/preview", 

73 view_func=listing_views.post_preview, 

74 methods=["POST"], 

75) 

76publisher_snaps.add_url_rule( 

77 "/<snap_name>/collaboration", 

78 view_func=collaboration_views.get_collaboration_snap, 

79 methods=["GET"], 

80) 

81 

82# Build views 

83publisher_snaps.add_url_rule( 

84 "/<snap_name>/builds", 

85 view_func=build_views.get_snap_builds_page, 

86 methods=["GET"], 

87), 

88 

89publisher_snaps.add_url_rule( 

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

91 view_func=build_views.get_snap_build_page, 

92 methods=["GET"], 

93), 

94 

95publisher_snaps.add_url_rule( 

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

97 view_func=build_views.get_snap_repo, 

98 methods=["GET"], 

99) 

100publisher_snaps.add_url_rule( 

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

102 view_func=build_views.get_snap_builds, 

103 methods=["GET"], 

104) 

105publisher_snaps.add_url_rule( 

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

107 view_func=build_views.post_snap_builds, 

108 methods=["POST"], 

109) 

110publisher_snaps.add_url_rule( 

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

112 view_func=build_views.get_snap_build, 

113 methods=["GET"], 

114) 

115publisher_snaps.add_url_rule( 

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

117 view_func=build_views.get_validate_repo, 

118 methods=["GET"], 

119) 

120publisher_snaps.add_url_rule( 

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

122 view_func=build_views.post_build, 

123 methods=["POST"], 

124) 

125publisher_snaps.add_url_rule( 

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

127 view_func=build_views.check_build_request, 

128 methods=["GET"], 

129) 

130publisher_snaps.add_url_rule( 

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

132 view_func=build_views.post_github_webhook, 

133 methods=["POST"], 

134) 

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

136publisher_snaps.add_url_rule( 

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

138 view_func=build_views.post_github_webhook, 

139 methods=["POST"], 

140) 

141publisher_snaps.add_url_rule( 

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

143 view_func=build_views.get_update_gh_webhooks, 

144 methods=["GET"], 

145) 

146publisher_snaps.add_url_rule( 

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

148 view_func=build_views.post_disconnect_repo, 

149 methods=["POST"], 

150) 

151 

152# Release views 

153publisher_snaps.add_url_rule( 

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

155 view_func=release_views.redirect_get_release_history, 

156) 

157publisher_snaps.add_url_rule( 

158 "/<snap_name>/release", 

159 view_func=release_views.redirect_get_release_history, 

160) 

161publisher_snaps.add_url_rule( 

162 "/<snap_name>/releases", 

163 view_func=release_views.get_releases, 

164 methods=["GET"], 

165) 

166publisher_snaps.add_url_rule( 

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

168 view_func=release_views.get_release_history_data, 

169 methods=["GET"], 

170) 

171publisher_snaps.add_url_rule( 

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

173 view_func=release_views.redirect_post_release, 

174 methods=["POST"], 

175) 

176publisher_snaps.add_url_rule( 

177 "/<snap_name>/release", 

178 view_func=release_views.redirect_post_release, 

179 methods=["POST"], 

180) 

181publisher_snaps.add_url_rule( 

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

183 view_func=release_views.get_release_history_json, 

184) 

185publisher_snaps.add_url_rule( 

186 "/<snap_name>/releases", 

187 view_func=release_views.post_release, 

188 methods=["POST"], 

189) 

190publisher_snaps.add_url_rule( 

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

192 view_func=release_views.redirect_post_close_channel, 

193 methods=["POST"], 

194) 

195publisher_snaps.add_url_rule( 

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

197 view_func=release_views.post_close_channel, 

198 methods=["POST"], 

199) 

200publisher_snaps.add_url_rule( 

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

202 view_func=release_views.post_default_track, 

203 methods=["POST"], 

204) 

205publisher_snaps.add_url_rule( 

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

207 view_func=release_views.get_snap_revision_json, 

208) 

209 

210# Metrics views 

211publisher_snaps.add_url_rule( 

212 "/snaps/metrics/json", 

213 view_func=metrics_views.get_account_snaps_metrics, 

214 methods=["POST"], 

215) 

216publisher_snaps.add_url_rule( 

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

218 view_func=metrics_views.get_measure_snap, 

219) 

220publisher_snaps.add_url_rule( 

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

222 view_func=metrics_views.get_measure_snap, 

223) 

224publisher_snaps.add_url_rule( 

225 "/<snap_name>/metrics", 

226 view_func=metrics_views.publisher_snap_metrics, 

227) 

228 

229publisher_snaps.add_url_rule( 

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

231 view_func=metrics_views.get_active_devices, 

232) 

233 

234publisher_snaps.add_url_rule( 

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

236 view_func=metrics_views.get_latest_active_devices, 

237) 

238 

239publisher_snaps.add_url_rule( 

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

241 view_func=metrics_views.get_metric_annotaion, 

242) 

243 

244publisher_snaps.add_url_rule( 

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

246 view_func=metrics_views.get_country_metric, 

247) 

248 

249# Publice views 

250publisher_snaps.add_url_rule( 

251 "/<snap_name>/publicise", 

252 view_func=publicise_views.get_publicise, 

253) 

254publisher_snaps.add_url_rule( 

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

256 view_func=publicise_views.get_publicise, 

257) 

258publisher_snaps.add_url_rule( 

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

260 view_func=publicise_views.get_publicise, 

261) 

262publisher_snaps.add_url_rule( 

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

264 view_func=publicise_views.get_publicise_data, 

265) 

266 

267# Settings views 

268publisher_snaps.add_url_rule( 

269 "/<snap_name>/settings", 

270 view_func=settings_views.get_settings, 

271) 

272publisher_snaps.add_url_rule( 

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

274 view_func=settings_views.post_settings_data, 

275 methods=["POST"], 

276) 

277publisher_snaps.add_url_rule( 

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

279 view_func=settings_views.get_settings_data, 

280) 

281 

282# CVE API 

283publisher_snaps.add_url_rule( 

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

285 view_func=cve_views.get_cves, 

286) 

287 

288publisher_snaps.add_url_rule( 

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

290 view_func=cve_views.get_revisions_with_cves, 

291) 

292 

293 

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

295@login_required 

296def redirect_get_account_snaps(): 

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

298 

299 

300@publisher_snaps.route("/snaps") 

301@login_required 

302def get_account_snaps(): 

303 account_info = dashboard.get_account(flask.session) 

304 

305 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info) 

306 

307 flask_user = flask.session["publisher"] 

308 

309 context = { 

310 "snaps": user_snaps, 

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

312 "registered_snaps": registered_snaps, 

313 } 

314 

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

316 

317 

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

319@login_required 

320def get_user_snaps(): 

321 account_info = dashboard.get_account(flask.session) 

322 

323 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info) 

324 

325 flask_user = flask.session["publisher"] 

326 

327 return flask.jsonify( 

328 { 

329 "snaps": user_snaps, 

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

331 "registered_snaps": registered_snaps, 

332 } 

333 ) 

334 

335 

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

337@login_required 

338def get_snap_build_status(): 

339 try: 

340 account_info = dashboard.get_account(flask.session) 

341 except (StoreApiError, ApiError) as api_error: 

342 return flask.jsonify(api_error), 400 

343 

344 response = [] 

345 user_snaps, _ = logic.get_snaps_account_info(account_info) 

346 

347 for snap_name in user_snaps: 

348 snap_build_statuses = launchpad.get_snap_build_status(snap_name) 

349 status = map_snap_build_status(snap_build_statuses) 

350 

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

352 

353 return flask.jsonify(response) 

354 

355 

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

357def redirect_get_register_name(): 

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

359 

360 

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

362@login_required 

363def get_register_name(): 

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

365 

366 

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

368def redirect_post_register_name(): 

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

370 

371 

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

373@login_required 

374def post_register_name(): 

375 snap_name = flask.request.form.get("snap_name") 

376 res = {} 

377 

378 if not snap_name: 

379 res["success"] = False 

380 res["message"] = "You must define a snap name" 

381 

382 return jsonify(res) 

383 

384 is_private = flask.request.form.get("is_private") == "private" 

385 store = flask.request.form.get("store") 

386 

387 try: 

388 dashboard.post_register_name( 

389 session=flask.session, 

390 snap_name=snap_name, 

391 registrant_comment=None, 

392 is_private=is_private, 

393 store=store, 

394 ) 

395 except StoreApiResponseErrorList as api_response_error_list: 

396 res = { 

397 "success": False, 

398 "data": { 

399 "is_private": is_private, 

400 "snap_name": snap_name, 

401 "store": store, 

402 }, 

403 } 

404 

405 if api_response_error_list.status_code == 409: 

406 for error in api_response_error_list.errors: 

407 res["data"]["error_code"] = error["code"] 

408 

409 return jsonify(res) 

410 

411 if api_response_error_list.status_code == 400: 

412 res["data"]["error_code"] = "no-permission" 

413 res[ 

414 "message" 

415 ] = """You don't have permission 

416 to register a snap in this store. 

417 Please see store administrator.""" 

418 

419 return jsonify(res) 

420 

421 res["message"] = "Unable to register snap name" 

422 res["data"] = { 

423 "snap_name": snap_name, 

424 "is_private": is_private, 

425 "store": store, 

426 } 

427 

428 return jsonify(res) 

429 

430 return jsonify({"success": True}) 

431 

432 

433@publisher_snaps.route("/api/packages/<snap_name>", methods=["GET"]) 

434@login_required 

435@exchange_required 

436def get_package_metadata(snap_name): 

437 try: 

438 package_metadata = publisher_gateway.get_package_metadata( 

439 flask.session, snap_name 

440 ) 

441 

442 return jsonify({"data": package_metadata, "success": True}) 

443 except StoreApiError: 

444 return ( 

445 jsonify({"error": "Package metadata not found", "success": False}), 

446 404, 

447 ) 

448 

449 

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

451@login_required 

452@exchange_required 

453def delete_package(package_name): 

454 response = publisher_gateway.unregister_package_name( 

455 flask.session, package_name 

456 ) 

457 

458 if response.status_code == 200: 

459 return ("", 200) 

460 return ( 

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

462 response.status_code, 

463 ) 

464 

465 

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

467@login_required 

468def get_is_user_snap(snap_name): 

469 is_users_snap = False 

470 try: 

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

472 except (StoreApiError, ApiError) as api_error: 

473 return flask.jsonify({"error": str(api_error)}), 400 

474 

475 if authentication.is_authenticated(flask.session): 

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

477 if ( 

478 publisher_info.get("nickname") 

479 == snap_info["publisher"]["username"] 

480 ): 

481 is_users_snap = True 

482 

483 return {"is_users_snap": is_users_snap} 

484 

485 

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

487@login_required 

488def post_register_name_json(): 

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

490 

491 if not snap_name: 

492 return ( 

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

494 400, 

495 ) 

496 

497 try: 

498 response = dashboard.post_register_name( 

499 session=flask.session, snap_name=snap_name 

500 ) 

501 except StoreApiResponseErrorList as api_response_error_list: 

502 for error in api_response_error_list.errors: 

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

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

505 return flask.jsonify( 

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

507 ) 

508 return ( 

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

510 api_response_error_list.status_code, 

511 ) 

512 

513 response["code"] = "created" 

514 

515 return flask.jsonify(response) 

516 

517 

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

519@login_required 

520def get_register_name_dispute(): 

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

522 

523 if not snap_name: 

524 return flask.redirect( 

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

526 ) 

527 return flask.render_template( 

528 "store/publisher.html", 

529 ) 

530 

531 

532@publisher_snaps.route("/api/register-name-dispute", methods=["POST"]) 

533@login_required 

534def post_register_name_dispute(): 

535 try: 

536 claim = flask.json.loads(flask.request.data) 

537 snap_name = claim["snap-name"] 

538 claim_comment = claim["claim-comment"] 

539 dashboard.post_register_name_dispute( 

540 flask.session, 

541 bleach.clean(snap_name), 

542 bleach.clean(claim_comment), 

543 ) 

544 except StoreApiResponseErrorList as api_response_error_list: 

545 if api_response_error_list.status_code in [400, 409]: 

546 return jsonify( 

547 { 

548 "success": False, 

549 "data": api_response_error_list.errors, 

550 "message": api_response_error_list.errors[0]["message"], 

551 } 

552 ) 

553 return jsonify({"success": True}) 

554 

555 

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

557@login_required 

558def get_request_reserved_name(): 

559 stores = dashboard.get_stores(flask.session) 

560 

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

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

563 store_name = logic.get_store_name(store_id, stores) 

564 

565 if not snap_name: 

566 return flask.redirect( 

567 flask.url_for( 

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

569 ) 

570 ) 

571 return flask.render_template( 

572 "store/publisher.html", 

573 snap_name=snap_name, 

574 store=store_name, 

575 ) 

576 

577 

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

579@login_required 

580def snap_count(): 

581 account_info = dashboard.get_account(flask.session) 

582 

583 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info) 

584 

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

586 

587 return flask.jsonify(context)