def publisher_details(publisher): """ A view to display the publisher details page for specific publisher. """ publisher_content_path = flask.current_app.config["CONTENT_DIRECTORY"][ "PUBLISHER_PAGES" ] context = helpers.get_yaml( publisher_content_path + publisher + ".yaml", typ="safe" ) if not context: flask.abort(404) popular_snaps = helpers.get_yaml( publisher_content_path + publisher + "-snaps.yaml", typ="safe", ) context["popular_snaps"] = ( popular_snaps["snaps"] if popular_snaps else [] ) if "publishers" in context: context["snaps"] = [] for publisher in context["publishers"]: snaps_results = [] try: snaps_results = api.get_publisher_items( publisher, size=500, page=1 )["results"] except StoreApiError: pass context["snaps"].extend( [snap for snap in snaps_results if snap["apps"]] ) featured_snaps = [ snap["package_name"] for snap in context["featured_snaps"] ] context["snaps"] = [ snap for snap in context["snaps"] if snap["package_name"] not in featured_snaps ] context["snaps_count"] = len(snaps_results) return flask.render_template("store/publisher-details.html", **context)
def get_livestreams(): """ Get available livestreams and decide whether they should be shown :returns: Dictionary of livestream details """ livestream_to_show = None livestreams = helpers.get_yaml("snapcraft/content/snapcraft_live.yaml", typ="safe") if livestreams: now = datetime.now() lead_time = 4 # 4 days cooldown_time = 2 # 2 days for livestream in livestreams: instance_lead_time = lead_time if "lead_time" in livestream: instance_lead_time = livestream["lead_time"] instance_cooldown_time = cooldown_time if "cooldown_time" in livestream: instance_cooldown_time = livestream["cooldown_time"] show_from = livestream["time"] - timedelta(days=instance_lead_time) show_until = livestream["time"] + timedelta( days=instance_cooldown_time) if show_from < now and show_until > now: livestream_to_show = livestream return livestream_to_show
def snap_distro_install(snap_name, distro): filename = f"store/content/distros/{distro}.yaml" distro_data = helpers.get_yaml(filename) if not distro_data: flask.abort(404) context = _get_context_snap_details(snap_name) context.update({ "distro": distro, "distro_name": distro_data["name"], "distro_logo": distro_data["logo"], "distro_logo_mono": distro_data["logo-mono"], "distro_color_1": distro_data["color-1"], "distro_color_2": distro_data["color-2"], "distro_install_steps": distro_data["install"], }) try: featured_snaps_results = api.get_searched_snaps( snap_searched="", category="featured", size=12, page=1) except ApiError: featured_snaps_results = [] featured_snaps = logic.get_searched_snaps(featured_snaps_results) context.update({"featured_snaps": featured_snaps}) return flask.render_template("store/snap-distro-install.html", **context)
def get_language_snapcraft_yaml(language): filename = f"first_snap/content/{language}/package.yaml" snapcraft_yaml_filename = f"first_snap/content/{language}/snapcraft.yaml" snap_name_cookie = f"fsf_snap_name_{language}" steps = helpers.get_yaml(filename, typ="rt") if not steps: return flask.abort(404) snap_name = steps["name"] if snap_name_cookie in flask.request.cookies: snap_name = flask.request.cookies.get(snap_name_cookie) snapcraft_yaml = helpers.get_file( snapcraft_yaml_filename, {"${name}": snap_name} ) if not snapcraft_yaml: return flask.abort(404) return flask.Response( snapcraft_yaml, mimetype="text/yaml", headers={"Content-Disposition": "attachment;filename=snapcraft.yaml"}, )
def get_build(language, operating_system): filename = f"first_snap/content/{language}/build.yaml" snap_name_cookie = f"fsf_snap_name_{language}" steps = helpers.get_yaml(filename, typ="rt") operating_system_parts = operating_system.split("-") operating_system_only = operating_system_parts[0] install_type = (operating_system_parts[1] if len(operating_system_parts) == 2 else "auto") if ((not steps) or (operating_system_only not in steps) or (install_type not in steps[operating_system_only])): return flask.abort(404) snap_name = steps["name"] if flask.session.get("openid"): user_name = flask.session["openid"]["nickname"] snap_name = snap_name.replace("{name}", user_name) if snap_name_cookie in flask.request.cookies: snap_name = flask.request.cookies.get(snap_name_cookie) context = { "language": language, "os": operating_system, "steps": steps[operating_system_only][install_type], "snap_name": snap_name, } return flask.render_template("first-snap/build.html", **context)
def get_build(language, operating_system): build_filename = f"first_snap/content/{language}/build.yaml" test_filename = f"first_snap/content/{language}/test.yaml" snap_name_cookie = f"fsf_snap_name_{language}" build_steps = helpers.get_yaml(build_filename, typ="rt") test_steps = helpers.get_yaml(test_filename, typ="rt") operating_system_parts = operating_system.split("-") operating_system_only = operating_system_parts[0] install_type = ( operating_system_parts[1] if len(operating_system_parts) == 2 else "auto" ) if ( (not (build_steps and test_steps)) or ( (operating_system_only not in build_steps) and (operating_system_only not in test_steps) ) or (install_type not in build_steps[operating_system_only]) ): return flask.abort(404) snap_name = build_steps["name"] if "publisher" in flask.session: user_name = flask.session["publisher"]["nickname"] snap_name = snap_name.replace("{name}", user_name) if snap_name_cookie in flask.request.cookies: snap_name = flask.request.cookies.get(snap_name_cookie) context = { "language": language, "os": operating_system, "build_steps": build_steps[operating_system_only][install_type], "test_steps": test_steps[operating_system_only], "snap_name": snap_name, "fsf_flow": FSF_FLOW, } return flask.render_template("first-snap/build-and-test.html", **context)
def get_package(language, operating_system): filename = f"first_snap/content/{language}/package.yaml" snapcraft_yaml_filename = f"first_snap/content/{language}/snapcraft.yaml" annotations_filename = "first_snap/content/snapcraft_yaml_annotations.yaml" snap_name_cookie = f"fsf_snap_name_{language}" steps = helpers.get_yaml(filename, typ="rt") if not steps: return flask.abort(404) snap_name = steps["name"] has_user_chosen_name = False if "publisher" in flask.session: user_name = flask.session["publisher"]["nickname"] snap_name = snap_name.replace("{name}", user_name) if snap_name_cookie in flask.request.cookies: snap_name = flask.request.cookies.get(snap_name_cookie) has_user_chosen_name = True context = { "language": language, "os": operating_system, "steps": steps, "snap_name": snap_name, "has_user_chosen_name": has_user_chosen_name, "fsf_flow": FSF_FLOW, } snapcraft_yaml = helpers.get_yaml( snapcraft_yaml_filename, typ="rt", replaces={"${name}": snap_name} ) annotations = helpers.get_yaml(annotations_filename, typ="rt") if snapcraft_yaml: context["snapcraft_yaml"] = transform_snapcraft_yaml(snapcraft_yaml) context["annotations"] = annotations return flask.render_template("first-snap/package.html", **context) else: return flask.abort(404)
def publisher_details(publisher): """ A view to display the publisher details page for specific publisher. """ publisher_content_path = flask.current_app.config["CONTENT_DIRECTORY"][ "PUBLISHER_PAGES"] context = helpers.get_yaml(publisher_content_path + publisher + ".yaml", typ="safe") if not context: flask.abort(404) return flask.render_template("store/publisher-details.html", **context)
def get_snap_developer(snap_name): """Is this a special snap published by Canonical? Show some developer information :param snap_name: The name of a snap :returns: a list of [display_name, url] """ filename = "store/content/developers/snaps.yaml" snaps = helpers.get_yaml(filename, typ="rt") if snaps and snap_name in snaps: return snaps[snap_name] return None
def snap_distro_install(snap_name, distro): filename = f"store/content/distros/{distro}.yaml" distro_data = helpers.get_yaml(filename) if not distro_data: flask.abort(404) context = _get_context_snap_details(snap_name) if distro == "raspbian": if ( "armhf" not in context["channel_map"] and "arm64" not in context["channel_map"] ): return flask.render_template("404.html"), 404 context.update( { "distro": distro, "distro_name": distro_data["name"], "distro_logo": distro_data["logo"], "distro_logo_mono": distro_data["logo-mono"], "distro_color_1": distro_data["color-1"], "distro_color_2": distro_data["color-2"], "distro_install_steps": distro_data["install"], } ) try: featured_snaps_results = api.get_featured_items(size=13, page=1) except StoreApiError: featured_snaps_results = [] featured_snaps = [ snap for snap in logic.get_searched_snaps(featured_snaps_results) if snap["package_name"] != snap_name ][:12] context.update({"featured_snaps": featured_snaps}) return flask.render_template( "store/snap-distro-install.html", **context )
def get_upload(language, operating_system): filename = f"first_snap/content/{language}/package.yaml" snap_name_cookie = f"fsf_snap_name_{language}" data = helpers.get_yaml(filename, typ="rt") if not data: return flask.abort(404) snap_name = data["name"] has_user_chosen_name = False if "publisher" in flask.session: user_name = flask.session["publisher"]["nickname"] snap_name = snap_name.replace("{name}", user_name) if snap_name_cookie in flask.request.cookies: snap_name = flask.request.cookies.get(snap_name_cookie) has_user_chosen_name = True flask_user = flask.session.get("publisher", {}) if "nickname" in flask_user: user = { "image": flask_user["image"], "username": flask_user["nickname"], "display_name": flask_user["fullname"], "email": flask_user["email"], } else: user = None context = { "language": language, "os": operating_system, "user": user, "snap_name": snap_name, "has_user_chosen_name": has_user_chosen_name, "fsf_flow": FSF_FLOW, } return flask.render_template("first-snap/upload.html", **context)
def get_test(language, operating_system): filename = f"first_snap/content/{language}/test.yaml" snap_name_cookie = f"fsf_snap_name_{language}" steps = helpers.get_yaml(filename, typ="rt") operating_system_only = operating_system.split("-")[0] if not steps or operating_system_only not in steps: return flask.abort(404) snap_name = steps["name"] if flask.session.get("openid"): user_name = flask.session["openid"]["nickname"] snap_name = snap_name.replace("{name}", user_name) if snap_name_cookie in flask.request.cookies: snap_name = flask.request.cookies.get(snap_name_cookie) converted_steps = [] for step in steps[operating_system_only]: action = logic.convert_md(step["action"]) converted_steps.append({ "action": action, "warning": step["warning"] if "warning" in step else None, "command": step["command"] if "command" in step else None, }) context = { "language": language, "os": operating_system, "steps": converted_steps, "snap_name": snap_name, } return flask.render_template("first-snap/test.html", **context)
def get_listing_snap(snap_name): try: snap_details = api.get_snap_info(snap_name, flask.session) except ApiResponseErrorList as api_response_error_list: if api_response_error_list.status_code == 404: return flask.abort(404, "No snap named {}".format(snap_name)) else: return _handle_error_list(api_response_error_list.errors) except ApiError as api_error: return _handle_error(api_error) details_metrics_enabled = snap_details["public_metrics_enabled"] details_blacklist = snap_details["public_metrics_blacklist"] is_on_stable = logic.is_snap_on_stable(snap_details["channel_maps_list"]) # Filter icon & screenshot urls from the media set. icon_urls, screenshot_urls, banner_urls = logic.categorise_media( snap_details["media"]) licenses = [] for license in get_licenses(): licenses.append({"key": license["licenseId"], "name": license["name"]}) license = snap_details["license"] license_type = "custom" if " AND " not in license.upper() and " WITH " not in license.upper(): license_type = "simple" referrer = None if flask.request.args.get("from"): referrer = flask.request.args.get("from") try: categories_results = store_api.get_categories() except StoreApiError: categories_results = [] categories = sorted( get_categories(categories_results), key=lambda category: category["slug"], ) snap_categories = logic.replace_reserved_categories_key( snap_details["categories"]) snap_categories = logic.filter_categories(snap_categories) snap_categories["categories"] = [ category["name"] for category in snap_categories["categories"] ] filename = f"publisher/content/listing_tour.yaml" tour_steps = helpers.get_yaml(filename, typ="rt") context = { "snap_id": snap_details["snap_id"], "snap_name": snap_details["snap_name"], "snap_title": snap_details["title"], "snap_categories": snap_categories, "summary": snap_details["summary"], "description": snap_details["description"], "icon_url": icon_urls[0] if icon_urls else None, "publisher_name": snap_details["publisher"]["display-name"], "username": snap_details["publisher"]["username"], "screenshot_urls": screenshot_urls, "banner_urls": banner_urls, "contact": snap_details["contact"], "private": snap_details["private"], "website": snap_details["website"] or "", "public_metrics_enabled": details_metrics_enabled, "public_metrics_blacklist": details_blacklist, "license": license, "license_type": license_type, "licenses": licenses, "video_urls": snap_details["video_urls"], "is_on_stable": is_on_stable, "from": referrer, "categories": categories, "tour_steps": tour_steps, } return flask.render_template("publisher/listing.html", **context)
def _get_context_snap_details(snap_name): try: details = api.get_item_details(snap_name, api_version=2) except StoreApiTimeoutError as api_timeout_error: flask.abort(504, str(api_timeout_error)) except StoreApiResponseDecodeError as api_response_decode_error: flask.abort(502, str(api_response_decode_error)) except StoreApiResponseErrorList as api_response_error_list: if api_response_error_list.status_code == 404: flask.abort(404, "No snap named {}".format(snap_name)) else: if api_response_error_list.errors: error_messages = ", ".join( api_response_error_list.errors.key()) else: error_messages = "An error occurred." flask.abort(502, error_messages) except StoreApiResponseError as api_response_error: flask.abort(502, str(api_response_error)) except StoreApiCircuitBreaker: flask.abort(503) except (StoreApiError, ApiError) as api_error: flask.abort(502, str(api_error)) # When removing all the channel maps of an existing snap the API, # responds that the snaps still exists with data. # Return a 404 if not channel maps, to avoid having a error. # For example: mir-kiosk-browser if not details.get("channel-map"): flask.abort(404, "No snap named {}".format(snap_name)) clean_description = bleach.clean(details["snap"]["description"], tags=[]) formatted_description = parse_markdown_description(clean_description) channel_maps_list = logic.convert_channel_maps( details.get("channel-map")) latest_channel = logic.get_last_updated_version( details.get("channel-map")) last_updated = latest_channel["created-at"] last_version = latest_channel["version"] binary_filesize = latest_channel["download"]["size"] # filter out banner and banner-icon images from screenshots screenshots = logic.filter_screenshots(details["snap"]["media"]) icons = logic.get_icon(details["snap"]["media"]) publisher_info = helpers.get_yaml( "{}{}.yaml".format( flask.current_app.config["CONTENT_DIRECTORY"] ["PUBLISHER_PAGES"], details["snap"]["publisher"]["username"], ), typ="safe", ) publisher_snaps = helpers.get_yaml( "{}{}-snaps.yaml".format( flask.current_app.config["CONTENT_DIRECTORY"] ["PUBLISHER_PAGES"], details["snap"]["publisher"]["username"], ), typ="safe", ) publisher_featured_snaps = None if publisher_info: publisher_featured_snaps = publisher_info.get("featured_snaps") publisher_snaps = logic.get_n_random_snaps( publisher_snaps["snaps"], 4) videos = logic.get_videos(details["snap"]["media"]) # until default tracks are supported by the API we special case node # to use 10, rather then latest default_track = helpers.get_default_track(details["name"]) if not default_track: default_track = (details.get("default-track") if details.get("default-track") else "latest") lowest_risk_available = logic.get_lowest_available_risk( channel_maps_list, default_track) confinement = logic.get_confinement(channel_maps_list, default_track, lowest_risk_available) last_version = logic.get_version(channel_maps_list, default_track, lowest_risk_available) is_users_snap = False if authentication.is_authenticated(flask.session): if (flask.session.get("openid").get("nickname") == details["snap"]["publisher"]["username"] ) or ("user_shared_snaps" in flask.session and snap_name in flask.session.get("user_shared_snaps")): is_users_snap = True # build list of categories of a snap categories = logic.get_snap_categories(details["snap"]["categories"]) developer = logic.get_snap_developer(details["name"]) context = { "snap-id": details.get("snap-id"), # Data direct from details API "snap_title": details["snap"]["title"], "package_name": details["name"], "categories": categories, "icon_url": icons[0] if icons else None, "version": last_version, "license": details["snap"]["license"], "publisher": details["snap"]["publisher"]["display-name"], "username": details["snap"]["publisher"]["username"], "screenshots": screenshots, "videos": videos, "publisher_snaps": publisher_snaps, "publisher_featured_snaps": publisher_featured_snaps, "has_publisher_page": publisher_info is not None, "prices": details["snap"]["prices"], "contact": details["snap"].get("contact"), "website": details["snap"].get("website"), "summary": details["snap"]["summary"], "description": formatted_description, "channel_map": channel_maps_list, "has_stable": logic.has_stable(channel_maps_list), "developer_validation": details["snap"]["publisher"]["validation"], "default_track": default_track, "lowest_risk_available": lowest_risk_available, "confinement": confinement, "trending": details["snap"]["trending"], # Transformed API data "filesize": humanize.naturalsize(binary_filesize), "last_updated": logic.convert_date(last_updated), "last_updated_raw": last_updated, "is_users_snap": is_users_snap, "unlisted": details["snap"]["unlisted"], "developer": developer, # TODO: This is horrible and hacky "appliances": { "adguard-home": "adguard", "mosquitto": "mosquitto", "nextcloud": "nextcloud", "plexmediaserver": "plex", "openhab": "openhab", }, } return context
def snap_posts(snap): try: blog_tags = blog_api.get_tag_by_name(f"sc:snap:{snap}") except NotFoundError: blog_tags = None blog_articles = None articles = [] third_party_blogs = get_yaml("blog/content/blog-posts.yaml") if third_party_blogs and snap in third_party_blogs: post = third_party_blogs[snap] cdn_image = "/".join( [ "https://res.cloudinary.com", "canonical", "image", "fetch", "f_auto,q_auto,fl_sanitize,w_346,h_231,c_fill", post["image"], ] ) brand_image = "https://assets.ubuntu.com/v1/aae0f33a-omgubuntu.svg" image_element = "".join( [ f'<img src="{cdn_image}" ', 'style="display:block">', f'<img src="{brand_image}" ', 'class="p-blog-post__source" />', ] ) articles.append( { "slug": post["uri"], "title": post["title"], "image": image_element, } ) if blog_tags: snapcraft_tag = blog_api.get_tag_by_name("snapcraft.io") try: blog_articles, total_pages = blog_api.get_articles( tags=blog_tags["id"], tags_exclude=[3184, 3265, 3408], per_page=3 - len(articles), ) except RequestException: blog_articles = [] for article in blog_articles: if article["image"]: featured_media = image_template( url=article["image"]["source_url"], alt="", width="346", height="231", fill=True, hi_def=True, loading="auto", ) else: featured_media = None url = f"/blog/{article['slug']}" if snapcraft_tag["id"] not in article["tags"]: url = f"https://ubuntu.com{url}" articles.append( { "slug": url, "title": article["title"]["rendered"], "image": featured_media, } ) return flask.jsonify(articles)
def post_listing_snap(snap_name): changes = None changed_fields = flask.request.form.get("changes") if changed_fields: changes = loads(changed_fields) if changes: snap_id = flask.request.form.get("snap_id") error_list = [] if "images" in changes: # Add existing screenshots try: current_screenshots = api.snap_screenshots( snap_id, flask.session) except ApiResponseErrorList as api_response_error_list: if api_response_error_list.status_code == 404: return flask.abort(404, "No snap named {}".format(snap_name)) else: return _handle_error_list(api_response_error_list.errors) except ApiError as api_error: return _handle_error(api_error) images_json, images_files = logic.build_changed_images( changes["images"], current_screenshots, flask.request.files.get("icon"), flask.request.files.getlist("screenshots"), flask.request.files.get("banner-image"), ) try: api.snap_screenshots(snap_id, flask.session, images_json, images_files) except ApiResponseErrorList as api_response_error_list: if api_response_error_list.status_code == 404: return flask.abort(404, "No snap named {}".format(snap_name)) else: error_list = error_list + api_response_error_list.errors except ApiError as api_error: return _handle_error(api_error) body_json = logic.filter_changes_data(changes) if body_json: if "description" in body_json: body_json["description"] = logic.remove_invalid_characters( body_json["description"]) try: api.snap_metadata(snap_id, flask.session, body_json) except ApiResponseErrorList as api_response_error_list: if api_response_error_list.status_code == 404: return flask.abort(404, "No snap named {}".format(snap_name)) else: error_list = error_list + api_response_error_list.errors except ApiError as api_error: return _handle_error(api_error) if error_list: try: snap_details = api.get_snap_info(snap_name, flask.session) except ApiResponseErrorList as api_response_error_list: if api_response_error_list.status_code == 404: return flask.abort(404, "No snap named {}".format(snap_name)) else: error_list = error_list + api_response_error_list.errors except ApiError as api_error: return _handle_error(api_error) field_errors, other_errors = logic.invalid_field_errors(error_list) details_metrics_enabled = snap_details["public_metrics_enabled"] details_blacklist = snap_details["public_metrics_blacklist"] is_on_stable = logic.is_snap_on_stable( snap_details["channel_maps_list"]) # Filter icon & screenshot urls from the media set. icon_urls, screenshot_urls, banner_urls = logic.categorise_media( snap_details["media"]) licenses = [] for license in get_licenses(): licenses.append({ "key": license["licenseId"], "name": license["name"] }) license = snap_details["license"] license_type = "custom" if (" AND " not in license.upper() and " WITH " not in license.upper()): license_type = "simple" try: categories_results = store_api.get_categories() except StoreApiError: categories_results = [] categories = get_categories(categories_results) snap_categories = logic.replace_reserved_categories_key( snap_details["categories"]) snap_categories = logic.filter_categories(snap_categories) filename = f"publisher/content/listing_tour.yaml" tour_steps = helpers.get_yaml(filename, typ="rt") context = { # read-only values from details API "snap_id": snap_details["snap_id"], "snap_name": snap_details["snap_name"], "snap_categories": snap_categories, "icon_url": icon_urls[0] if icon_urls else None, "publisher_name": snap_details["publisher"]["display-name"], "username": snap_details["publisher"]["username"], "screenshot_urls": screenshot_urls, "banner_urls": banner_urls, "display_title": snap_details["title"], # values posted by user "snap_title": (changes["title"] if "title" in changes else snap_details["title"] or ""), "summary": (changes["summary"] if "summary" in changes else snap_details["summary"] or ""), "description": (changes["description"] if "description" in changes else snap_details["description"] or ""), "contact": (changes["contact"] if "contact" in changes else snap_details["contact"] or ""), "private": snap_details["private"], "website": (changes["website"] if "website" in changes else snap_details["website"] or ""), "public_metrics_enabled": details_metrics_enabled, "video_urls": ([changes["video_urls"]] if "video_urls" in changes else snap_details["video_urls"]), "public_metrics_blacklist": details_blacklist, "license": license, "license_type": license_type, "licenses": licenses, "is_on_stable": is_on_stable, "categories": categories, # errors "error_list": error_list, "field_errors": field_errors, "other_errors": other_errors, "tour_steps": tour_steps, } return flask.render_template("publisher/listing.html", **context) flask.flash("Changes applied successfully.", "positive") else: flask.flash("No changes to save.", "information") return flask.redirect( flask.url_for(".get_listing_snap", snap_name=snap_name))
def test_get_yaml_error(self, mock_open_file): yaml_read = helpers.get_yaml("filename.yaml", typ="rt") self.assertEqual(yaml_read, None) mock_open_file.assert_called_with( os.path.join(self.app.root_path, "filename.yaml"), "r")
def snap_posts(snap): try: blog_tags = wordpress_api.get_tag_by_name(f"sc:snap:{snap}") except Exception: blog_tags = None blog_articles = None articles = [] third_party_blogs = get_yaml("blog/content/blog-posts.yaml") if snap in third_party_blogs: post = third_party_blogs[snap] cdn_image = "/".join([ "https://res.cloudinary.com", "canonical", "image", "fetch", "f_auto,q_auto,fl_sanitize,w_346,h_231,c_fill", post["image"], ]) brand_image = "https://assets.ubuntu.com/v1/aae0f33a-omgubuntu.svg" image_element = "".join([ f'<img src="{cdn_image}" ', 'style="display:block">', f'<img src="{brand_image}" ', 'class="p-blog-post__source" />', ]) articles.append({ "slug": post["uri"], "title": post["title"], "image": image_element, }) if blog_tags: try: blog_articles, total_pages = wordpress_api.get_articles( blog_tags["id"], 3 - len(articles)) except Exception: blog_articles = [] for article in blog_articles: try: featured_media = wordpress_api.get_media( article["featured_media"])["source_url"] featured_media = image_template( url=featured_media, alt="", width="346", height="231", hi_def=True, loading="auto", ) except Exception: featured_media = None transformed_article = logic.transform_article( article, featured_image=featured_media, author=None) articles.append({ "slug": "/blog/" + transformed_article["slug"], "title": transformed_article["title"]["rendered"], "image": transformed_article["image"], }) return flask.jsonify(articles)