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