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

178 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-14 22:07 +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 exchange_required, login_required 

16from webapp.endpoints.publisher import listing as listing_endpoint 

17from webapp.endpoints import cves 

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.endpoints.publisher.builds import ( 

29 get_snap_build_page, 

30 get_validate_repo, 

31 post_build, 

32 post_disconnect_repo, 

33) 

34from webapp.endpoints.publisher.settings import ( 

35 get_settings_data, 

36 post_settings_data, 

37) 

38from webapp.endpoints.publisher.publicise import get_publicise_data 

39from webapp.endpoints.publisher.packages import get_package_metadata 

40from webapp.endpoints.publisher.register import ( 

41 post_register_name, 

42 post_register_name_dispute, 

43) 

44from webapp.endpoints import releases, builds 

45from webapp.publisher.snaps.builds import map_snap_build_status 

46 

47dashboard = Dashboard(api_publisher_session) 

48publisher_gateway = PublisherGW("snap", api_publisher_session) 

49 

50 

51publisher_snaps = flask.Blueprint( 

52 "publisher_snaps", 

53 __name__, 

54 template_folder="/templates", 

55 static_folder="/static", 

56) 

57 

58# Listing views 

59publisher_snaps.add_url_rule( 

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

61 view_func=listing_views.get_market_snap, 

62) 

63publisher_snaps.add_url_rule( 

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

65 view_func=listing_views.get_market_snap, 

66 methods=["GET"], 

67) 

68publisher_snaps.add_url_rule( 

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

70 view_func=listing_views.redirect_post_market_snap, 

71 methods=["POST"], 

72) 

73publisher_snaps.add_url_rule( 

74 "/<snap_name>/listing", 

75 view_func=listing_views.get_listing_snap, 

76 methods=["GET"], 

77) 

78publisher_snaps.add_url_rule( 

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

80 view_func=listing_endpoint.get_listing_data, 

81 methods=["GET"], 

82) 

83publisher_snaps.add_url_rule( 

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

85 view_func=listing_endpoint.post_listing_data, 

86 methods=["POST"], 

87) 

88publisher_snaps.add_url_rule( 

89 "/<snap_name>/preview", 

90 view_func=listing_views.post_preview, 

91 methods=["POST"], 

92) 

93publisher_snaps.add_url_rule( 

94 "/<snap_name>/collaboration", 

95 view_func=collaboration_views.get_collaboration_snap, 

96 methods=["GET"], 

97) 

98 

99# Build views 

100publisher_snaps.add_url_rule( 

101 "/<snap_name>/builds", 

102 view_func=build_views.get_snap_builds_page, 

103 methods=["GET"], 

104), 

105 

106publisher_snaps.add_url_rule( 

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

108 view_func=get_snap_build_page, 

109 methods=["GET"], 

110), 

111 

112publisher_snaps.add_url_rule( 

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

114 view_func=builds.get_snap_repo, 

115 methods=["GET"], 

116) 

117publisher_snaps.add_url_rule( 

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

119 view_func=build_views.get_snap_builds, 

120 methods=["GET"], 

121) 

122publisher_snaps.add_url_rule( 

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

124 view_func=build_views.post_snap_builds, 

125 methods=["POST"], 

126) 

127publisher_snaps.add_url_rule( 

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

129 view_func=build_views.get_snap_build, 

130 methods=["GET"], 

131) 

132publisher_snaps.add_url_rule( 

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

134 view_func=get_validate_repo, 

135 methods=["GET"], 

136) 

137publisher_snaps.add_url_rule( 

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

139 view_func=post_build, 

140 methods=["POST"], 

141) 

142publisher_snaps.add_url_rule( 

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

144 view_func=build_views.check_build_request, 

145 methods=["GET"], 

146) 

147publisher_snaps.add_url_rule( 

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

149 view_func=build_views.post_github_webhook, 

150 methods=["POST"], 

151) 

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

153publisher_snaps.add_url_rule( 

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

155 view_func=build_views.post_github_webhook, 

156 methods=["POST"], 

157) 

158publisher_snaps.add_url_rule( 

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

160 view_func=build_views.get_update_gh_webhooks, 

161 methods=["GET"], 

162) 

