Coverage for webapp/store/views.py: 49%
190 statements
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-05 22:06 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-05 22:06 +0000
1from math import floor
2import talisker.requests
3import flask
4from dateutil import parser
5from webapp.decorators import exchange_required, login_required
6import webapp.helpers as helpers
7import webapp.store.logic as logic
8from webapp.api import requests
10from canonicalwebteam.exceptions import StoreApiError
11from canonicalwebteam.store_api.dashboard import Dashboard
12from canonicalwebteam.store_api.publishergw import PublisherGW
13from canonicalwebteam.store_api.devicegw import DeviceGW
15from webapp.api.exceptions import ApiError
16from webapp.store.snap_details_views import snap_details_views
17from webapp.helpers import api_publisher_session, api_session
18from flask.json import jsonify
19import os
20from webapp.extensions import csrf
22session = talisker.requests.get_session(requests.Session)
24YOUTUBE_API_KEY = os.getenv("YOUTUBE_API_KEY")
25dashboard = Dashboard(api_session)
26publisher_gateway = PublisherGW("snap", api_publisher_session)
27device_gateway = DeviceGW("snap", api_session)
30def store_blueprint(store_query=None):
31 store = flask.Blueprint(
32 "store",
33 __name__,
34 template_folder="/templates",
35 static_folder="/static",
36 )
37 snap_details_views(store)
39 @store.route("/validation-sets", defaults={"path": ""})
40 @store.route("/validation-sets/<path:path>")
41 @login_required
42 def validation_sets(path):
43 return flask.render_template("store/publisher.html")
45 @store.route("/discover")
46 def discover():
47 return flask.redirect(flask.url_for(".homepage"))
49 def brand_store_view():
50 error_info = {}
51 status_code = 200
53 try:
54 snaps = device_gateway.get_all_items(size=16)["results"]
55 except (StoreApiError, ApiError):
56 snaps = []
58 for snap in snaps:
59 if "media" in snap:
60 snap["icon_url"] = helpers.get_icon(snap["media"])
62 return (
63 flask.render_template(
64 "brand-store/store.html", snaps=snaps, error_info=error_info
65 ),
66 status_code,
67 )
69 def brand_search_snap():
70 status_code = 200
71 snap_searched = flask.request.args.get("q", default="", type=str)
73 if not snap_searched:
74 return flask.redirect(flask.url_for(".homepage"))
76 size = flask.request.args.get("limit", default=25, type=int)
77 offset = flask.request.args.get("offset", default=0, type=int)
79 try:
80 page = floor(offset / size) + 1
81 except ZeroDivisionError:
82 size = 10
83 page = floor(offset / size) + 1
85 error_info = {}
86 searched_results = []
88 searched_results = device_gateway.search(
89 snap_searched, size=size, page=page
90 )
92 snaps_results = searched_results["results"]
94 for snap in snaps_results:
95 snap["icon_url"] = helpers.get_icon(snap["media"])
97 links = logic.get_pages_details(
98 flask.request.base_url,
99 (
100 searched_results["_links"]
101 if "_links" in searched_results
102 else []
103 ),
104 )
106 context = {
107 "query": snap_searched,
108 "snaps": snaps_results,
109 "links": links,
110 "error_info": error_info,
111 }
113 return (
114 flask.render_template("brand-store/search.html", **context),
115 status_code,
116 )
118 @store.route("/store")
119 def store_view():
120 return flask.render_template("store/store.html")
122 @store.route("/youtube", methods=["POST"])
123 def get_video_thumbnail_data():
124 body = flask.request.form
125 thumbnail_url = "https://www.googleapis.com/youtube/v3/videos"
126 thumbnail_data = session.get(
127 (
128 f"{thumbnail_url}?id={body['videoId']}"
129 f"&part=snippet&key={YOUTUBE_API_KEY}"
130 )
131 )
133 if thumbnail_data:
134 return thumbnail_data.json()
136 return {}
138 @store.route("/publisher/<regex('[a-z0-9-]*[a-z][a-z0-9-]*'):publisher>")
139 def publisher_details(publisher):
140 """
141 A view to display the publisher details page for specific publisher.
142 """
144 # 404 for the snap-quarantine publisher
145 if publisher == "snap-quarantine":
146 flask.abort(404)
148 publisher_content_path = flask.current_app.config["CONTENT_DIRECTORY"][
149 "PUBLISHER_PAGES"
150 ]
152 if publisher in ["kde", "snapcrafters", "jetbrains"]:
153 context = helpers.get_yaml(
154 publisher_content_path + publisher + ".yaml", typ="safe"
155 )
157 if not context:
158 flask.abort(404)
160 popular_snaps = helpers.get_yaml(
161 publisher_content_path + publisher + "-snaps.yaml",
162 typ="safe",
163 )
165 context["popular_snaps"] = (
166 popular_snaps["snaps"] if popular_snaps else []
167 )
169 if "publishers" in context:
170 context["snaps"] = []
171 for publisher in context["publishers"]:
172 snaps_results = []
173 try:
174 snaps_results = device_gateway.get_publisher_items(
175 publisher, size=500, page=1
176 )["_embedded"]["clickindex:package"]
177 except StoreApiError:
178 pass
180 for snap in snaps_results:
181 snap["icon_url"] = helpers.get_icon(snap["media"])
183 context["snaps"].extend(
184 [snap for snap in snaps_results if snap["apps"]]
185 )
187 featured_snaps = [
188 snap["package_name"] for snap in context["featured_snaps"]
189 ]
191 context["snaps"] = [
192 snap
193 for snap in context["snaps"]
194 if snap["package_name"] not in featured_snaps
195 ]
197 context["snaps_count"] = len(context["snaps"]) + len(
198 featured_snaps
199 )
201 return flask.render_template(
202 "store/publisher-details.html", **context
203 )
205 status_code = 200
206 error_info = {}
207 snaps_results = []
208 snaps = []
209 snaps_count = 0
210 publisher_details = {"display-name": publisher, "username": publisher}
212 snaps_results = device_gateway.find(
213 publisher=publisher,
214 fields=[
215 "title",
216 "summary",
217 "media",
218 "publisher",
219 ],
220 )["results"]
222 for snap in snaps_results:
223 item = snap["snap"]
224 item["package_name"] = snap["name"]
225 item["icon_url"] = helpers.get_icon(item["media"])
226 snaps.append(item)
228 snaps_count = len(snaps)
230 if snaps_count > 0:
231 publisher_details = snaps[0]["publisher"]
233 context = {
234 "snaps": snaps,
235 "snaps_count": snaps_count,
236 "publisher": publisher_details,
237 "error_info": error_info,
238 }
240 return (
241 flask.render_template(
242 "store/community-publisher-details.html", **context
243 ),
244 status_code,
245 )
247 @store.route("/store/categories/<category>")
248 def store_category(category):
249 status_code = 200
250 error_info = {}
251 snaps_results = []
253 snaps_results = device_gateway.get_category_items(
254 category=category, size=10, page=1
255 )["results"]
256 for snap in snaps_results:
257 snap["icon_url"] = helpers.get_icon(snap["media"])
259 # if the first snap (banner snap) doesn't have an icon, remove the last
260 # snap from the list to avoid a hanging snap (grid of 9)
261 if len(snaps_results) == 10 and snaps_results[0]["icon_url"] == "":
262 snaps_results = snaps_results[:-1]
264 for index in range(len(snaps_results)):
265 snaps_results[index] = logic.get_snap_banner_url(
266 snaps_results[index]
267 )
269 context = {
270 "category": category,
271 "has_featured": True,
272 "snaps": snaps_results,
273 "error_info": error_info,
274 }
276 return (
277 flask.render_template("store/_category-partial.html", **context),
278 status_code,
279 )
281 @store.route("/store/featured-snaps/<category>")
282 def featured_snaps_in_category(category):
283 snaps_results = []
285 snaps_results = device_gateway.get_category_items(
286 category=category, size=3, page=1
287 )["_embedded"]["clickindex:package"]
289 for snap in snaps_results:
290 snap["icon_url"] = helpers.get_icon(snap["media"])
292 return flask.jsonify(snaps_results)
294 @store.route("/store/sitemap.xml")
295 def sitemap():
296 base_url = "https://snapcraft.io/store"
298 snaps = []
299 page = 0
300 url = f"https://api.snapcraft.io/api/v1/snaps/search?page={page}"
301 while url:
302 response = session.get(url)
303 try:
304 snaps_response = response.json()
305 except Exception:
306 continue
308 for snap in snaps_response["_embedded"]["clickindex:package"]:
309 try:
310 last_udpated = (
311 parser.parse(snap["last_updated"])
312 .replace(tzinfo=None)
313 .strftime("%Y-%m-%d")
314 )
315 snaps.append(
316 {
317 "url": "https://snapcraft.io/"
318 + snap["package_name"],
319 "last_udpated": last_udpated,
320 }
321 )
322 except Exception:
323 continue
324 if "next" in snaps_response["_links"]:
325 url = snaps_response["_links"]["next"]["href"]
326 else:
327 url = None
329 xml_sitemap = flask.render_template(
330 "sitemap/sitemap.xml",
331 base_url=base_url,
332 links=snaps,
333 )
335 response = flask.make_response(xml_sitemap)
336 response.headers["Content-Type"] = "application/xml"
337 response.headers["Cache-Control"] = "public, max-age=43200"
339 return response
341 if store_query:
342 store.add_url_rule("/", "homepage", brand_store_view)
343 store.add_url_rule("/search", "search", brand_search_snap)
344 else:
345 store.add_url_rule("/store", "homepage", store_view)
347 @store.route("/<snap_name>/create-track", methods=["POST"])
348 @login_required
349 @csrf.exempt
350 @exchange_required
351 def post_create_track(snap_name):
352 track_name = flask.request.form["track-name"]
353 version_pattern = flask.request.form.get("version-pattern")
354 auto_phasing_percentage = flask.request.form.get(
355 "automatic-phasing-percentage"
356 )
358 if auto_phasing_percentage is not None:
359 auto_phasing_percentage = float(auto_phasing_percentage)
361 response = publisher_gateway.create_track(
362 flask.session,
363 snap_name,
364 track_name,
365 version_pattern,
366 auto_phasing_percentage,
367 )
368 if response.status_code == 201:
369 return response.json(), response.status_code
370 if response.status_code == 409:
371 return (
372 jsonify({"error": "Track already exists."}),
373 response.status_code,
374 )
375 if "error-list" in response.json():
376 return (
377 jsonify(
378 {"error": response.json()["error-list"][0]["message"]}
379 ),
380 response.status_code,
381 )
382 return response.json(), response.status_code
384 return store