Coverage for webapp/store/views.py: 49%
189 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-13 22:07 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-13 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("/youtube", methods=["POST"])
122 def get_video_thumbnail_data():
123 body = flask.request.form
124 thumbnail_url = "https://www.googleapis.com/youtube/v3/videos"
125 thumbnail_data = session.get(
126 (
127 f"{thumbnail_url}?id={body['videoId']}"
128 f"&part=snippet&key={YOUTUBE_API_KEY}"
129 )
130 )
132 if thumbnail_data:
133 return thumbnail_data.json()
135 return {}
137 @store.route("/publisher/<regex('[a-z0-9-]*[a-z][a-z0-9-]*'):publisher>")
138 def publisher_details(publisher):
139 """
140 A view to display the publisher details page for specific publisher.
141 """
143 # 404 for the snap-quarantine publisher
144 if publisher == "snap-quarantine":
145 flask.abort(404)
147 publisher_content_path = flask.current_app.config["CONTENT_DIRECTORY"][
148 "PUBLISHER_PAGES"
149 ]
151 if publisher in ["kde", "snapcrafters", "jetbrains"]:
152 context = helpers.get_yaml(
153 publisher_content_path + publisher + ".yaml", typ="safe"
154 )
156 if not context:
157 flask.abort(404)
159 popular_snaps = helpers.get_yaml(
160 publisher_content_path + publisher + "-snaps.yaml",
161 typ="safe",
162 )
164 context["popular_snaps"] = (
165 popular_snaps["snaps"] if popular_snaps else []
166 )
168 if "publishers" in context:
169 context["snaps"] = []
170 for publisher in context["publishers"]:
171 snaps_results = []
172 try:
173 snaps_results = device_gateway.get_publisher_items(
174 publisher, size=500, page=1
175 )["_embedded"]["clickindex:package"]
176 except StoreApiError:
177 pass
179 for snap in snaps_results:
180 snap["icon_url"] = helpers.get_icon(snap["media"])
182 context["snaps"].extend(
183 [snap for snap in snaps_results if snap["apps"]]
184 )
186 featured_snaps = [
187 snap["package_name"] for snap in context["featured_snaps"]
188 ]
190 context["snaps"] = [
191 snap
192 for snap in context["snaps"]
193 if snap["package_name"] not in featured_snaps
194 ]
196 context["snaps_count"] = len(context["snaps"]) + len(
197 featured_snaps
198 )
200 return flask.render_template(
201 "store/publisher-details.html", **context
202 )
204 status_code = 200
205 error_info = {}
206 snaps_results = []
207 snaps = []
208 snaps_count = 0
209 publisher_details = {"display-name": publisher, "username": publisher}
211 snaps_results = device_gateway.find(
212 publisher=publisher,
213 fields=[
214 "title",
215 "summary",
216 "media",
217 "publisher",
218 ],
219 )["results"]
221 for snap in snaps_results:
222 item = snap["snap"]
223 item["package_name"] = snap["name"]
224 item["icon_url"] = helpers.get_icon(item["media"])
225 snaps.append(item)
227 snaps_count = len(snaps)
229 if snaps_count > 0:
230 publisher_details = snaps[0]["publisher"]
232 context = {
233 "snaps": snaps,
234 "snaps_count": snaps_count,
235 "publisher": publisher_details,
236 "error_info": error_info,
237 }
239 return (
240 flask.render_template(
241 "store/community-publisher-details.html", **context
242 ),
243 status_code,
244 )
246 @store.route("/store/categories/<category>")
247 def store_category(category):
248 status_code = 200
249 error_info = {}
250 snaps_results = []
252 snaps_results = device_gateway.get_category_items(
253 category=category, size=10, page=1
254 )["results"]
255 for snap in snaps_results:
256 snap["icon_url"] = helpers.get_icon(snap["media"])
258 # if the first snap (banner snap) doesn't have an icon, remove the last
259 # snap from the list to avoid a hanging snap (grid of 9)
260 if len(snaps_results) == 10 and snaps_results[0]["icon_url"] == "":
261 snaps_results = snaps_results[:-1]
263 for index in range(len(snaps_results)):
264 snaps_results[index] = logic.get_snap_banner_url(
265 snaps_results[index]
266 )
268 context = {
269 "category": category,
270 "has_featured": True,
271 "snaps": snaps_results,
272 "error_info": error_info,
273 }
275 return (
276 flask.render_template("store/_category-partial.html", **context),
277 status_code,
278 )
280 @store.route("/store/featured-snaps/<category>")
281 def featured_snaps_in_category(category):
282 snaps_results = []
284 snaps_results = device_gateway.get_category_items(
285 category=category, size=3, page=1
286 )["_embedded"]["clickindex:package"]
288 for snap in snaps_results:
289 snap["icon_url"] = helpers.get_icon(snap["media"])
291 return flask.jsonify(snaps_results)
293 @store.route("/store/sitemap.xml")
294 def sitemap():
295 base_url = "https://snapcraft.io/store"
297 snaps = []
298 page = 0
299 url = f"https://api.snapcraft.io/api/v1/snaps/search?page={page}"
300 while url:
301 response = session.get(url)
302 try:
303 snaps_response = response.json()
304 except Exception:
305 continue
307 for snap in snaps_response["_embedded"]["clickindex:package"]:
308 try:
309 last_udpated = (
310 parser.parse(snap["last_updated"])
311 .replace(tzinfo=None)
312 .strftime("%Y-%m-%d")
313 )
314 snaps.append(
315 {
316 "url": "https://snapcraft.io/"
317 + snap["package_name"],
318 "last_udpated": last_udpated,
319 }
320 )
321 except Exception:
322 continue
323 if "next" in snaps_response["_links"]:
324 url = snaps_response["_links"]["next"]["href"]
325 else:
326 url = None
328 xml_sitemap = flask.render_template(
329 "sitemap/sitemap.xml",
330 base_url=base_url,
331 links=snaps,
332 )
334 response = flask.make_response(xml_sitemap)
335 response.headers["Content-Type"] = "application/xml"
336 response.headers["Cache-Control"] = "public, max-age=43200"
338 return response
340 if store_query:
341 store.add_url_rule("/", "homepage", brand_store_view)
342 store.add_url_rule("/search", "search", brand_search_snap)
343 else:
344 store.add_url_rule("/store", "homepage", store_view)
346 @store.route("/<snap_name>/create-track", methods=["POST"])
347 @login_required
348 @csrf.exempt
349 @exchange_required
350 def post_create_track(snap_name):
351 track_name = flask.request.form["track-name"]
352 version_pattern = flask.request.form.get("version-pattern")
353 auto_phasing_percentage = flask.request.form.get(
354 "automatic-phasing-percentage"
355 )
357 if auto_phasing_percentage is not None:
358 auto_phasing_percentage = float(auto_phasing_percentage)
360 response = publisher_gateway.create_track(
361 flask.session,
362 snap_name,
363 track_name,
364 version_pattern,
365 auto_phasing_percentage,
366 )
367 if response.status_code == 201:
368 return response.json(), response.status_code
369 if response.status_code == 409:
370 return (
371 jsonify({"error": "Track already exists."}),
372 response.status_code,
373 )
374 if "error-list" in response.json():
375 return (
376 jsonify(
377 {"error": response.json()["error-list"][0]["message"]}
378 ),
379 response.status_code,
380 )
381 return response.json(), response.status_code
383 return store