Coverage for webapp/publisher/views.py: 82%

283 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-27 22:07 +0000

1from canonicalwebteam.exceptions import StoreApiResponseErrorList 

2from flask import ( 

3 Blueprint, 

4 flash, 

5 redirect, 

6 render_template, 

7 request, 

8 session, 

9 url_for, 

10 make_response, 

11) 

12from flask.json import jsonify 

13from webapp.config import DETAILS_VIEW_REGEX 

14from webapp.decorators import login_required, cached_redirect 

15from webapp.publisher.logic import get_all_architectures, process_releases 

16from webapp.observability.utils import trace_function 

17from webapp.store_api import publisher_gateway 

18 

19publisher = Blueprint( 

20 "publisher", 

21 __name__, 

22 template_folder="/templates", 

23 static_folder="/static", 

24) 

25 

26 

27@trace_function 

28@publisher.route("/account/details") 

29@login_required 

30def get_account_details(): 

31 return render_template("publisher/account-details.html") 

32 

33 

34@trace_function 

35@publisher.route( 

36 '/<regex("' 

37 + DETAILS_VIEW_REGEX 

38 + '"):entity_name>/' 

39 + '<regex("listing|releases|publicise|collaboration|settings"):path>' 

40) 

41@login_required 

42def get_publisher(entity_name, path): 

43 session["developer_token"] = session["account-auth"] 

44 package = publisher_gateway.get_package_metadata(session, entity_name) 

45 

46 context = { 

47 "package": package, 

48 } 

49 

50 return render_template("publisher/publisher.html", **context) 

51 

52 

53@trace_function 

54@publisher.route( 

55 '/api/packages/<regex("' + DETAILS_VIEW_REGEX + '"):entity_name>', 

56) 

57@login_required 

58def get_package(entity_name): 

59 session["developer_token"] = session["account-auth"] 

60 package = publisher_gateway.get_package_metadata(session, entity_name) 

61 

62 return jsonify({"data": package, "success": True}) 

63 

64 

65@trace_function 

66@publisher.route( 

67 '/api/packages/<regex("' + DETAILS_VIEW_REGEX + '"):entity_name>', 

68 methods=["PATCH"], 

69) 

70@login_required 

71def update_package(entity_name): 

72 payload = request.get_json() 

73 

74 res = {} 

75 

76 try: 

77 package = publisher_gateway.update_package_metadata( 

78 session["account-auth"], "charm", entity_name, payload 

79 ) 

80 res["data"] = package 

81 res["success"] = True 

82 res["message"] = "" 

83 response = make_response(res, 200) 

84 except StoreApiResponseErrorList as error_list: 

85 error_messages = [ 

86 f"{error.get('message', 'Unable to update this charm or bundle')}" 

87 for error in error_list.errors 

88 ] 

89 if "unauthorized" in error_messages: 

90 res["message"] = "Package not found" 

91 else: 

92 res["message"] = " ".join(error_messages) 

93 res["success"] = False 

94 response = make_response(res, 500) 

95 

96 return response 

97 

98 

99@trace_function 

100@publisher.route("/charms") 

101@publisher.route("/bundles") 

102@login_required 

103def list_page(): 

104 publisher_charms = publisher_gateway.get_account_packages( 

105 session["account-auth"], "charm", include_collaborations=True 

106 ) 

107 

108 page_type = request.path[1:-1] 

109 

110 context = { 

111 "published": [ 

112 {**c, "is_owner": c["publisher"]["id"] == session["account"]["id"]} 

113 for c in publisher_charms 

114 if c["status"] == "published" and c["type"] == page_type 

115 ], 

116 "registered": [ 

117 {**c, "is_owner": c["publisher"]["id"] == session["account"]["id"]} 

118 for c in publisher_charms 

119 if c["status"] == "registered" and c["type"] == page_type 

120 ], 

121 "page_type": page_type, 

122 } 

123 

124 return render_template("publisher/list.html", **context) 

125 

126 

127@trace_function 

128@publisher.route("/accept-invite") 

129@login_required 

130@cached_redirect 

131def accept_invite(): 

