Coverage for webapp/store/views.py: 49%
193 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
1from math import floor
2import flask
3from dateutil import parser
4from webapp.decorators import exchange_required, login_required
5import webapp.helpers as helpers
6import webapp.store.logic as logic
7from webapp.api import requests
9from canonicalwebteam.exceptions import StoreApiError
10from canonicalwebteam.store_api.dashboard import Dashboard
11from canonicalwebteam.store_api.publishergw import PublisherGW
12from canonicalwebteam.store_api.devicegw import DeviceGW
14from webapp.api.exceptions import ApiError
15from webapp.store.snap_details_views import snap_details_views
16from webapp.helpers import api_publisher_session, api_session
17from flask.json import jsonify
18import os
19from webapp.extensions import csrf
21session = requests.Session()
23YOUTUBE_API_KEY = os.getenv("YOUTUBE_API_KEY")
24dashboard = Dashboard(api_session)
25publisher_gateway = PublisherGW("snap", api_publisher_session)
26device_gateway = DeviceGW("snap", api_session)
29def store_blueprint(store_query=None):
30 store = flask.Blueprint(
31 "store",
32 __name__,
33 template_folder="/templates",
34 static_folder="/static",
35 )
36 snap_details_views(store)
38 @store.route("/validation-sets", defaults={"path": ""})
39 @store.route("/validation-sets/<path:path>")
40 @login_required
41 def validation_sets(path):
42 return flask.render_template("store/publisher.html")
44 @store.route("/discover")
45 def discover():
46 return flask.redirect(flask.url_for(".homepage"))
48 def brand_store_view():
49 error_info = {}
50 status_code = 200
52 try:
53 snaps = device_gateway.get_all_items(size=16)["results"]
54 except (StoreApiError, ApiError):
55 snaps = []
57 for snap in snaps:
58 if "media" in snap:
59 snap["icon_url"] = helpers.get_icon(snap["media"])
61 return (
62 flask.render_template(
63 "brand-store/store.html", snaps=snaps, error_info=error_info
64 ),
65 status_code,
66 )
68 def brand_search_snap():
69 status_code = 200
70 snap_searched = flask.request.args.get("q", default="", type=str)
72 if not snap_searched:
73 return flask.redirect(flask.url_for(".homepage"))
75 size = flask.request.args.get("limit", default=25, type=int)
76 offset = flask.request.args.get("offset", default=0, type=int)
78 try:
79 page = floor(offset / size) + 1
80 except ZeroDivisionError:
81 size = 10
82 page = floor(offset / size) + 1
84 error_info = {}
85 searched_results = []
87 searched_results = device_gateway.search(
88 snap_searched, size=size, page=page
89 )
91 snaps_results = searched_results["results"]
93 for snap in snaps_results:
94 snap["icon_url"] = helpers.get_icon(snap["media"])
96 links = logic.get_pages_details(
97 flask.request.base_url,
98 (
99 searched_results["_links"]
100 if "_links" in searched_results
101 else []
102 ),
103 )
105 context = {
106 "query": snap_searched,
107 "snaps": snaps_results,
108 "links": links,
109 "error_info": error_info,
110 }
112 return (
113 flask.render_template("brand-store/search.html", **context),
114 status_code,
115 )
117 @store.route("/store")
118 def store_view():
119 return flask.render_template("store/store.html")
121 @store.route("/explore")
122 def explore_view():
123 return flask.render_template("store/store.html")
125 @store.route("/youtube", methods=["POST"])
126 def get_video_thumbnail_data():
127 body = flask.request.form
128 thumbnail_url = "https://www.googleapis.com/youtube/v3/videos"
129 thumbnail_data = session.get(
130 (
131 f"{thumbnail_url}?id={body['videoId']}"
132 f"&part=snippet&key={YOUTUBE_API_KEY}"
133 )
134 )
136 if thumbnail_data:
137 return thumbnail_data.json()
139 return {}
141 @store.route("/publisher/<regex('[a-z0-9-]*[a-z][a-z0-9-]*'):publisher>")
142 def publisher_details(publisher):
143 """
144 A view to display the publisher details page for specific publisher.
145 """
147 # 404 for the snap-quarantine publisher
148 if publisher == "snap-quarantine":
149 flask.abort(404)
151 publisher_content_path = flask.current_app.config["CONTENT_DIRECTORY"][
152 "PUBLISHER_PAGES"
153 ]
155 if publisher in ["kde", "snapcrafters", "jetbrains"]:
156 context = helpers.get_yaml(
157 publisher_content_path + publisher + ".yaml", typ="safe"
158 )
160 if not context:
161 flask.abort(404)
163 popular_snaps = helpers.get_yaml(
164 publisher_content_path + publisher + "-snaps.yaml",
165 typ="safe",
166 )
168 context["popular_snaps"] = (
169 popular_snaps["snaps"] if popular_snaps else []
170 )
172 if "publishers" in context:
173 context["snaps"] = []
174 for publisher in context["publishers"]:
175 snaps_results = []
176 try:
177 snaps_results = device_gateway.get_publisher_items(
178 publisher, size=500, page=1
179 )["_embedded"]["clickindex:package"]
180 except StoreApiError:
181 pass
183 for snap in snaps_results:
184 snap["icon_url"] = helpers.get_icon(snap["media"])
186 context["snaps"].extend(
187 [snap for snap in snaps_results if snap["apps"]]
188 )
190 featured_snaps = [
191 snap["package_name"] for snap in context["featured_snaps"]
192 ]
194 context["snaps"] = [
195 snap
196 for snap in context["snaps"]
197 if snap["package_name"] not in featured_snaps
198 ]
200 context["snaps_count"] = len(context["snaps"]) + len(
201 featured_snaps
202 )
204 return flask.render_template(
205 "store/publisher-details.html", **context
206 )
208 status_code = 200
209 error_info = {}
210 snaps_results = []
211 snaps = []
212 snaps_count = 0
213 publisher_details = {"display-name": publisher, "username": publisher}
215 snaps_results = device_gateway.find(
216 publisher=publisher,
217 fields=[
218 "title",
219 "summary",
220 "media",
221 "publisher",
222 ],
223 )["results"]
225 for snap in snaps_results:
226 item = snap["snap"]
227 item["package_name"] = snap["name"]
228 item["icon_url"] = helpers.get_icon(item["media"])
229 snaps.append(item)
231 snaps_count = len(snaps)
233 if snaps_count > 0:
234 publisher_details = snaps[0]["publisher"]
236 context = {
237 "snaps": snaps,
238 "snaps_count": snaps_count,
239 "publisher": publisher_details,
240 "error_info": error_info,
241 }
243 return (
244 flask.render_template(
245 "store/community-publisher-details.html", **context
246 ),
247 status_code,
248 )
250 @store.route("/store/categories/<category>")
251 def store_category(category):
252 status_code = 200
253 error_info = {}
254 snaps_results = []
256 snaps_results = device_gateway.get_category_items(
257 category=category, size=10, page=1
258 )["results"]
259 for snap in snaps_results:
260 snap["icon_url"] = helpers.get_icon(snap["media"])
262 # if the first snap (banner snap) doesn't have an icon, remove the last
263 # snap from the list to avoid a hanging snap (grid of 9)
264 if len(snaps_results) == 10 and snaps_results[0]["icon_url"] == "":
265 snaps_results = snaps_results[:-1]
267 for index in range(len(snaps_results)):
268 snaps_results[index] = logic.get_snap_banner_url(
269 snaps_results[index]
270 )
272 context = {
273 "category": category,
274 "has_featured": True,
275 "snaps": snaps_results,
276 "error_info": error_info,
277 }
279 return (
280 flask.render_template("store/_category-partial.html", **context),
281 status_code,
282 )
284 @store.route("/store/featured-snaps/<category>")
285 def featured_snaps_in_category(category):
286 snaps_results = []
288 snaps_results = device_gateway.get_category_items(
289 category=category, size=3, page=1
290 )["_embedded"]["clickindex:package"]
292 for snap in snaps_results:
293 snap["icon_url"] = helpers.get_icon(snap["media"])
295 return flask.jsonify(snaps_results)
297 @store.route("/store/sitemap.xml")
298 def sitemap():
299 base_url = "https://snapcraft.io/store"
301 snaps = []
302 page = 0
303 url = f"https://api.snapcraft.io/api/v1/snaps/search?page={page}"
304 while url:
305 response = session.get(url)
306 try:
307 snaps_response = response.json()
308 except Exception:
309 continue
311 for snap in snaps_response["_embedded"]["clickindex:package"]:
312 try:
313 last_udpated = (
314 parser.parse(snap["last_updated"])
315 .replace(tzinfo=None)
316 .strftime("%Y-%m-%d")
317 )
318 snaps.append(
319 {
320 "url": "https://snapcraft.io/"
321 + snap["package_name"],
322 "last_udpated": last_udpated,
323 }
324 )
325 except Exception:
326 continue
327 if "next" in snaps_response["_links"]:
328 url = snaps_response["_links"]["next"]["href"]
329 else:
330 url = None
332 xml_sitemap = flask.render_template(
333 "sitemap/sitemap.xml",
334 base_url=base_url,
335 links=snaps,
336 )
338 response = flask.make_response(xml_sitemap)
339 response.headers["Content-Type"] = "application/xml"
340 response.headers["Cache-Control"] = "public, max-age=43200"
342 return response
344 if store_query:
345 store.add_url_rule("/", "homepage", brand_store_view)
346 store.add_url_rule("/search", "search", brand_search_snap)
347 else:
348 store.add_url_rule("/store", "homepage", store_view)
350 store.add_url_rule("/explore", "explore", explore_view)
352 @store.route("/<snap_name>/create-track", methods=["POST"])
353 @login_required
354 @csrf.exempt
355 @exchange_required
356 def post_create_track(snap_name):
357 track_name = flask.request.form["track-name"]
358 version_pattern = flask.request.form.get("version-pattern")
359 auto_phasing_percentage = flask.request.form.get(
360 "automatic-phasing-percentage"
361 )
363 if auto_phasing_percentage is not None:
364 auto_phasing_percentage = float(auto_phasing_percentage)
366 response = publisher_gateway.create_track(
367 flask.session,
368 snap_name,
369 track_name,
370 version_pattern,
371 auto_phasing_percentage,
372 )
373 if response.status_code == 201:
374 return response.json(), response.status_code
375 if response.status_code == 409:
376 return (
377 jsonify({"error": "Track already exists."}),
378 response.status_code,
379 )
380 if "error-list" in response.json():
381 return (
382 jsonify(
383 {"error": response.json()["error-list"][0]["message"]}
384 ),
385 response.status_code,
386 )
387 return response.json(), response.status_code
389 return store