163publisher_snaps.add_url_rule( 

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

165 view_func=post_disconnect_repo, 

166 methods=["POST"], 

167) 

168 

169# Release views 

170publisher_snaps.add_url_rule( 

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

172 view_func=release_views.redirect_get_release_history, 

173) 

174publisher_snaps.add_url_rule( 

175 "/<snap_name>/release", 

176 view_func=release_views.redirect_get_release_history, 

177) 

178publisher_snaps.add_url_rule( 

179 "/<snap_name>/releases", 

180 view_func=release_views.get_releases, 

181 methods=["GET"], 

182) 

183publisher_snaps.add_url_rule( 

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

185 view_func=releases.get_release_history_data, 

186 methods=["GET"], 

187) 

188publisher_snaps.add_url_rule( 

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

190 view_func=release_views.redirect_post_release, 

191 methods=["POST"], 

192) 

193publisher_snaps.add_url_rule( 

194 "/<snap_name>/release", 

195 view_func=release_views.redirect_post_release, 

196 methods=["POST"], 

197) 

198publisher_snaps.add_url_rule( 

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

200 view_func=release_views.get_release_history_json, 

201) 

202publisher_snaps.add_url_rule( 

203 "/<snap_name>/releases", 

204 view_func=release_views.post_release, 

205 methods=["POST"], 

206) 

207publisher_snaps.add_url_rule( 

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

209 view_func=release_views.redirect_post_close_channel, 

210 methods=["POST"], 

211) 

212publisher_snaps.add_url_rule( 

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

214 view_func=release_views.post_close_channel, 

215 methods=["POST"], 

216) 

217publisher_snaps.add_url_rule( 

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

219 view_func=release_views.post_default_track, 

220 methods=["POST"], 

221) 

222publisher_snaps.add_url_rule( 

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

224 view_func=release_views.get_snap_revision_json, 

225) 

226 

227# Metrics views 

228publisher_snaps.add_url_rule( 

229 "/snaps/metrics/json", 

230 view_func=metrics_views.get_account_snaps_metrics, 

231 methods=["POST"], 

232) 

233publisher_snaps.add_url_rule( 

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

235 view_func=metrics_views.get_measure_snap, 

236) 

237publisher_snaps.add_url_rule( 

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

239 view_func=metrics_views.get_measure_snap, 

240) 

241publisher_snaps.add_url_rule( 

242 "/<snap_name>/metrics", 

243 view_func=metrics_views.publisher_snap_metrics, 

244) 

245 

246publisher_snaps.add_url_rule( 

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

248 view_func=metrics_views.get_active_devices, 

249) 

250 

251publisher_snaps.add_url_rule( 

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

253 view_func=metrics_views.get_latest_active_devices, 

254) 

255 

256publisher_snaps.add_url_rule( 

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

258 view_func=metrics_views.get_metric_annotaion, 

259) 

260 

261publisher_snaps.add_url_rule( 

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

263 view_func=metrics_views.get_country_metric, 

264) 

265 

266# Publice views 

267publisher_snaps.add_url_rule( 

268 "/<snap_name>/publicise", 

269 view_func=publicise_views.get_publicise, 

270) 

271publisher_snaps.add_url_rule( 

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

273 view_func=publicise_views.get_publicise, 

274) 

275publisher_snaps.add_url_rule( 

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

277 view_func=publicise_views.get_publicise, 

278) 

279publisher_snaps.add_url_rule( 

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

281 view_func=get_publicise_data, 

282) 

283 

284# Settings views 

285publisher_snaps.add_url_rule( 

286 "/<snap_name>/settings", 

287 view_func=settings_views.get_settings, 

288) 

289publisher_snaps.add_url_rule( 

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

291 view_func=post_settings_data, 

292 methods=["POST"], 

293) 

294publisher_snaps.add_url_rule( 

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

296 view_func=get_settings_data, 

297) 

298 

299# CVE API 

300publisher_snaps.add_url_rule( 

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

302 view_func=cves.get_cves, 

303) 

304 

305publisher_snaps.add_url_rule( 

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

307 view_func=cves.get_revisions_with_cves, 

308) 

309 

310 

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

312@login_required 

313def redirect_get_account_snaps(): 

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

315 

316 

317@publisher_snaps.route("/snaps") 