132 return render_template("publisher/accept-invite.html") 

133 

134 

135@trace_function 

136@publisher.route("/accept-invite", methods=["POST"]) 

137@login_required 

138def accept_post_invite(): 

139 res = {} 

140 

141 try: 

142 token = request.form.get("token") 

143 package = request.form.get("package") 

144 response = publisher_gateway.accept_invite( 

145 session["account-auth"], package, token 

146 ) 

147 

148 if response.status_code == 204: 

149 res["success"] = True 

150 return make_response(res, 200) 

151 else: 

152 res["success"] = False 

153 errors = response.json().get("error-list", []) 

154 res["message"] = ( 

155 errors[0].get("message") if errors else "Unknown error" 

156 ) 

157 return make_response(res, 500) 

158 

159 except StoreApiResponseErrorList as error_list: 

160 res["success"] = False 

161 error_messages = [ 

162 f"{error.get('message', 'An error occured')}" 

163 for error in error_list.errors 

164 ] 

165 res["message"] = " ".join(error_messages) 

166 except Exception: 

167 res["success"] = False 

168 res["message"] = "An error occured" 

169 

170 return make_response(res, 500) 

171 

172 

173@trace_function 

174@publisher.route("/reject-invite", methods=["POST"]) 

175@login_required 

176def reject_post_invite(): 

177 res = {} 

178 

179 try: 

180 token = request.form.get("token") 

181 package = request.form.get("package") 

182 response = publisher_gateway.reject_invite( 

183 session["account-auth"], package, token 

184 ) 

185 

186 if response.status_code == 204: 

187 res["success"] = True 

188 return make_response(res, 200) 

189 else: 

190 res["success"] = False 

191 res["message"] = "An error occured" 

192 return make_response(res, 200) 

193 

194 except StoreApiResponseErrorList as error_list: 

195 res["success"] = False 

196 error_messages = [ 

197 f"{error.get('message', 'An error occured')}" 

198 for error in error_list.errors 

199 ] 

200 res["message"] = " ".join(error_messages) 

201 response = make_response(res, 500) 

202 except Exception: 

203 res["success"] = False 

204 res["message"] = "An error occured" 

205 

206 return response 

207 

208 

209@trace_function 

210@publisher.route( 

211 '/api/packages/<regex("' 

212 + DETAILS_VIEW_REGEX 

213 + '"):entity_name>/collaborators', 

214) 

215@login_required 

216def get_collaborators(entity_name): 

217 res = {} 

218 

219 try: 

220 collaborators = publisher_gateway.get_collaborators( 

221 session["account-auth"], entity_name 

222 ) 

223 res["success"] = True 

224 res["data"] = collaborators 

225 response = make_response(res, 200) 

226 except StoreApiResponseErrorList as error_list: 

227 error_messages = [ 

228 f"{error.get('message', 'An error occured')}" 

229 for error in error_list.errors 

230 ] 

231 res["message"] = " ".join(error_messages) 

232 res["success"] = False 

233 response = make_response(res, 500) 

234 

235 return response 

236 

237 

238@trace_function 

239@publisher.route( 

240 '/api/packages/<regex("' + DETAILS_VIEW_REGEX + '"):entity_name>/invites', 

241) 

242@login_required 

243def get_pending_invites(entity_name): 

244 res = {} 

245 

246 try: 

247 invites = publisher_gateway.get_pending_invites( 

248 session["account-auth"], entity_name 

249 ) 

250 res["success"] = True 

251 res["data"] = invites["invites"] 

252 response = make_response(res, 200) 

253 except StoreApiResponseErrorList as error_list: 

254 error_messages = [ 

255 f"{error.get('message', 'An error occured')}" 

256 for error in error_list.errors 

257 ] 

258 res["message"] = " ".join(error_messages) 

259 res["success"] = False 

260 response = make_response(res, 500) 

261 

262 return response 

263 

264 

265@trace_function 

266@publisher.route( 

267 '/api/packages/<regex("' + DETAILS_VIEW_REGEX + '"):entity_name>/invites', 

268 methods=["POST"], 

269) 

