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