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

181 statements  

« prev     ^ index     » next       coverage.py v7.10.5, created at 2025-08-26 22:06 +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 

10from talisker import logging 

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.endpoints.publisher import listing as listing_endpoint 

18from webapp.endpoints import cves 

19from webapp.publisher.snaps import ( 

20 build_views, 

21 listing_views, 

22 logic, 

23 metrics_views, 

24 publicise_views, 

25 release_views, 

26 settings_views, 

27 collaboration_views, 

28) 

29from webapp.endpoints.publisher.builds import ( 

30 get_snap_build_page, 

31 get_validate_repo, 

32 post_build, 

33 post_disconnect_repo, 

34) 

35from webapp.endpoints.publisher.settings import ( 

36 get_settings_data, 

37 post_settings_data, 

38) 

39from webapp.endpoints.publisher.publicise import get_publicise_data 

40from webapp.endpoints.publisher.packages import get_package_metadata 

41from webapp.endpoints.publisher.register import ( 

42 post_register_name, 

43 post_register_name_dispute, 

44) 

45from webapp.endpoints import releases, builds 

46from webapp.publisher.snaps.builds import map_snap_build_status 

47 

48dashboard = Dashboard(api_publisher_session) 

49publisher_gateway = PublisherGW("snap", api_publisher_session) 

50 

51 

52publisher_snaps = flask.Blueprint( 

53 "publisher_snaps", 

54 __name__, 

55 template_folder="/templates", 

56 static_folder="/static", 

57) 

58 

59# Listing views 

60publisher_snaps.add_url_rule( 

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

62 view_func=listing_views.get_market_snap, 

63) 

64publisher_snaps.add_url_rule( 

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

66 view_func=listing_views.get_market_snap, 

67 methods=["GET"], 

68) 

69publisher_snaps.add_url_rule( 

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

71 view_func=listing_views.redirect_post_market_snap, 

72 methods=["POST"], 

73) 

74publisher_snaps.add_url_rule( 

75 "/<snap_name>/listing", 

76 view_func=listing_views.get_listing_snap, 

77 methods=["GET"], 

78) 

79publisher_snaps.add_url_rule( 

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

81 view_func=listing_endpoint.get_listing_data, 

82 methods=["GET"], 

83) 

84publisher_snaps.add_url_rule( 

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

86 view_func=listing_endpoint.post_listing_data, 

87 methods=["POST"], 

88) 

89publisher_snaps.add_url_rule( 

90 "/<snap_name>/preview", 

91 view_func=listing_views.post_preview, 

92 methods=["POST"], 

93) 

94publisher_snaps.add_url_rule( 

95 "/<snap_name>/collaboration", 

96 view_func=collaboration_views.get_collaboration_snap, 

97 methods=["GET"], 

98) 

99 

100# Build views 

101publisher_snaps.add_url_rule( 

102 "/<snap_name>/builds", 

103 view_func=build_views.get_snap_builds_page, 

104 methods=["GET"], 

105), 

106 

107publisher_snaps.add_url_rule( 

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

109 view_func=get_snap_build_page, 

110 methods=["GET"], 

111), 

112 

113publisher_snaps.add_url_rule( 

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

115 view_func=builds.get_snap_repo, 

116 methods=["GET"], 

117) 

118publisher_snaps.add_url_rule( 

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

120 view_func=build_views.get_snap_builds, 

121 methods=["GET"], 

122) 

123publisher_snaps.add_url_rule( 

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

125 view_func=build_views.post_snap_builds, 

126 methods=["POST"], 

127) 

128publisher_snaps.add_url_rule( 

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

130 view_func=build_views.get_snap_build, 

131 methods=["GET"], 

132) 

133publisher_snaps.add_url_rule( 

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

135 view_func=get_validate_repo, 

136 methods=["GET"], 

137) 

138publisher_snaps.add_url_rule( 

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

140 view_func=post_build, 

141 methods=["POST"], 

142) 

143publisher_snaps.add_url_rule( 

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

145 view_func=build_views.check_build_request, 

146 methods=["GET"], 

147) 

148publisher_snaps.add_url_rule( 

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

150 view_func=build_views.post_github_webhook, 

151 methods=["POST"], 

152) 

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

154publisher_snaps.add_url_rule( 

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

156 view_func=build_views.post_github_webhook, 

157 methods=["POST"], 

158) 

