Coverage for webapp/publisher/snaps/views.py: 85%
186 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-20 22:09 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-20 22:09 +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/<build_id>/logs",
139 view_func=build_views.get_snap_build_logs,
140 methods=["GET"],
141)
142publisher_snaps.add_url_rule(
143 "/api/<snap_name>/builds/validate-repo",
144 view_func=get_validate_repo,
145 methods=["GET"],
146)
147publisher_snaps.add_url_rule(
148 "/api/<snap_name>/builds/trigger-build",
149 view_func=post_build,
150 methods=["POST"],
151)
152publisher_snaps.add_url_rule(
153 "/api/<snap_name>/builds/check-build-request/<build_id>",
154 view_func=build_views.check_build_request,
155 methods=["GET"],
156)
157publisher_snaps.add_url_rule(
158 "/api/<snap_name>/webhook/notify",
159 view_func=build_views.post_github_webhook,
160 methods=["POST"],
161)
162# This route is to support previous webhooks from build.snapcraft.io
163publisher_snaps.add_url_rule(
164 "/api/<github_owner>/<github_repo>/webhook/notify",
165 view_func=build_views.post_github_webhook,
166 methods=["POST"],
167)
168publisher_snaps.add_url_rule(
169 "/api/<snap_name>/builds/update-webhook",
170 view_func=build_views.get_update_gh_webhooks,
171 methods=["GET"],
172)
173publisher_snaps.add_url_rule(
174 "/api/<snap_name>/builds/disconnect/",
175 view_func=post_disconnect_repo,
176 methods=["POST"],
177)
179# Release views
180publisher_snaps.add_url_rule(
181 "/account/snaps/<snap_name>/release",
182 view_func=release_views.redirect_get_release_history,
183)
184publisher_snaps.add_url_rule(
185 "/<snap_name>/release",
186 view_func=release_views.redirect_get_release_history,
187)
188publisher_snaps.add_url_rule(
189 "/<snap_name>/releases",
190 view_func=release_views.get_releases,
191 methods=["GET"],
192)
193publisher_snaps.add_url_rule(
194 "/api/<snap_name>/releases",
195 view_func=releases.get_release_history_data,
196 methods=["GET"],
197)
198publisher_snaps.add_url_rule(
199 "/account/snaps/<snap_name>/release",
200 view_func=release_views.redirect_post_release,
201 methods=["POST"],
202)
203publisher_snaps.add_url_rule(
204 "/<snap_name>/release",
205 view_func=release_views.redirect_post_release,
206 methods=["POST"],
207)
208publisher_snaps.add_url_rule(
209 "/<snap_name>/releases/json",
210 view_func=release_views.get_release_history_json,
211)
212publisher_snaps.add_url_rule(
213 "/<snap_name>/releases",
214 view_func=release_views.post_release,
215 methods=["POST"],
216)
217publisher_snaps.add_url_rule(
218 "/<snap_name>/release/close-channel",
219 view_func=release_views.redirect_post_close_channel,
220 methods=["POST"],
221)
222publisher_snaps.add_url_rule(
223 "/<snap_name>/releases/close-channel",
224 view_func=release_views.post_close_channel,
225 methods=["POST"],
226)
227publisher_snaps.add_url_rule(
228 "/<snap_name>/releases/default-track",
229 view_func=release_views.post_default_track,
230 methods=["POST"],
231)
232publisher_snaps.add_url_rule(
233 "/<snap_name>/releases/revision/<revision>",
234 view_func=release_views.get_snap_revision_json,
235)
236publisher_snaps.add_url_rule(
237 "/api/<snap_name>/release-status",
238 view_func=release_views.get_release_status,
239 methods=["GET"],
240)
242# Metrics views
243publisher_snaps.add_url_rule(
244 "/snaps/metrics/json",
245 view_func=metrics_views.get_account_snaps_metrics,
246 methods=["POST"],
247)
248publisher_snaps.add_url_rule(
249 "/account/snaps/<snap_name>/measure",
250 view_func=metrics_views.get_measure_snap,
251)
252publisher_snaps.add_url_rule(
253 "/account/snaps/<snap_name>/metrics",
254 view_func=metrics_views.get_measure_snap,
255)
256publisher_snaps.add_url_rule(
257 "/<snap_name>/metrics",
258 view_func=metrics_views.publisher_snap_metrics,
259)
261publisher_snaps.add_url_rule(
262 "/<snap_name>/metrics/active-devices",
263 view_func=metrics_views.get_active_devices,
264)
266publisher_snaps.add_url_rule(
267 "/<snap_name>/metrics/active-latest-devices",
268 view_func=metrics_views.get_latest_active_devices,
269)
271publisher_snaps.add_url_rule(
272 "/<snap_name>/metrics/active-device-annotation",
273 view_func=metrics_views.get_metric_annotaion,
274)
276publisher_snaps.add_url_rule(
277 "/<snap_name>/metrics/country-metric",
278 view_func=metrics_views.get_country_metric,
279)
281# Publice views
282publisher_snaps.add_url_rule(
283 "/<snap_name>/publicise",
284 view_func=publicise_views.get_publicise,
285)
286publisher_snaps.add_url_rule(
287 "/<snap_name>/publicise/badges",
288 view_func=publicise_views.get_publicise,
289)
290publisher_snaps.add_url_rule(
291 "/<snap_name>/publicise/cards",
292 view_func=publicise_views.get_publicise,
293)
294publisher_snaps.add_url_rule(
295 "/api/<snap_name>/publicise",
296 view_func=get_publicise_data,
297)
299# Settings views
300publisher_snaps.add_url_rule(
301 "/<snap_name>/settings",
302 view_func=settings_views.get_settings,
303)
304publisher_snaps.add_url_rule(
305 "/api/<snap_name>/settings",
306 view_func=post_settings_data,
307 methods=["POST"],
308)
309publisher_snaps.add_url_rule(
310 "/api/<snap_name>/settings",
311 view_func=get_settings_data,
312)
314# CVE API
315publisher_snaps.add_url_rule(
316 "/api/<snap_name>/<revision>/cves",
317 view_func=cves.get_cves,
318)
320publisher_snaps.add_url_rule(
321 "/api/<snap_name>/cves",
322 view_func=cves.get_revisions_with_cves,
323)
326@publisher_snaps.route("/account/snaps")
327@login_required
328def redirect_get_account_snaps():
329 return flask.redirect(flask.url_for(".get_account_snaps"))
332@publisher_snaps.route("/snaps")
333@login_required
334def get_account_snaps():
335 account_info = dashboard.get_account(flask.session)
337 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info)
339 flask_user = flask.session["publisher"]
341 context = {
342 "snaps": user_snaps,
343 "current_user": flask_user["nickname"],
344 "registered_snaps": registered_snaps,
345 }
347 return flask.render_template("store/publisher.html", **context)
350@publisher_snaps.route("/account-keys.json")
351@login_required
352def get_user_keys():
353 account_keys = dashboard.get_account_keys(flask.session)
355 return flask.jsonify(account_keys)
358@publisher_snaps.route("/snaps.json")
359@login_required
360def get_user_snaps():
361 account_info = dashboard.get_account(flask.session)
363 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info)
365 flask_user = flask.session["publisher"]
367 return flask.jsonify(
368 {
369 "snaps": user_snaps,
370 "current_user": flask_user["nickname"],
371 "registered_snaps": registered_snaps,
372 }
373 )
376@publisher_snaps.route("/snap-builds.json")
377@login_required
378def get_snap_build_status():
379 try:
380 account_info = dashboard.get_account(flask.session)
381 except (StoreApiError, ApiError):
382 return flask.jsonify({"error": "An unexpected error occurred"}), 400
384 response = []
385 user_snaps, _ = logic.get_snaps_account_info(account_info)
387 for snap_name in user_snaps:
388 snap_build_statuses = launchpad.get_snap_build_status(snap_name)
389 status = map_snap_build_status(snap_build_statuses)
391 response.append({"name": snap_name, "status": status})
393 return flask.jsonify(response)
396@publisher_snaps.route("/account/register-snap")
397def redirect_get_register_name():
398 return flask.redirect(flask.url_for(".get_register_name"))
401@publisher_snaps.route("/register-snap")
402@login_required
403def get_register_name():
404 return flask.render_template("store/publisher.html")
407@publisher_snaps.route("/account/register-snap", methods=["POST"])
408def redirect_post_register_name():
409 return flask.redirect(flask.url_for(".post_register_name"), 307)
412publisher_snaps.add_url_rule(
413 "/api/packages/<snap_name>",
414 view_func=get_package_metadata,
415 methods=["GET"],
416)
418publisher_snaps.add_url_rule(
419 "/api/register-snap",
420 view_func=post_register_name,
421 methods=["POST"],
422)
424publisher_snaps.add_url_rule(
425 "/api/register-name-dispute",
426 view_func=post_register_name_dispute,
427 methods=["POST"],
428)
431@publisher_snaps.route("/packages/<package_name>", methods=["DELETE"])
432@login_required
433@exchange_required
434def delete_package(package_name):
435 response = publisher_gateway.unregister_package_name(
436 flask.session, package_name
437 )
439 if response.status_code == 200:
440 return ("", 200)
441 return (
442 jsonify({"error": response.json()["error-list"][0]["message"]}),
443 response.status_code,
444 )
447@publisher_snaps.route("/snap_info/user_snap/<snap_name>", methods=["GET"])
448@login_required
449def get_is_user_snap(snap_name):
450 is_users_snap = False
451 try:
452 snap_info = dashboard.get_snap_info(flask.session, snap_name)
453 except (StoreApiError, ApiError):
454 return flask.jsonify({"error": "An unexpected error occurred"}), 400
456 if authentication.is_authenticated(flask.session):
457 publisher_info = flask.session.get("publisher", {})
458 if (
459 publisher_info.get("nickname")
460 == snap_info["publisher"]["username"]
461 ):
462 is_users_snap = True
464 return {"is_users_snap": is_users_snap}
467@publisher_snaps.route("/register-snap/json", methods=["POST"])
468@login_required
469def post_register_name_json():
470 snap_name = flask.request.form.get("snap-name")
472 if not snap_name:
473 return (
474 flask.jsonify({"errors": [{"message": "Snap name is required"}]}),
475 400,
476 )
478 try:
479 response = dashboard.post_register_name(
480 session=flask.session, snap_name=snap_name
481 )
482 except StoreApiResponseErrorList as api_response_error_list:
483 for error in api_response_error_list.errors:
484 # if snap name is already owned treat it as success
485 if error["code"] == "already_owned":
486 return flask.jsonify(
487 {"code": error["code"], "snap_name": snap_name}
488 )
489 return (
490 flask.jsonify({"errors": api_response_error_list.errors}),
491 api_response_error_list.status_code,
492 )
494 response["code"] = "created"
496 return flask.jsonify(response)
499@publisher_snaps.route("/register-name-dispute")
500@login_required
501def get_register_name_dispute():
502 snap_name = flask.request.args.get("snap-name")
504 if not snap_name:
505 return flask.redirect(
506 flask.url_for(".get_register_name", snap_name=snap_name)
507 )
508 return flask.render_template(
509 "store/publisher.html",
510 )
513@publisher_snaps.route("/request-reserved-name")
514@login_required
515def get_request_reserved_name():
516 stores = dashboard.get_stores(flask.session)
518 snap_name = flask.request.args.get("snap_name")
519 store_id = flask.request.args.get("store")
520 store_name = logic.get_store_name(store_id, stores)
522 if not snap_name:
523 return flask.redirect(
524 flask.url_for(
525 ".get_register_name", snap_name=snap_name, store=store_id
526 )
527 )
528 return flask.render_template(
529 "store/publisher.html",
530 snap_name=snap_name,
531 store=store_name,
532 )
535@publisher_snaps.route("/snaps/api/snap-count")
536@login_required
537def snap_count():
538 account_info = dashboard.get_account(flask.session)
540 user_snaps, registered_snaps = logic.get_snaps_account_info(account_info)
542 context = {"count": len(user_snaps), "snaps": list(user_snaps.keys())}
544 return flask.jsonify(context)