270@login_required 

271def invite_collaborators(entity_name): 

272 res = {} 

273 

274 try: 

275 collaborators = request.form.get("collaborators") 

276 result = publisher_gateway.invite_collaborators( 

277 session["account-auth"], entity_name, [collaborators] 

278 ) 

279 res["success"] = True 

280 res["data"] = result["tokens"] 

281 return make_response(res, 200) 

282 except StoreApiResponseErrorList as error_list: 

283 res["success"] = False 

284 messages = [ 

285 f"{error.get('message', 'An error occurred')}" 

286 for error in error_list.errors 

287 ] 

288 res["message"] = (" ").join(messages) 

289 except Exception: 

290 res["success"] = False 

291 res["message"] = "An error occurred" 

292 

293 return make_response(res, 500) 

294 

295 

296@trace_function 

297@publisher.route( 

298 '/api/packages/<regex("' + DETAILS_VIEW_REGEX + '"):entity_name>/invites', 

299 methods=["DELETE"], 

300) 

301@login_required 

302def revoke_invite(entity_name): 

303 res = {} 

304 

305 try: 

306 collaborator = request.form.get("collaborator") 

307 response = publisher_gateway.revoke_invites( 

308 session["account-auth"], entity_name, [collaborator] 

309 ) 

310 

311 if response.status_code == 204: 

312 res["success"] = True 

313 return make_response(res, 200) 

314 else: 

315 res["success"] = False 

316 res["message"] = "An error occurred" 

317 return make_response(res, 500) 

318 

319 except StoreApiResponseErrorList as error_list: 

320 res["success"] = False 

321 messages = [ 

322 f"{error.get('message', 'An error occurred')}" 

323 for error in error_list.errors 

324 ] 

325 res["message"] = (" ").join(messages) 

326 

327 return make_response(res, 500) 

328 

329 

330@trace_function 

331@publisher.route("/register-name") 

332@login_required 

333def register_name(): 

334 entity_name = request.args.get("entity_name", default="", type=str) 

335 

336 invalid_name_str = request.args.get( 

337 "invalid_name", default="False", type=str 

338 ) 

339 invalid_name = invalid_name_str == "True" 

340 

341 reserved_name_str = request.args.get( 

342 "reserved_name", default="False", type=str 

343 ) 

344 reserved_name = reserved_name_str == "True" 

345 

346 already_registered_str = request.args.get( 

347 "already_registered", default="False", type=str 

348 ) 

349 already_registered = already_registered_str == "True" 

350 

351 already_owned_str = request.args.get( 

352 "already_owned", default="False", type=str 

353 ) 

354 already_owned = already_owned_str == "True" 

355 

356 context = { 

357 "entity_name": entity_name, 

358 "reserved_name": reserved_name, 

359 "invalid_name": invalid_name, 

360 "already_owned": already_owned, 

361 "already_registered": already_registered, 

362 } 

363 return render_template("publisher/register-name.html", **context) 

364 

365 

366@trace_function 

367@publisher.route("/register-name", methods=["POST"]) 

368@login_required 

369def post_register_name(): 

370 VALID_TYPES = {"charm", "bundle"} 

371 data = { 

372 "name": request.form["name"], 

373 "type": request.form["type"], 

374 "private": True if request.form.get("private") == "private" else False, 

375 } 

376 

377 if data["type"] not in VALID_TYPES: 

378 flash("Invalid type specified.", "negative") 

379 return redirect(url_for(".register_name")) 

380 

381 try: 

382 result = publisher_gateway.register_package_name( 

383 session["account-auth"], data 

384 ) 

385 if result: 

386 flash( 

387 f"Your {data['type']} name has been successfully registered.", 

388 "positive", 

389 ) 

390 except StoreApiResponseErrorList as api_response_error_list: 

391 for error in api_response_error_list.errors: 

392 if error["code"] == "api-error": 

393 return redirect( 

394 url_for( 

395 ".register_name", 

396 entity_name=data["name"], 

397 invalid_name=True, 

398 ) 

399 ) 

400 elif error["code"] == "reserved-name": 

