Coverage for webapp/publisher/snaps/views.py: 85%
185 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-15 22:43 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-15 22:43 +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
11# Local
12from webapp import authentication
13from webapp.helpers import api_publisher_session, launchpad
14from webapp.api.exceptions import ApiError
15from webapp.decorators import (
16 exchange_required,
17 gate_unreleased_snap_pages,
18 login_required,
19)
20from webapp.endpoints.publisher import listing as listing_endpoint
21from webapp.endpoints import cves
22from webapp.publisher.snaps import (
23 build_views,
24 listing_views,
25 logic,
26 metrics_views,
27 publicise_views,
28 release_views,
29 settings_views,
30 collaboration_views,
31)
32from webapp.endpoints.publisher.builds import (
33 get_snap_build_page,
34 get_validate_repo,
35 post_build,
36 post_disconnect_repo,
37)
38from webapp.endpoints.publisher.settings import (
39 get_settings_data,
40 post_settings_data,
41)
42from webapp.endpoints.publisher.publicise import get_publicise_data
43from webapp.endpoints.publisher.packages import get_package_metadata
44from webapp.endpoints.publisher.register import (
45 post_register_name,
46 post_register_name_dispute,
47)
48from webapp.endpoints import releases, builds
49from webapp.publisher.snaps.builds import map_snap_build_status
51dashboard = Dashboard(api_publisher_session)
52publisher_gateway = PublisherGW("snap", api_publisher_session)
55publisher_snaps = flask.Blueprint(
56 "publisher_snaps",
57 __name__,
58 template_folder="/templates",
59 static_folder="/static",
60)
61publisher_snaps.before_request(gate_unreleased_snap_pages)
63# Listing views
64publisher_snaps.add_url_rule(
65 "/account/snaps/<snap_name>/market",
66 view_func=listing_views.get_market_snap,
67)
68publisher_snaps.add_url_rule(
69 "/account/snaps/<snap_name>/listing",
70 view_func=listing_views.get_market_snap,
71 methods=["GET"],
72)
73publisher_snaps.add_url_rule(
74 "/account/snaps/<snap_name>/listing",
75 view_func=listing_views.redirect_post_market_snap,
76 methods=["POST"],
77)
78publisher_snaps.add_url_rule(
79 "/<snap_name>/listing",
80 view_func=listing_views.get_listing_snap,
81 methods=["GET"],
82)
83publisher_snaps.add_url_rule(
84 "/api/<snap_name>/listing",
85 view_func=listing_endpoint.get_listing_data,
86 methods=["GET"],
87)
88publisher_snaps.add_url_rule(
89 "/api/<snap_name>/listing",
90 view_func=listing_endpoint.post_listing_data,
91 methods=["POST"],
92)
93publisher_snaps.add_url_rule(
94 "/<snap_name>/preview",
95 view_func=listing_views.post_preview,
96 methods=["POST"],
97)
98publisher_snaps.add_url_rule(
99 "/<snap_name>/collaboration",
100 view_func=collaboration_views.get_collaboration_snap,
101 methods=["GET"],
102)
104# Build views
105publisher_snaps.add_url_rule(
106 "/<snap_name>/builds",
107 view_func=build_views.get_snap_builds_page,
108 methods=["GET"],
109),
111publisher_snaps.add_url_rule(
112 "/<snap_name>/builds/<build_id>",
113 view_func=get_snap_build_page,
114 methods=["GET"],
115),
117publisher_snaps.add_url_rule(
118 "/api/<snap_name>/repo",
119 view_func=builds.get_snap_repo,
120 methods=["GET"],
121)
122publisher_snaps.add_url_rule(
123 "/api/<snap_name>/builds",
124 view_func=build_views.get_snap_builds,
125 methods=["GET"],
126)
127publisher_snaps.add_url_rule(
128 "/api/<snap_name>/builds",
129 view_func=build_views.post_snap_builds,
130 methods=["POST"],
131)
132publisher_snaps.add_url_rule(
133 "/api/<snap_name>/builds/<build_id>",
134 view_func=build_views.get_snap_build,
135 methods=["GET"],
136)
137publisher_snaps.add_url_rule(
138 "/api/<snap_name>/builds/validate-repo",
139 view_func=get_validate_repo,
140 methods=["GET"],
141)
142publisher_snaps.add_url_rule(
143 "/api/<snap_name>/builds/trigger-build",
144 view_func=post_build,
145 methods=["POST"],
146)
147publisher_snaps.add_url_rule(
148 "/api/<snap_name>/builds/check-build-request/<build_id>",
149 view_func=build_views.check_build_request,
150 methods=["GET"],
151)
152publisher_snaps.add_url_rule(
153 "/api/<snap_name>/webhook/notify",
154 view_func=build_views.post_github_webhook,
155 methods=["POST"],
156)
157# This route is to support previous webhooks from build.snapcraft.io
158publisher_snaps.add_url_rule(
159 "/api/<github_owner>/<github_repo>/webhook/notify",
160 view_func=build_views.post_github_webhook,
161 methods=["POST"],
162)
163publisher_snaps.add_url_rule(
164 "/api/<snap_name>/builds/update-webhook",
165 view_func=build_views.get_update_gh_webhooks,
166 methods=["GET"],
167)
168publisher_snaps.add_url_rule(
169 "/api/<snap_name>/builds/disconnect/",
170 view_func=post_disconnect_repo,
171 methods=["POST"],
172)
174# Release views
175publisher_snaps.add_url_rule(
176 "/account/snaps/<snap_name>/release",
177 view_func=release_views.redirect_get_release_history,
178)
179publisher_snaps.add_url_rule(
180 "/<snap_name>/release",
181 view_func=release_views.redirect_get_release_history,
182)
183publisher_snaps.add_url_rule(
184 "/<snap_name>/releases",
185 view_func=release_views.get_releases,
186 methods=["GET"],
187)
188publisher_snaps.add_url_rule(
189 "/api/<snap_name>/releases",
190 view_func=releases.get_release_history_data,
191 methods=["GET"],
192)
193publisher_snaps.add_url_rule(
194 "/account/snaps/<snap_name>/release",
195 view_func=release_views.redirect_post_release,
196 methods=["POST"],
197)
198publisher_snaps.add_url_rule(
199 "/<snap_name>/release",
200 view_func=release_views.redirect_post_release,
201 methods=["POST"],
202)
203publisher_snaps.add_url_rule(
204 "/<snap_name>/releases/json",
205 view_func=release_views.get_release_history_json,
206)
207publisher_snaps.add_url_rule(
208 "/<snap_name>/releases",
209 view_func=release_views.post_release,
210 methods=["POST"],
211)
212publisher_snaps.add_url_rule(
213 "/<snap_name>/release/close-channel",
214 view_func=release_views.redirect_post_close_channel,
215 methods=["POST"],
216)
217publisher_snaps.add_url_rule(
218 "/<snap_name>/releases/close-channel",
219 view_func=release_views.post_close_channel,
220 methods=["POST"],
221)
222publisher_snaps.add_url_rule(
223 "/<snap_name>/releases/default-track",
224 view_func=release_views.post_default_track,
225 methods=["POST"],
226)
227publisher_snaps.add_url_rule(
228 "/<snap_name>/releases/revision/<revision>",
229 view_func=release_views.get_snap_revision_json,
230)
231publisher_snaps.add_url_rule(
232 "/api/<snap_name>/release-status",
233 view_func=release_views.get_release_status,
234 methods=["GET"],
235)
237# Metrics views
238publisher_snaps.add_url_rule(
239 "/snaps/metrics/json",
240 view_func=metrics_views.get_account_snaps_metrics,
241 methods=["POST"],
242)
243publisher_snaps.add_url_rule(
244 "/account/snaps/<snap_name>/measure",
245 view_func=metrics_views.get_measure_snap,
246)
247publisher_snaps.add_url_rule(
248 "/account/snaps/<snap_name>/metrics",
249 view_func=metrics_views.get_measure_snap,
250)
251publisher_snaps.add_url_rule(
252 "/<snap_name>/metrics",
253 view_func=metrics_views.publisher_snap_metrics,
254)
256publisher_snaps.add_url_rule(
257 "/<snap_name>/metrics/active-devices",
258 view_func=metrics_views.get_active_devices,
259)
261publisher_snaps.add_url_rule(
262 "/<snap_name>/metrics/active-latest-devices",
263 view_func=metrics_views.get_latest_active_devices,
264)
266publisher_snaps.add_url_rule(
267 "/<snap_name>/metrics/active-device-annotation",
268 view_func=metrics_views.get_metric_annotaion,
269)
271publisher_snaps.add_url_rule(
272 "/<snap_name>/metrics/country-metric",
273 view_func=metrics_views.get_country_metric,
274)
276# Publice views
277publisher_snaps.add_url_rule(
278 "/<snap_name>/publicise",
279 view_func=publicise_views.get_publicise,
280)
281publisher_snaps.add_url_rule(
282 "/<snap_name>/publicise/badges",
283 view_func=publicise_views.get_publicise,
284)
285publisher_snaps.add_url_rule(
286 "/<snap_name>/publicise/cards",
287 view_func=publicise_views.get_publicise,
288)
289publisher_snaps.add_url_rule(
290 "/api/<snap_name>/publicise",
291 view_func=get_publicise_data,
292)
294# Settings views
295publisher_snaps.add_url_rule(
296 "/<snap_name>/settings",
297 view_func=settings_views.get_settings,
298)
299publisher_snaps.add_url_rule(
300 "/api/<snap_name>/settings",
301 view_func=post_settings_data,
302 methods=["POST"],
303)
304publisher_snaps.add_url_rule(
305 "/api/<snap_name>/settings",
306 view_func=get_settings_data,
307)
309# CVE API
310publisher_snaps.add_url_rule(
311 "/api/<snap_name>/<revision>/cves",
312 view_func=cves.get_cves,
313)
315publisher_snaps.add_url_rule(
316 "/api/<snap_name>/cves",
317 view_func=cves.get_revisions_with_cves,
318)
321@publisher_snaps.route("/account/snaps")
322@login_required
323def redirect_get_account_snaps():
324 return flask.redirect(flask.url_for(".get_account_snaps"))
327@publisher_snaps.route("/snaps")
328@login_required
329def get_account_snaps():
330 account_info = dashboard.get_account(flask.session)
332 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info)
334 flask_user = flask.session["publisher"]
336 context = {
337 "snaps": user_snaps,
338 "current_user": flask_user["nickname"],
339 "registered_snaps": registered_snaps,
340 }
342 return flask.render_template("store/publisher.html", **context)
345@publisher_snaps.route("/account-keys.json")
346@login_required
347def get_user_keys():
348 account_keys = dashboard.get_account_keys(flask.session)
350 return flask.jsonify(account_keys)
353@publisher_snaps.route("/snaps.json")
354@login_required
355def get_user_snaps():
356 account_info = dashboard.get_account(flask.session)
358 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info)
360 flask_user = flask.session["publisher"]
362 return flask.jsonify(
363 {
364 "snaps": user_snaps,
365 "current_user": flask_user["nickname"],
366 "registered_snaps": registered_snaps,
367 }
368 )
371@publisher_snaps.route("/snap-builds.json")
372@login_required
373def get_snap_build_status():
374 try:
375 account_info = dashboard.get_account(flask.session)
376 except (StoreApiError, ApiError):
377 return flask.jsonify({"error": "An unexpected error occurred"}), 400
379 response = []
380 user_snaps, _ = logic.get_snaps_account_info(account_info)
382 for snap_name in user_snaps:
383 snap_build_statuses = launchpad.get_snap_build_status(snap_name)
384 status = map_snap_build_status(snap_build_statuses)
386 response.append({"name": snap_name, "status": status})
388 return flask.jsonify(response)
391@publisher_snaps.route("/account/register-snap")
392def redirect_get_register_name():
393 return flask.redirect(flask.url_for(".get_register_name"))
396@publisher_snaps.route("/register-snap")
397@login_required
398def get_register_name():
399 return flask.render_template("store/publisher.html")
402@publisher_snaps.route("/account/register-snap", methods=["POST"])
403def redirect_post_register_name():
404 return flask.redirect(flask.url_for(".post_register_name"), 307)
407publisher_snaps.add_url_rule(
408 "/api/packages/<snap_name>",
409 view_func=get_package_metadata,
410 methods=["GET"],
411)
413publisher_snaps.add_url_rule(
414 "/api/register-snap",
415 view_func=post_register_name,
416 methods=["POST"],
417)
419publisher_snaps.add_url_rule(
420 "/api/register-name-dispute",
421 view_func=post_register_name_dispute,
422 methods=["POST"],
423)
426@publisher_snaps.route("/packages/<package_name>", methods=["DELETE"])
427@login_required
428@exchange_required
429def delete_package(package_name):
430 response = publisher_gateway.unregister_package_name(
431 flask.session, package_name
432 )
434 if response.status_code == 200:
435 return ("", 200)
436 return (
437 jsonify({"error": response.json()["error-list"][0]["message"]}),
438 response.status_code,
439 )
442@publisher_snaps.route("/snap_info/user_snap/<snap_name>", methods=["GET"])
443@login_required
444def get_is_user_snap(snap_name):
445 is_users_snap = False
446 try:
447 snap_info = dashboard.get_snap_info(flask.session, snap_name)
448 except (StoreApiError, ApiError):
449 return flask.jsonify({"error": "An unexpected error occurred"}), 400
451 if authentication.is_authenticated(flask.session):
452 publisher_info = flask.session.get("publisher", {})
453 if (
454 publisher_info.get("nickname")
455 == snap_info["publisher"]["username"]
456 ):
457 is_users_snap = True
459 return {"is_users_snap": is_users_snap}
462@publisher_snaps.route("/register-snap/json", methods=["POST"])
463@login_required
464def post_register_name_json():
465 snap_name = flask.request.form.get("snap-name")
467 if not snap_name:
468 return (
469 flask.jsonify({"errors": [{"message": "Snap name is required"}]}),
470 400,
471 )
473 try:
474 response = dashboard.post_register_name(
475 session=flask.session, snap_name=snap_name
476 )
477 except StoreApiResponseErrorList as api_response_error_list:
478 for error in api_response_error_list.errors:
479 # if snap name is already owned treat it as success
480 if error["code"] == "already_owned":
481 return flask.jsonify(
482 {"code": error["code"], "snap_name": snap_name}
483 )
484 return (
485 flask.jsonify({"errors": api_response_error_list.errors}),
486 api_response_error_list.status_code,
487 )
489 response["code"] = "created"
491 return flask.jsonify(response)
494@publisher_snaps.route("/register-name-dispute")
495@login_required
496def get_register_name_dispute():
497 snap_name = flask.request.args.get("snap-name")
499 if not snap_name:
500 return flask.redirect(
501 flask.url_for(".get_register_name", snap_name=snap_name)
502 )
503 return flask.render_template(
504 "store/publisher.html",
505 )
508@publisher_snaps.route("/request-reserved-name")
509@login_required
510def get_request_reserved_name():
511 stores = dashboard.get_stores(flask.session)
513 snap_name = flask.request.args.get("snap_name")
514 store_id = flask.request.args.get("store")
515 store_name = logic.get_store_name(store_id, stores)
517 if not snap_name:
518 return flask.redirect(
519 flask.url_for(
520 ".get_register_name", snap_name=snap_name, store=store_id
521 )
522 )
523 return flask.render_template(
524 "store/publisher.html",
525 snap_name=snap_name,
526 store=store_name,
527 )
530@publisher_snaps.route("/snaps/api/snap-count")
531@login_required
532def snap_count():
533 account_info = dashboard.get_account(flask.session)
535 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info)
537 context = {"count": len(user_snaps), "snaps": list(user_snaps.keys())}
539 return flask.jsonify(context)