Coverage for webapp/publisher/snaps/views.py: 72%
228 statements
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-05 22:06 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-05 22:06 +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 StoreApiResponseError,
10 StoreApiResourceNotFound,
11)
12from flask.json import jsonify
13from talisker import logging
15# Local
16from webapp import authentication
17from webapp.helpers import api_publisher_session, launchpad
18from webapp.api.exceptions import ApiError
19from webapp.decorators import exchange_required, login_required
20from webapp.publisher.cve import cve_views
21from webapp.publisher.snaps import (
22 build_views,
23 listing_views,
24 logic,
25 metrics_views,
26 publicise_views,
27 release_views,
28 settings_views,
29 collaboration_views,
30)
31from webapp.publisher.snaps.builds import map_snap_build_status
33dashboard = Dashboard(api_publisher_session)
34publisher_gateway = PublisherGW("snap", api_publisher_session)
37publisher_snaps = flask.Blueprint(
38 "publisher_snaps",
39 __name__,
40 template_folder="/templates",
41 static_folder="/static",
42)
44# Listing views
45publisher_snaps.add_url_rule(
46 "/account/snaps/<snap_name>/market",
47 view_func=listing_views.get_market_snap,
48)
49publisher_snaps.add_url_rule(
50 "/account/snaps/<snap_name>/listing",
51 view_func=listing_views.get_market_snap,
52 methods=["GET"],
53)
54publisher_snaps.add_url_rule(
55 "/account/snaps/<snap_name>/listing",
56 view_func=listing_views.redirect_post_market_snap,
57 methods=["POST"],
58)
59publisher_snaps.add_url_rule(
60 "/<snap_name>/listing",
61 view_func=listing_views.get_listing_snap,
62 methods=["GET"],
63)
64publisher_snaps.add_url_rule(
65 "/api/<snap_name>/listing",
66 view_func=listing_views.get_listing_data,
67 methods=["GET"],
68)
69publisher_snaps.add_url_rule(
70 "/api/<snap_name>/listing",
71 view_func=listing_views.post_listing_data,
72 methods=["POST"],
73)
74publisher_snaps.add_url_rule(
75 "/<snap_name>/preview",
76 view_func=listing_views.post_preview,
77 methods=["POST"],
78)
79publisher_snaps.add_url_rule(
80 "/<snap_name>/collaboration",
81 view_func=collaboration_views.get_collaboration_snap,
82 methods=["GET"],
83)
85# Build views
86publisher_snaps.add_url_rule(
87 "/<snap_name>/builds",
88 view_func=build_views.get_snap_builds_page,
89 methods=["GET"],
90),
92publisher_snaps.add_url_rule(
93 "/<snap_name>/builds/<build_id>",
94 view_func=build_views.get_snap_build_page,
95 methods=["GET"],
96),
98publisher_snaps.add_url_rule(
99 "/api/<snap_name>/repo",
100 view_func=build_views.get_snap_repo,
101 methods=["GET"],
102)
103publisher_snaps.add_url_rule(
104 "/api/<snap_name>/builds",
105 view_func=build_views.get_snap_builds,
106 methods=["GET"],
107)
108publisher_snaps.add_url_rule(
109 "/api/<snap_name>/builds",
110 view_func=build_views.post_snap_builds,
111 methods=["POST"],
112)
113publisher_snaps.add_url_rule(
114 "/api/<snap_name>/builds/<build_id>",
115 view_func=build_views.get_snap_build,
116 methods=["GET"],
117)
118publisher_snaps.add_url_rule(
119 "/api/<snap_name>/builds/validate-repo",
120 view_func=build_views.get_validate_repo,
121 methods=["GET"],
122)
123publisher_snaps.add_url_rule(
124 "/api/<snap_name>/builds/trigger-build",
125 view_func=build_views.post_build,
126 methods=["POST"],
127)
128publisher_snaps.add_url_rule(
129 "/api/<snap_name>/builds/check-build-request/<build_id>",
130 view_func=build_views.check_build_request,
131 methods=["GET"],
132)
133publisher_snaps.add_url_rule(
134 "/api/<snap_name>/webhook/notify",
135 view_func=build_views.post_github_webhook,
136 methods=["POST"],
137)
138# This route is to support previous webhooks from build.snapcraft.io
139publisher_snaps.add_url_rule(
140 "/api/<github_owner>/<github_repo>/webhook/notify",
141 view_func=build_views.post_github_webhook,
142 methods=["POST"],
143)
144publisher_snaps.add_url_rule(
145 "/api/<snap_name>/builds/update-webhook",
146 view_func=build_views.get_update_gh_webhooks,
147 methods=["GET"],
148)
149publisher_snaps.add_url_rule(
150 "/api/<snap_name>/builds/disconnect/",
151 view_func=build_views.post_disconnect_repo,
152 methods=["POST"],
153)
155# Release views
156publisher_snaps.add_url_rule(
157 "/account/snaps/<snap_name>/release",
158 view_func=release_views.redirect_get_release_history,
159)
160publisher_snaps.add_url_rule(
161 "/<snap_name>/release",
162 view_func=release_views.redirect_get_release_history,
163)
164publisher_snaps.add_url_rule(
165 "/<snap_name>/releases",
166 view_func=release_views.get_releases,
167 methods=["GET"],
168)
169publisher_snaps.add_url_rule(
170 "/api/<snap_name>/releases",
171 view_func=release_views.get_release_history_data,
172 methods=["GET"],
173)
174publisher_snaps.add_url_rule(
175 "/account/snaps/<snap_name>/release",
176 view_func=release_views.redirect_post_release,
177 methods=["POST"],
178)
179publisher_snaps.add_url_rule(
180 "/<snap_name>/release",
181 view_func=release_views.redirect_post_release,
182 methods=["POST"],
183)
184publisher_snaps.add_url_rule(
185 "/<snap_name>/releases/json",
186 view_func=release_views.get_release_history_json,
187)
188publisher_snaps.add_url_rule(
189 "/<snap_name>/releases",
190 view_func=release_views.post_release,
191 methods=["POST"],
192)
193publisher_snaps.add_url_rule(
194 "/<snap_name>/release/close-channel",
195 view_func=release_views.redirect_post_close_channel,
196 methods=["POST"],
197)
198publisher_snaps.add_url_rule(
199 "/<snap_name>/releases/close-channel",
200 view_func=release_views.post_close_channel,
201 methods=["POST"],
202)
203publisher_snaps.add_url_rule(
204 "/<snap_name>/releases/default-track",
205 view_func=release_views.post_default_track,
206 methods=["POST"],
207)
208publisher_snaps.add_url_rule(
209 "/<snap_name>/releases/revision/<revision>",
210 view_func=release_views.get_snap_revision_json,
211)
213# Metrics views
214publisher_snaps.add_url_rule(
215 "/snaps/metrics/json",
216 view_func=metrics_views.get_account_snaps_metrics,
217 methods=["POST"],
218)
219publisher_snaps.add_url_rule(
220 "/account/snaps/<snap_name>/measure",
221 view_func=metrics_views.get_measure_snap,
222)
223publisher_snaps.add_url_rule(
224 "/account/snaps/<snap_name>/metrics",
225 view_func=metrics_views.get_measure_snap,
226)
227publisher_snaps.add_url_rule(
228 "/<snap_name>/metrics",
229 view_func=metrics_views.publisher_snap_metrics,
230)
232publisher_snaps.add_url_rule(
233 "/<snap_name>/metrics/active-devices",
234 view_func=metrics_views.get_active_devices,
235)
237publisher_snaps.add_url_rule(
238 "/<snap_name>/metrics/active-latest-devices",
239 view_func=metrics_views.get_latest_active_devices,
240)
242publisher_snaps.add_url_rule(
243 "/<snap_name>/metrics/active-device-annotation",
244 view_func=metrics_views.get_metric_annotaion,
245)
247publisher_snaps.add_url_rule(
248 "/<snap_name>/metrics/country-metric",
249 view_func=metrics_views.get_country_metric,
250)
252# Publice views
253publisher_snaps.add_url_rule(
254 "/<snap_name>/publicise",
255 view_func=publicise_views.get_publicise,
256)
257publisher_snaps.add_url_rule(
258 "/<snap_name>/publicise/badges",
259 view_func=publicise_views.get_publicise,
260)
261publisher_snaps.add_url_rule(
262 "/<snap_name>/publicise/cards",
263 view_func=publicise_views.get_publicise,
264)
265publisher_snaps.add_url_rule(
266 "/api/<snap_name>/publicise",
267 view_func=publicise_views.get_publicise_data,
268)
270# Settings views
271publisher_snaps.add_url_rule(
272 "/<snap_name>/settings",
273 view_func=settings_views.get_settings,
274)
275publisher_snaps.add_url_rule(
276 "/api/<snap_name>/settings",
277 view_func=settings_views.post_settings_data,
278 methods=["POST"],
279)
280publisher_snaps.add_url_rule(
281 "/api/<snap_name>/settings",
282 view_func=settings_views.get_settings_data,
283)
285# CVE API
286publisher_snaps.add_url_rule(
287 "/api/<snap_name>/<revision>/cves",
288 view_func=cve_views.get_cves,
289)
291publisher_snaps.add_url_rule(
292 "/api/<snap_name>/cves",
293 view_func=cve_views.get_revisions_with_cves,
294)
297@publisher_snaps.route("/account/snaps")
298@login_required
299def redirect_get_account_snaps():
300 return flask.redirect(flask.url_for(".get_account_snaps"))
303@publisher_snaps.route("/snaps")
304@login_required
305def get_account_snaps():
306 account_info = dashboard.get_account(flask.session)
308 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info)
310 flask_user = flask.session["publisher"]
312 context = {
313 "snaps": user_snaps,
314 "current_user": flask_user["nickname"],
315 "registered_snaps": registered_snaps,
316 }
318 return flask.render_template("store/publisher.html", **context)
321@publisher_snaps.route("/snaps.json")
322@login_required
323def get_user_snaps():
324 account_info = dashboard.get_account(flask.session)
326 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info)
328 flask_user = flask.session["publisher"]
330 return flask.jsonify(
331 {
332 "snaps": user_snaps,
333 "current_user": flask_user["nickname"],
334 "registered_snaps": registered_snaps,
335 }
336 )
339@publisher_snaps.route("/snap-builds.json")
340@login_required
341def get_snap_build_status():
342 try:
343 account_info = dashboard.get_account(flask.session)
344 except (StoreApiError, ApiError) as api_error:
345 logging.getLogger("talisker.wsgi").error(
346 "Error with session: %s", api_error
347 )
349 return flask.jsonify({"error": "An unexpected error occurred"}), 400
351 response = []
352 user_snaps, _ = logic.get_snaps_account_info(account_info)
354 for snap_name in user_snaps:
355 snap_build_statuses = launchpad.get_snap_build_status(snap_name)
356 status = map_snap_build_status(snap_build_statuses)
358 response.append({"name": snap_name, "status": status})
360 return flask.jsonify(response)
363@publisher_snaps.route("/account/register-snap")
364def redirect_get_register_name():
365 return flask.redirect(flask.url_for(".get_register_name"))
368@publisher_snaps.route("/register-snap")
369@login_required
370def get_register_name():
371 return flask.render_template("store/publisher.html")
374@publisher_snaps.route("/account/register-snap", methods=["POST"])
375def redirect_post_register_name():
376 return flask.redirect(flask.url_for(".post_register_name"), 307)
379@publisher_snaps.route("/api/register-snap", methods=["POST"])
380@login_required
381def post_register_name():
382 snap_name = flask.request.form.get("snap_name")
383 res = {}
385 if not snap_name:
386 res["success"] = False
387 res["message"] = "You must define a snap name"
389 return jsonify(res)
391 is_private = flask.request.form.get("is_private") == "private"
392 store = flask.request.form.get("store")
394 try:
395 dashboard.post_register_name(
396 session=flask.session,
397 snap_name=snap_name,
398 registrant_comment=None,
399 is_private=is_private,
400 store=store,
401 )
402 except StoreApiResponseErrorList as api_response_error_list:
403 res = {
404 "success": False,
405 "data": {
406 "is_private": is_private,
407 "snap_name": snap_name,
408 "store": store,
409 },
410 }
412 if api_response_error_list.status_code == 409:
413 for error in api_response_error_list.errors:
414 res["data"]["error_code"] = error["code"]
416 return jsonify(res)
418 if api_response_error_list.status_code == 400:
419 res["data"]["error_code"] = "no-permission"
420 res[
421 "message"
422 ] = """You don't have permission
423 to register a snap in this store.
424 Please see store administrator."""
426 return jsonify(res)
428 res["message"] = "Unable to register snap name"
429 res["data"] = {
430 "snap_name": snap_name,
431 "is_private": is_private,
432 "store": store,
433 }
435 return jsonify(res)
437 return jsonify({"success": True})
440@publisher_snaps.route("/api/packages/<snap_name>", methods=["GET"])
441@login_required
442@exchange_required
443def get_package_metadata(snap_name):
444 try:
445 package_metadata = publisher_gateway.get_package_metadata(
446 flask.session, snap_name
447 )
448 return jsonify({"data": package_metadata, "success": True})
449 except StoreApiResourceNotFound:
450 return (jsonify({"error": "Package not found", "success": False}), 404)
451 except StoreApiResponseErrorList as error:
452 return (
453 jsonify(
454 {
455 "error": "Error occurred while fetching snap metadata.",
456 "errors": error.errors,
457 "success": False,
458 }
459 ),
460 error.status_code,
461 )
462 except StoreApiResponseError as error:
463 return (
464 jsonify(
465 {
466 "error": "Error occurred while fetching snap metadata.",
467 "success": False,
468 }
469 ),
470 error.status_code,
471 )
472 except StoreApiError:
473 return (
474 jsonify(
475 {
476 "error": "Error occurred while fetching snap metadata.",
477 "success": False,
478 }
479 ),
480 500,
481 )
482 except Exception:
483 return (jsonify({"error": "Unexpected error", "success": False}), 500)
486@publisher_snaps.route("/packages/<package_name>", methods=["DELETE"])
487@login_required
488@exchange_required
489def delete_package(package_name):
490 response = publisher_gateway.unregister_package_name(
491 flask.session, package_name
492 )
494 if response.status_code == 200:
495 return ("", 200)
496 return (
497 jsonify({"error": response.json()["error-list"][0]["message"]}),
498 response.status_code,
499 )
502@publisher_snaps.route("/snap_info/user_snap/<snap_name>", methods=["GET"])
503@login_required
504def get_is_user_snap(snap_name):
505 is_users_snap = False
506 try:
507 snap_info = dashboard.get_snap_info(flask.session, snap_name)
508 except (StoreApiError, ApiError) as api_error:
509 logging.getLogger("talisker.wsgi").error(
510 "Error with session: %s", api_error
511 )
513 return flask.jsonify({"error": "An unexpected error occurred"}), 400
515 if authentication.is_authenticated(flask.session):
516 publisher_info = flask.session.get("publisher", {})
517 if (
518 publisher_info.get("nickname")
519 == snap_info["publisher"]["username"]
520 ):
521 is_users_snap = True
523 return {"is_users_snap": is_users_snap}
526@publisher_snaps.route("/register-snap/json", methods=["POST"])
527@login_required
528def post_register_name_json():
529 snap_name = flask.request.form.get("snap-name")
531 if not snap_name:
532 return (
533 flask.jsonify({"errors": [{"message": "Snap name is required"}]}),
534 400,
535 )
537 try:
538 response = dashboard.post_register_name(
539 session=flask.session, snap_name=snap_name
540 )
541 except StoreApiResponseErrorList as api_response_error_list:
542 for error in api_response_error_list.errors:
543 # if snap name is already owned treat it as success
544 if error["code"] == "already_owned":
545 return flask.jsonify(
546 {"code": error["code"], "snap_name": snap_name}
547 )
548 return (
549 flask.jsonify({"errors": api_response_error_list.errors}),
550 api_response_error_list.status_code,
551 )
553 response["code"] = "created"
555 return flask.jsonify(response)
558@publisher_snaps.route("/register-name-dispute")
559@login_required
560def get_register_name_dispute():
561 snap_name = flask.request.args.get("snap-name")
563 if not snap_name:
564 return flask.redirect(
565 flask.url_for(".get_register_name", snap_name=snap_name)
566 )
567 return flask.render_template(
568 "store/publisher.html",
569 )
572@publisher_snaps.route("/api/register-name-dispute", methods=["POST"])
573@login_required
574def post_register_name_dispute():
575 try:
576 claim = flask.json.loads(flask.request.data)
577 snap_name = claim["snap-name"]
578 claim_comment = claim["claim-comment"]
579 dashboard.post_register_name_dispute(
580 flask.session,
581 bleach.clean(snap_name),
582 bleach.clean(claim_comment),
583 )
584 except StoreApiResponseErrorList as api_response_error_list:
585 if api_response_error_list.status_code in [400, 409]:
586 return jsonify(
587 {
588 "success": False,
589 "data": api_response_error_list.errors,
590 "message": api_response_error_list.errors[0]["message"],
591 }
592 )
593 return jsonify({"success": True})
596@publisher_snaps.route("/request-reserved-name")
597@login_required
598def get_request_reserved_name():
599 stores = dashboard.get_stores(flask.session)
601 snap_name = flask.request.args.get("snap_name")
602 store_id = flask.request.args.get("store")
603 store_name = logic.get_store_name(store_id, stores)
605 if not snap_name:
606 return flask.redirect(
607 flask.url_for(
608 ".get_register_name", snap_name=snap_name, store=store_id
609 )
610 )
611 return flask.render_template(
612 "store/publisher.html",
613 snap_name=snap_name,
614 store=store_name,
615 )
618@publisher_snaps.route("/snaps/api/snap-count")
619@login_required
620def snap_count():
621 account_info = dashboard.get_account(flask.session)
623 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info)
625 context = {"count": len(user_snaps), "snaps": list(user_snaps.keys())}
627 return flask.jsonify(context)