401 return redirect( 

402 url_for( 

403 ".register_name", 

404 entity_name=data["name"], 

405 reserved_name=True, 

406 ) 

407 ) 

408 elif error["code"] == "already-registered": 

409 return redirect( 

410 url_for( 

411 ".register_name", 

412 entity_name=data["name"], 

413 already_registered=True, 

414 ) 

415 ) 

416 elif error["code"] == "already-owned": 

417 return redirect( 

418 url_for( 

419 ".register_name", 

420 entity_name=data["name"], 

421 already_owned=True, 

422 ) 

423 ) 

424 

425 if data["type"] == "charm": 

426 return redirect("/charms") 

427 elif data["type"] == "bundle": 

428 return redirect("/bundles") 

429 

430 

431@trace_function 

432@publisher.route("/register-name-dispute") 

433@login_required 

434def register_name_dispute(): 

435 entity_name = request.args.get("entity-name", type=str) 

436 

437 if not entity_name: 

438 return redirect(url_for(".register_name", entity_name=entity_name)) 

439 

440 return render_template( 

441 "publisher/register-name-dispute/index.html", entity_name=entity_name 

442 ) 

443 

444 

445@trace_function 

446@publisher.route("/register-name-dispute/thank-you") 

447@login_required 

448def register_name_dispute_thank_you(): 

449 entity_name = request.args.get("entity-name", type=str) 

450 

451 if not entity_name: 

452 return redirect(url_for(".register_name", entity_name=entity_name)) 

453 

454 return render_template( 

455 "publisher/register-name-dispute/thank-you.html", 

456 entity_name=entity_name, 

457 ) 

458 

459 

460@trace_function 

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

462@login_required 

463def delete_package(package_name): 

464 resp = publisher_gateway.unregister_package_name( 

465 session["account-auth"], package_name 

466 ) 

467 if resp.status_code == 200: 

468 return ("", 200) 

469 return ( 

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

471 resp.status_code, 

472 ) 

473 

474 

475@trace_function 

476@publisher.route("/<charm_name>/create-track", methods=["POST"]) 

477@login_required 

478def post_create_track(charm_name): 

479 track_name = request.form.get("track-name") 

480 version_pattern = request.form.get("version-pattern") 

481 auto_phasing_percentage = request.form.get("auto-phasing-percentage") 

482 

483 if auto_phasing_percentage is not None: 

484 auto_phasing_percentage = float(auto_phasing_percentage) 

485 

486 response = publisher_gateway.create_track( 

487 session, 

488 charm_name, 

489 track_name, 

490 version_pattern, 

491 auto_phasing_percentage, 

492 ) 

493 if response.status_code == 201: 

494 return response.json(), response.status_code 

495 if response.status_code == 409: 

496 return ( 

497 jsonify({"error": "Track already exists."}), 

498 response.status_code, 

499 ) 

500 if "error-list" in response.json(): 

501 return ( 

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

503 response.status_code, 

504 ) 

505 return response.json(), response.status_code 

506 

507 

508@trace_function 

509@publisher.route( 

510 '/api/packages/<regex("' + DETAILS_VIEW_REGEX + '"):entity_name>/releases', 

511) 

512@login_required 

513def get_releases(entity_name: str): 

514 res = {} 

515 

516 try: 

517 release_data = publisher_gateway.get_releases( 

518 session["account-auth"], entity_name 

519 ) 

520 res["success"] = True 

521 

522 res["data"] = {} 

523 

524 res["data"]["releases"] = process_releases( 

525 release_data["channel-map"], 

526 release_data["package"]["channels"], 

527 release_data["revisions"], 

528 ) 

529 res["data"]["all_architectures"] = get_all_architectures( 

530 res["data"]["releases"] 

531 ) 

532 response = make_response(res, 200) 

533 

534 except StoreApiResponseErrorList as error_list: 

535 error_messages = [ 

536 f"{error.get('message', 'An error occured')}" 

537 for error in error_list.errors 

538 ] 

539 res["message"] = " ".join(error_messages) 

540 res["success"] = False 

541 response = make_response(res, 500) 

542 

543 return response