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
« 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
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
30dashboard = Dashboard(api_publisher_session)
31publisher_gateway = PublisherGW("snap", api_publisher_session)
34publisher_snaps = flask.Blueprint(
35 "publisher_snaps",
36 __name__,
37 template_folder="/templates",
38 static_folder="/static",
39)
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)
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),
89publisher_snaps.add_url_rule(
90 "/<snap_name>/builds/<build_id>",
91 view_func=build_views.get_snap_build_page,
92 methods=["GET"],
93),
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)
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)
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)
229publisher_snaps.add_url_rule(
230 "/<snap_name>/metrics/active-devices",
231 view_func=metrics_views.get_active_devices,
232)
234publisher_snaps.add_url_rule(
235 "/<snap_name>/metrics/active-latest-devices",
236 view_func=metrics_views.get_latest_active_devices,
237)
239publisher_snaps.add_url_rule(
240 "/<snap_name>/metrics/active-device-annotation",
241 view_func=metrics_views.get_metric_annotaion,
242)
244publisher_snaps.add_url_rule(
245 "/<snap_name>/metrics/country-metric",
246 view_func=metrics_views.get_country_metric,
247)
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)
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)
282# CVE API
283publisher_snaps.add_url_rule(
284 "/api/<snap_name>/<revision>/cves",
285 view_func=cve_views.get_cves,
286)
288publisher_snaps.add_url_rule(
289 "/api/<snap_name>/cves",
290 view_func=cve_views.get_revisions_with_cves,
291)
294@publisher_snaps.route("/account/snaps")
295@login_required
296def redirect_get_account_snaps():
297 return flask.redirect(flask.url_for(".get_account_snaps"))
300@publisher_snaps.route("/snaps")
301@login_required
302def get_account_snaps():
303 account_info = dashboard.get_account(flask.session)
305 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info)
307 flask_user = flask.session["publisher"]
309 context = {
310 "snaps": user_snaps,
311 "current_user": flask_user["nickname"],
312 "registered_snaps": registered_snaps,
313 }
315 return flask.render_template("store/publisher.html", **context)
318@publisher_snaps.route("/snaps.json")
319@login_required
320def get_user_snaps():
321 account_info = dashboard.get_account(flask.session)
323 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info)
325 flask_user = flask.session["publisher"]
327 return flask.jsonify(
328 {
329 "snaps": user_snaps,
330 "current_user": flask_user["nickname"],
331 "registered_snaps": registered_snaps,
332 }
333 )
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
344 response = []
345 user_snaps, _ = logic.get_snaps_account_info(account_info)
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)
351 response.append({"name": snap_name, "status": status})
353 return flask.jsonify(response)
356@publisher_snaps.route("/account/register-snap")
357def redirect_get_register_name():
358 return flask.redirect(flask.url_for(".get_register_name"))
361@publisher_snaps.route("/register-snap")
362@login_required
363def get_register_name():
364 return flask.render_template("store/publisher.html")
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)
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 = {}
378 if not snap_name:
379 res["success"] = False
380 res["message"] = "You must define a snap name"
382 return jsonify(res)
384 is_private = flask.request.form.get("is_private") == "private"
385 store = flask.request.form.get("store")
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 }
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"]
409 return jsonify(res)
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."""
419 return jsonify(res)
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 }
428 return jsonify(res)
430 return jsonify({"success": True})
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 )
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 )
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 )
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 )
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
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
483 return {"is_users_snap": is_users_snap}
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")
491 if not snap_name:
492 return (
493 flask.jsonify({"errors": [{"message": "Snap name is required"}]}),
494 400,
495 )
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 )
513 response["code"] = "created"
515 return flask.jsonify(response)
518@publisher_snaps.route("/register-name-dispute")
519@login_required
520def get_register_name_dispute():
521 snap_name = flask.request.args.get("snap-name")
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 )
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})
556@publisher_snaps.route("/request-reserved-name")
557@login_required
558def get_request_reserved_name():
559 stores = dashboard.get_stores(flask.session)
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)
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 )
578@publisher_snaps.route("/snaps/api/snap-count")
579@login_required
580def snap_count():
581 account_info = dashboard.get_account(flask.session)
583 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info)
585 context = {"count": len(user_snaps), "snaps": list(user_snaps.keys())}
587 return flask.jsonify(context)