318@login_required 

319def get_account_snaps(): 

320 account_info = dashboard.get_account(flask.session) 

321 

322 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info) 

323 

324 flask_user = flask.session["publisher"] 

325 

326 context = { 

327 "snaps": user_snaps, 

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

329 "registered_snaps": registered_snaps, 

330 } 

331 

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

333 

334 

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

336@login_required 

337def get_user_snaps(): 

338 account_info = dashboard.get_account(flask.session) 

339 

340 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info) 

341 

342 flask_user = flask.session["publisher"] 

343 

344 return flask.jsonify( 

345 { 

346 "snaps": user_snaps, 

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

348 "registered_snaps": registered_snaps, 

349 } 

350 ) 

351 

352 

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

354@login_required 

355def get_snap_build_status(): 

356 try: 

357 account_info = dashboard.get_account(flask.session) 

358 except (StoreApiError, ApiError): 

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

360 

361 response = [] 

362 user_snaps, _ = logic.get_snaps_account_info(account_info) 

363 

364 for snap_name in user_snaps: 

365 snap_build_statuses = launchpad.get_snap_build_status(snap_name) 

366 status = map_snap_build_status(snap_build_statuses) 

367 

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

369 

370 return flask.jsonify(response) 

371 

372 

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

374def redirect_get_register_name(): 

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

376 

377 

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

379@login_required 

380def get_register_name(): 

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

382 

383 

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

385def redirect_post_register_name(): 

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

387 

388 

389publisher_snaps.add_url_rule( 

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

391 view_func=get_package_metadata, 

392 methods=["GET"], 

393) 

394 

395publisher_snaps.add_url_rule( 

396 "/api/register-snap", 

397 view_func=post_register_name, 

398 methods=["POST"], 

399) 

400 

401publisher_snaps.add_url_rule( 

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

403 view_func=post_register_name_dispute, 

404 methods=["POST"], 

405) 

406 

407 

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

409@login_required 

410@exchange_required 

411def delete_package(package_name): 

412 response = publisher_gateway.unregister_package_name( 

413 flask.session, package_name 

414 ) 

415 

416 if response.status_code == 200: 

417 return ("", 200) 

418 return ( 

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

420 response.status_code, 

421 ) 

422 

423 

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

425@login_required 

426def get_is_user_snap(snap_name): 

427 is_users_snap = False 

428 try: 

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

430 except (StoreApiError, ApiError): 

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

432 

433 if authentication.is_authenticated(flask.session): 

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

435 if ( 

436 publisher_info.get("nickname") 

437 == snap_info["publisher"]["username"] 

438 ): 

439 is_users_snap = True 

440 

441 return {"is_users_snap": is_users_snap} 

442 

443 

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

445@login_required 

446def post_register_name_json(): 

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

448 

449 if not snap_name: 

450 return ( 

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

452 400, 

453 ) 

454 

455 try: 

456 response = dashboard.post_register_name( 

457 session=flask.session, snap_name=snap_name 

458 ) 

459 except StoreApiResponseErrorList as api_response_error_list: 

460 for error in api_response_error_list.errors: 

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

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

463 return flask.jsonify( 

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

465 ) 

466 return ( 

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

468 api_response_error_list.status_code, 

469 ) 

470 

471 response["code"] = "created" 

472 

473 return flask.jsonify(response) 

474 

475 

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

477@login_required 

478def get_register_name_dispute(): 

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

480 

481 if not snap_name: 

482 return flask.redirect( 

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

484 ) 

485 return flask.render_template( 

486 "store/publisher.html", 

487 ) 

488 

489 

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

491@login_required 

492def get_request_reserved_name(): 

493 stores = dashboard.get_stores(flask.session) 

494 

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

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

497 store_name = logic.get_store_name(store_id, stores) 

498 

499 if not snap_name: 

500 return flask.redirect( 

501 flask.url_for( 

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

503 ) 

504 ) 

505 return flask.render_template( 

506 "store/publisher.html", 

507 snap_name=snap_name, 

508 store=store_name, 

509 ) 

510 

511 

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

513@login_required 

514def snap_count(): 

515 account_info = dashboard.get_account(flask.session) 

516 

517 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info) 

518 

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

520 

521 return flask.jsonify(context)