159publisher_snaps.add_url_rule( 

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

161 view_func=build_views.get_update_gh_webhooks, 

162 methods=["GET"], 

163) 

164publisher_snaps.add_url_rule( 

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

166 view_func=post_disconnect_repo, 

167 methods=["POST"], 

168) 

169 

170# Release views 

171publisher_snaps.add_url_rule( 

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

173 view_func=release_views.redirect_get_release_history, 

174) 

175publisher_snaps.add_url_rule( 

176 "/<snap_name>/release", 

177 view_func=release_views.redirect_get_release_history, 

178) 

179publisher_snaps.add_url_rule( 

180 "/<snap_name>/releases", 

181 view_func=release_views.get_releases, 

182 methods=["GET"], 

183) 

184publisher_snaps.add_url_rule( 

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

186 view_func=releases.get_release_history_data, 

187 methods=["GET"], 

188) 

189publisher_snaps.add_url_rule( 

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

191 view_func=release_views.redirect_post_release, 

192 methods=["POST"], 

193) 

194publisher_snaps.add_url_rule( 

195 "/<snap_name>/release", 

196 view_func=release_views.redirect_post_release, 

197 methods=["POST"], 

198) 

199publisher_snaps.add_url_rule( 

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

201 view_func=release_views.get_release_history_json, 

202) 

203publisher_snaps.add_url_rule( 

204 "/<snap_name>/releases", 

205 view_func=release_views.post_release, 

206 methods=["POST"], 

207) 

208publisher_snaps.add_url_rule( 

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

210 view_func=release_views.redirect_post_close_channel, 

211 methods=["POST"], 

212) 

213publisher_snaps.add_url_rule( 

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

215 view_func=release_views.post_close_channel, 

216 methods=["POST"], 

217) 

218publisher_snaps.add_url_rule( 

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

220 view_func=release_views.post_default_track, 

221 methods=["POST"], 

222) 

223publisher_snaps.add_url_rule( 

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

225 view_func=release_views.get_snap_revision_json, 

226) 

227 

228# Metrics views 

229publisher_snaps.add_url_rule( 

230 "/snaps/metrics/json", 

231 view_func=metrics_views.get_account_snaps_metrics, 

232 methods=["POST"], 

233) 

234publisher_snaps.add_url_rule( 

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

236 view_func=metrics_views.get_measure_snap, 

237) 

238publisher_snaps.add_url_rule( 

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

240 view_func=metrics_views.get_measure_snap, 

241) 

242publisher_snaps.add_url_rule( 

243 "/<snap_name>/metrics", 

244 view_func=metrics_views.publisher_snap_metrics, 

245) 

246 

247publisher_snaps.add_url_rule( 

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

249 view_func=metrics_views.get_active_devices, 

250) 

251 

252publisher_snaps.add_url_rule( 

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

254 view_func=metrics_views.get_latest_active_devices, 

255) 

256 

257publisher_snaps.add_url_rule( 

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

259 view_func=metrics_views.get_metric_annotaion, 

260) 

261 

262publisher_snaps.add_url_rule( 

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

264 view_func=metrics_views.get_country_metric, 

265) 

266 

267# Publice views 

268publisher_snaps.add_url_rule( 

269 "/<snap_name>/publicise", 

270 view_func=publicise_views.get_publicise, 

271) 

272publisher_snaps.add_url_rule( 

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

274 view_func=publicise_views.get_publicise, 

275) 

276publisher_snaps.add_url_rule( 

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

278 view_func=publicise_views.get_publicise, 

279) 

280publisher_snaps.add_url_rule( 

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

282 view_func=get_publicise_data, 

283) 

284 

285# Settings views 

286publisher_snaps.add_url_rule( 

287 "/<snap_name>/settings", 

288 view_func=settings_views.get_settings, 

289) 

290publisher_snaps.add_url_rule( 

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

292 view_func=post_settings_data, 

293 methods=["POST"], 

294) 

295publisher_snaps.add_url_rule( 

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

297 view_func=get_settings_data, 

298) 

299 

300# CVE API 

301publisher_snaps.add_url_rule( 

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

303 view_func=cves.get_cves, 

304) 

305 

306publisher_snaps.add_url_rule( 

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

308 view_func=cves.get_revisions_with_cves, 

309) 

310 

311 

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

313@login_required 

314def redirect_get_account_snaps(): 

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

316 

317 

318@publisher_snaps.route("/snaps") 

319@login_required 

320def get_account_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 context = { 

328 "snaps": user_snaps, 

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

330 "registered_snaps": registered_snaps, 

331 } 

332 

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

334 

335 

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

337@login_required 

338def get_user_snaps(): 

339 account_info = dashboard.get_account(flask.session) 

340 

341 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info) 

342 

343 flask_user = flask.session["publisher"] 

344 

345 return flask.jsonify( 

346 { 

347 "snaps": user_snaps, 

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

349 "registered_snaps": registered_snaps, 

350 } 

351 ) 

352 

353 

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

355@login_required 

356def get_snap_build_status(): 

357 try: 

358 account_info = dashboard.get_account(flask.session) 

359 except (StoreApiError, ApiError) as api_error: 

360 logging.getLogger("talisker.wsgi").error( 

361 "Error with session: %s", api_error 

362 ) 

363 

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

365 

366 response = [] 

367 user_snaps, _ = logic.get_snaps_account_info(account_info) 

368 

369 for snap_name in user_snaps: 

370 snap_build_statuses = launchpad.get_snap_build_status(snap_name) 

371 status = map_snap_build_status(snap_build_statuses) 

372 

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

374 

375 return flask.jsonify(response) 

376 

377 

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

379def redirect_get_register_name(): 

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

381 

382 

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

384@login_required 

385def get_register_name(): 

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

387 

388 

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

390def redirect_post_register_name(): 

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

392 

393 

394publisher_snaps.add_url_rule( 

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

396 view_func=get_package_metadata, 

397 methods=["GET"], 

398) 

399 

400publisher_snaps.add_url_rule( 

401 "/api/register-snap", 

402 view_func=post_register_name, 

403 methods=["POST"], 

404) 

405 

406publisher_snaps.add_url_rule( 

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

408 view_func=post_register_name_dispute, 

409 methods=["POST"], 

410) 

411 

412 

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

414@login_required 

415@exchange_required 

416def delete_package(package_name): 

417 response = publisher_gateway.unregister_package_name( 

418 flask.session, package_name 

419 ) 

420 

421 if response.status_code == 200: 

422 return ("", 200) 

423 return ( 

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

425 response.status_code, 

426 ) 

427 

428 

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

430@login_required 

431def get_is_user_snap(snap_name): 

432 is_users_snap = False 

433 try: 

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

435 except (StoreApiError, ApiError) as api_error: 

436 logging.getLogger("talisker.wsgi").error( 

437 "Error with session: %s", api_error 

438 ) 

439 

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

441 

442 if authentication.is_authenticated(flask.session): 

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

444 if ( 

445 publisher_info.get("nickname") 

446 == snap_info["publisher"]["username"] 

447 ): 

448 is_users_snap = True 

449 

450 return {"is_users_snap": is_users_snap} 

451 

452 

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

454@login_required 

455def post_register_name_json(): 

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

457 

458 if not snap_name: 

459 return ( 

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

461 400, 

462 ) 

463 

464 try: 

465 response = dashboard.post_register_name( 

466 session=flask.session, snap_name=snap_name 

467 ) 

468 except StoreApiResponseErrorList as api_response_error_list: 

469 for error in api_response_error_list.errors: 

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

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

472 return flask.jsonify( 

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

474 ) 

475 return ( 

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

477 api_response_error_list.status_code, 

478 ) 

479 

480 response["code"] = "created" 

481 

482 return flask.jsonify(response) 

483 

484 

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

486@login_required 

487def get_register_name_dispute(): 

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

489 

490 if not snap_name: 

491 return flask.redirect( 

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

493 ) 

494 return flask.render_template( 

495 "store/publisher.html", 

496 ) 

497 

498 

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

500@login_required 

501def get_request_reserved_name(): 

502 stores = dashboard.get_stores(flask.session) 

503 

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

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

506 store_name = logic.get_store_name(store_id, stores) 

507 

508 if not snap_name: 

509 return flask.redirect( 

510 flask.url_for( 

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

512 ) 

513 ) 

514 return flask.render_template( 

515 "store/publisher.html", 

516 snap_name=snap_name, 

517 store=store_name, 

518 ) 

519 

520 

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

522@login_required 

523def snap_count(): 

524 account_info = dashboard.get_account(flask.session) 

525 

526 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info) 

527 

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

529 

530 return flask.jsonify(context)