예제 #1
0
    get_icon,
    get_videos,
)
from webapp.decorators import login_required
from webapp.helpers import get_licenses
from webapp.publisher.snaps import logic
from webapp.publisher.snaps import preview_data

publisher_snaps = flask.Blueprint(
    "publisher_snaps",
    __name__,
    template_folder="/templates",
    static_folder="/static",
)

store_api = StoreApi(cache=False)


def refresh_redirect(path):
    try:
        macaroon_discharge = authentication.get_refreshed_discharge(
            flask.session["macaroon_discharge"]
        )
    except ApiResponseError as api_response_error:
        if api_response_error.status_code == 401:
            return flask.redirect(flask.url_for("login.logout"))
        else:
            return flask.abort(502, str(api_response_error))
    except ApiError as api_error:
        return flask.abort(502, str(api_error))
예제 #2
0
def store_blueprint(store_query=None, testing=False):
    api = StoreApi(store=store_query, testing=testing)

    store = flask.Blueprint(
        "store",
        __name__,
        template_folder="/templates",
        static_folder="/static",
    )

    def _handle_errors(api_error: ApiError):
        status_code = 502
        error = {"message": str(api_error)}

        if type(api_error) is ApiTimeoutError:
            status_code = 504
        elif type(api_error) is ApiResponseDecodeError:
            status_code = 502
        elif type(api_error) is ApiResponseErrorList:
            error["errors"] = api_error.errors
            status_code = 502
        elif type(api_error) is ApiResponseError:
            status_code = 502
        elif type(api_error) is ApiConnectionError:
            status_code = 502
        elif type(api_error) is ApiCircuitBreaker:
            # Special case for this one, because it is the only case where we
            # don't want the user to be able to access the page.
            return flask.abort(503)

        return status_code, error

    snap_details_views(store, api, _handle_errors)

    @store.route("/discover")
    def discover():
        return flask.redirect(flask.url_for(".homepage"))

    def store_view():
        error_info = {}
        status_code = 200

        try:
            categories_results = api.get_categories()
        except ApiError as api_error:
            categories_results = []
            status_code, error_info = _handle_errors(api_error)

        categories = logic.get_categories(categories_results)

        try:
            featured_snaps_results = api.get_searched_snaps(
                snap_searched="", category="featured", size=24, page=1)
        except ApiError:
            featured_snaps_results = []

        featured_snaps = logic.get_searched_snaps(featured_snaps_results)

        return (
            flask.render_template(
                "store/store.html",
                categories=categories,
                featured_snaps=featured_snaps,
                error_info=error_info,
            ),
            status_code,
        )

    def brand_store_view():
        error_info = {}
        status_code = 200

        try:
            snaps_results = api.get_all_snaps(size=16)
        except ApiError as api_error:
            snaps_results = []
            status_code, error_info = _handle_errors(api_error)

        snaps = logic.get_searched_snaps(snaps_results)

        return (
            flask.render_template("brand-store/store.html",
                                  snaps=snaps,
                                  error_info=error_info),
            status_code,
        )

    def search_snap():
        status_code = 200
        snap_searched = flask.request.args.get("q", default="", type=str)
        snap_category = flask.request.args.get("category",
                                               default="",
                                               type=str)

        if snap_category:
            snap_category_display = snap_category.capitalize().replace(
                "-", " ")
        else:
            snap_category_display = None

        if not snap_searched and not snap_category:
            return flask.redirect(flask.url_for(".homepage"))

        size = flask.request.args.get("limit", default=24, type=int)
        offset = flask.request.args.get("offset", default=0, type=int)

        try:
            page = floor(offset / size) + 1
        except ZeroDivisionError:
            size = 10
            page = floor(offset / size) + 1

        error_info = {}
        categories_results = []
        searched_results = []

        try:
            categories_results = api.get_categories()
        except ApiError as api_error:
            status_code, error_info = _handle_errors(api_error)

        categories = logic.get_categories(categories_results)

        try:
            searched_results = api.get_searched_snaps(
                quote_plus(snap_searched),
                category=snap_category,
                size=size,
                page=page,
            )
        except ApiError as api_error:
            status_code, error_info = _handle_errors(api_error)

        if "total" in searched_results:
            total_results_count = searched_results["total"]
        else:
            total_results_count = None

        snaps_results = logic.get_searched_snaps(searched_results)
        links = logic.get_pages_details(
            flask.request.base_url,
            (searched_results["_links"]
             if "_links" in searched_results else []),
        )

        context = {
            "query": snap_searched,
            "category": snap_category,
            "category_display": snap_category_display,
            "categories": categories,
            "snaps": snaps_results,
            "total": total_results_count,
            "links": links,
            "error_info": error_info,
        }

        return (
            flask.render_template("store/search.html", **context),
            status_code,
        )

    def brand_search_snap():
        status_code = 200
        snap_searched = flask.request.args.get("q", default="", type=str)

        if not snap_searched:
            return flask.redirect(flask.url_for(".homepage"))

        size = flask.request.args.get("limit", default=25, type=int)
        offset = flask.request.args.get("offset", default=0, type=int)

        try:
            page = floor(offset / size) + 1
        except ZeroDivisionError:
            size = 10
            page = floor(offset / size) + 1

        error_info = {}
        searched_results = []

        try:
            searched_results = api.get_searched_snaps(
                quote_plus(snap_searched), size=size, page=page)
        except ApiError as api_error:
            status_code, error_info = _handle_errors(api_error)

        snaps_results = logic.get_searched_snaps(searched_results)
        links = logic.get_pages_details(
            flask.request.base_url,
            (searched_results["_links"]
             if "_links" in searched_results else []),
        )

        context = {
            "query": snap_searched,
            "snaps": snaps_results,
            "links": links,
            "error_info": error_info,
        }

        return (
            flask.render_template("brand-store/search.html", **context),
            status_code,
        )

    def _get_file(file):
        try:
            with open(os.path.join(flask.current_app.root_path, file),
                      "r") as stream:
                data = yaml.load(stream)
        except Exception:
            data = None

        return data

    @store.route("/publisher/<regex('[a-z0-9-]*[a-z][a-z0-9-]*'):publisher>")
    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 = _get_file(publisher_content_path + publisher + ".yaml")

        if not context:
            flask.abort(404)

        return flask.render_template("store/publisher-details.html", **context)

    @store.route("/store/categories/<category>")
    def store_category(category):
        status_code = 200
        error_info = {}
        category_results = []

        try:
            category_results = api.get_searched_snaps(snap_searched="",
                                                      category=category,
                                                      size=24,
                                                      page=1)
        except ApiError as api_error:
            status_code, error_info = _handle_errors(api_error)

        snaps_results = logic.get_searched_snaps(category_results)

        context = {
            "category": category,
            "snaps": snaps_results,
            "error_info": error_info,
        }

        return (
            flask.render_template("store/_category-partial.html", **context),
            status_code,
        )

    if store_query:
        store.add_url_rule("/", "homepage", brand_store_view)
        store.add_url_rule("/search", "search", brand_search_snap)
    else:
        store.add_url_rule("/store", "homepage", store_view)
        store.add_url_rule("/search", "search", search_snap)

    return store
예제 #3
0
def store_blueprint(store_query=None, testing=False):
    api = StoreApi(store=store_query, testing=testing)

    store = flask.Blueprint(
        "store",
        __name__,
        template_folder="/templates",
        static_folder="/static",
    )

    def _handle_errors(api_error: ApiError):
        status_code = 502
        error = {"message": str(api_error)}

        if type(api_error) is ApiTimeoutError:
            status_code = 504
        elif type(api_error) is ApiResponseDecodeError:
            status_code = 502
        elif type(api_error) is ApiResponseErrorList:
            error["errors"] = api_error.errors
            status_code = 502
        elif type(api_error) is ApiResponseError:
            status_code = 502
        elif type(api_error) is ApiConnectionError:
            status_code = 502
        elif type(api_error) is ApiCircuitBreaker:
            # Special case for this one, because it is the only case where we
            # don't want the user to be able to access the page.
            return flask.abort(503)

        return status_code, error

    @store.route("/discover")
    def discover():
        return flask.redirect(flask.url_for(".homepage"))

    def store_view():
        error_info = {}
        status_code = 200

        try:
            categories_results = api.get_categories()
        except ApiError as api_error:
            categories_results = []
            status_code, error_info = _handle_errors(api_error)

        categories = logic.get_categories(categories_results)

        return (
            flask.render_template(
                "store/store.html",
                categories=categories,
                error_info=error_info,
            ),
            status_code,
        )

    def brand_store_view():
        error_info = {}
        status_code = 200

        try:
            snaps_results = api.get_all_snaps(size=12)
        except ApiError as api_error:
            snaps_results = []
            status_code, error_info = _handle_errors(api_error)

        snaps = logic.get_searched_snaps(snaps_results)

        return (
            flask.render_template("brand-store/store.html",
                                  snaps=snaps,
                                  error_info=error_info),
            status_code,
        )

    def search_snap():
        status_code = 200
        snap_searched = flask.request.args.get("q", default="", type=str)
        snap_category = flask.request.args.get("category",
                                               default="",
                                               type=str)

        if snap_category:
            snap_category_display = snap_category.capitalize().replace(
                "-", " ")
        else:
            snap_category_display = None

        if not snap_searched and not snap_category:
            return flask.redirect(flask.url_for(".homepage"))

        size = flask.request.args.get("limit", default=25, type=int)
        offset = flask.request.args.get("offset", default=0, type=int)

        try:
            page = floor(offset / size) + 1
        except ZeroDivisionError:
            size = 10
            page = floor(offset / size) + 1

        error_info = {}
        featured_snaps = []
        categories_results = []
        searched_results = []

        try:
            categories_results = api.get_categories()
        except ApiError as api_error:
            status_code, error_info = _handle_errors(api_error)

        categories = logic.get_categories(categories_results)

        try:
            searched_results = api.get_searched_snaps(
                quote_plus(snap_searched),
                category=snap_category,
                size=size,
                page=page,
            )
        except ApiError as api_error:
            status_code, error_info = _handle_errors(api_error)

        snaps_results = logic.get_searched_snaps(searched_results)
        links = logic.get_pages_details(
            flask.request.base_url,
            (searched_results["_links"]
             if "_links" in searched_results else []),
        )

        if not snaps_results:
            featured_snaps_results = []
            try:
                featured_snaps_results = api.get_featured_snaps()
            except ApiError as api_error:
                status_code, error_info = _handle_errors(api_error)

            featured_snaps = logic.get_searched_snaps(featured_snaps_results)

        context = {
            "query": snap_searched,
            "category": snap_category,
            "category_display": snap_category_display,
            "categories": categories,
            "snaps": snaps_results,
            "links": links,
            "featured_snaps": featured_snaps,
            "error_info": error_info,
        }

        return (
            flask.render_template("store/search.html", **context),
            status_code,
        )

    def brand_search_snap():
        status_code = 200
        snap_searched = flask.request.args.get("q", default="", type=str)

        if not snap_searched:
            return flask.redirect(flask.url_for(".homepage"))

        size = flask.request.args.get("limit", default=25, type=int)
        offset = flask.request.args.get("offset", default=0, type=int)

        try:
            page = floor(offset / size) + 1
        except ZeroDivisionError:
            size = 10
            page = floor(offset / size) + 1

        error_info = {}
        searched_results = []

        try:
            searched_results = api.get_searched_snaps(
                quote_plus(snap_searched), size=size, page=page)
        except ApiError as api_error:
            status_code, error_info = _handle_errors(api_error)

        snaps_results = logic.get_searched_snaps(searched_results)
        links = logic.get_pages_details(
            flask.request.base_url,
            (searched_results["_links"]
             if "_links" in searched_results else []),
        )

        context = {
            "query": snap_searched,
            "snaps": snaps_results,
            "links": links,
            "error_info": error_info,
        }

        return (
            flask.render_template("brand-store/search.html", **context),
            status_code,
        )

    @store.route('/<regex("[a-z0-9-]*[a-z][a-z0-9-]*"):snap_name>')
    def snap_details(snap_name):
        """
        A view to display the snap details page for specific snaps.

        This queries the snapcraft API (api.snapcraft.io) and passes
        some of the data through to the snap-details.html template,
        with appropriate sanitation.
        """

        error_info = {}
        status_code = 200

        try:
            details = api.get_snap_details(snap_name)
        except ApiTimeoutError as api_timeout_error:
            flask.abort(504, str(api_timeout_error))
        except ApiResponseDecodeError as api_response_decode_error:
            flask.abort(502, str(api_response_decode_error))
        except ApiResponseErrorList 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 ApiResponseError as api_response_error:
            flask.abort(502, str(api_response_error))
        except ApiCircuitBreaker:
            flask.abort(503)
        except ApiError as api_error:
            flask.abort(502, str(api_error))

        # When removing all the channel maps of an exsting 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))

        formatted_paragraphs = logic.split_description_into_paragraphs(
            details["snap"]["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"]

        country_metric_name = "weekly_installed_base_by_country_percent"
        os_metric_name = "weekly_installed_base_by_operating_system_normalized"

        webapp_config = flask.current_app.config.get("WEBAPP_CONFIG")

        if "STORE_QUERY" not in webapp_config:
            end = metrics_helper.get_last_metrics_processed_date()

            metrics_query_json = [
                metrics_helper.get_filter(
                    metric_name=country_metric_name,
                    snap_id=details["snap-id"],
                    start=end,
                    end=end,
                ),
                metrics_helper.get_filter(
                    metric_name=os_metric_name,
                    snap_id=details["snap-id"],
                    start=end,
                    end=end,
                ),
            ]

            try:
                metrics_response = api.get_public_metrics(
                    snap_name, metrics_query_json)
            except ApiError as api_error:
                status_code, error_info = _handle_errors(api_error)
                metrics_response = None

            os_metrics = None
            country_devices = None
            if metrics_response:
                oses = metrics_helper.find_metric(metrics_response,
                                                  os_metric_name)
                os_metrics = metrics.OsMetric(
                    name=oses["metric_name"],
                    series=oses["series"],
                    buckets=oses["buckets"],
                    status=oses["status"],
                )

                territories = metrics_helper.find_metric(
                    metrics_response, country_metric_name)
                country_devices = metrics.CountryDevices(
                    name=territories["metric_name"],
                    series=territories["series"],
                    buckets=territories["buckets"],
                    status=territories["status"],
                    private=False,
                )
        else:
            os_metrics = None
            country_devices = None

        # filter out banner and banner-icon images from screenshots
        screenshots = [
            m["url"] for m in details["snap"]["media"]
            if m["type"] == "screenshot" and "banner" not in m["url"]
        ]
        icons = [
            m["url"] for m in details["snap"]["media"] if m["type"] == "icon"
        ]

        # until default tracks are supported by the API we special case node
        # to use 10, rather then latest
        default_track = "10" if details["name"] == "node" 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)

        context = {
            # Data direct from details API
            "snap_title":
            details["snap"]["title"],
            "package_name":
            details["name"],
            "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,
            "prices":
            details["snap"]["prices"],
            "contact":
            details["snap"].get("contact"),
            "website":
            details["snap"].get("website"),
            "summary":
            details["snap"]["summary"],
            "description_paragraphs":
            formatted_paragraphs,
            "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,
            # Transformed API data
            "filesize":
            humanize.naturalsize(binary_filesize),
            "last_updated": (humanize.naturaldate(parser.parse(last_updated))),
            "last_updated_raw":
            last_updated,
            # Data from metrics API
            "countries":
            (country_devices.country_data if country_devices else None),
            "normalized_os":
            os_metrics.os if os_metrics else None,
            # Context info
            "is_linux":
            ("Linux" in flask.request.headers.get("User-Agent", "")
             and "Android" not in flask.request.headers.get("User-Agent", "")),
            "error_info":
            error_info,
        }

        return (
            flask.render_template("store/snap-details.html", **context),
            status_code,
        )

    @store.route('/<regex("[A-Za-z0-9-]*[A-Za-z][A-Za-z0-9-]*"):snap_name>')
    def snap_details_case_sensitive(snap_name):
        return flask.redirect(
            flask.url_for(".snap_details", snap_name=snap_name.lower()))

    @store.route("/store/categories/<category>")
    def store_category(category):
        status_code = 200
        error_info = {}
        category_results = []

        try:
            category_results = api.get_searched_snaps(snap_searched="",
                                                      category=category,
                                                      size=24,
                                                      page=1)
        except ApiError as api_error:
            status_code, error_info = _handle_errors(api_error)

        snaps_results = logic.get_searched_snaps(category_results)

        context = {
            "category": category,
            "snaps": snaps_results,
            "error_info": error_info,
        }

        return (
            flask.render_template("store/_category-partial.html", **context),
            status_code,
        )

    if store_query:
        store.add_url_rule("/", "homepage", brand_store_view)
        store.add_url_rule("/search", "search", brand_search_snap)
    else:
        store.add_url_rule("/store", "homepage", store_view)
        store.add_url_rule("/search", "search", search_snap)

    return store
예제 #4
0
def store_blueprint(store_query=None, testing=False):
    api = StoreApi(store=store_query, testing=testing)

    store = flask.Blueprint(
        "store",
        __name__,
        template_folder="/templates",
        static_folder="/static",
    )

    def _handle_errors(api_error: ApiError):
        status_code = 502
        error = {"message": str(api_error)}

        if type(api_error) is ApiTimeoutError:
            status_code = 504
        elif type(api_error) is ApiResponseDecodeError:
            status_code = 502
        elif type(api_error) is ApiResponseErrorList:
            error["errors"] = api_error.errors
            status_code = 502
        elif type(api_error) is ApiResponseError:
            status_code = 502
        elif type(api_error) is ApiConnectionError:
            status_code = 502
        elif type(api_error) is ApiCircuitBreaker:
            # Special case for this one, because it is the only case where we
            # don't want the user to be able to access the page.
            return flask.abort(503)

        return status_code, error

    snap_details_views(store, api, _handle_errors)

    @store.route("/discover")
    def discover():
        return flask.redirect(flask.url_for(".homepage"))

    def store_view():
        error_info = {}
        status_code = 200

        try:
            categories_results = api.get_categories()
        except ApiError as api_error:
            categories_results = []
            status_code, error_info = _handle_errors(api_error)

        categories = logic.get_categories(categories_results)

        try:
            featured_snaps_results = api.get_searched_snaps(
                snap_searched="", category="featured", size=10, page=1
            )
        except ApiError:
            featured_snaps_results = []

        featured_snaps = logic.get_searched_snaps(featured_snaps_results)

        # if the first snap (banner snap) doesn't have an icon, remove the last
        # snap from the list to avoid a hanging snap (grid of 9)
        if len(featured_snaps) == 10 and featured_snaps[0]["icon_url"] == "":
            featured_snaps = featured_snaps[:-1]

        for index in range(len(featured_snaps)):
            featured_snaps[index] = logic.get_snap_banner_url(
                featured_snaps[index]
            )

        livestream = snapcraft_logic.get_livestreams()

        return (
            flask.render_template(
                "store/store.html",
                categories=categories,
                has_featured=True,
                featured_snaps=featured_snaps,
                error_info=error_info,
                livestream=livestream,
            ),
            status_code,
        )

    def brand_store_view():
        error_info = {}
        status_code = 200

        try:
            snaps_results = api.get_all_snaps(size=16)
        except ApiError as api_error:
            snaps_results = []
            status_code, error_info = _handle_errors(api_error)

        snaps = logic.get_searched_snaps(snaps_results)

        return (
            flask.render_template(
                "brand-store/store.html", snaps=snaps, error_info=error_info
            ),
            status_code,
        )

    def search_snap():
        status_code = 200
        snap_searched = flask.request.args.get("q", default="", type=str)
        snap_category = flask.request.args.get(
            "category", default="", type=str
        )
        page = flask.request.args.get("page", default=1, type=int)

        if snap_category:
            snap_category_display = snap_category.capitalize().replace(
                "-", " "
            )
        else:
            snap_category_display = None

        if not snap_searched and not snap_category:
            return flask.redirect(flask.url_for(".homepage"))

        # The default size should be 44 (rows of 4)
        # it's important that this is smaller than the category page 1 size
        # below otherwise snaps can be missed out of results
        size = 44

        # Page 1 has a snap at the top, and a few rows of 3, followed by rows
        # of 4 - so we need to offset to ensure there's no hanging snap
        if snap_category and page == 1:
            size = 47

        error_info = {}
        searched_results = []

        try:
            searched_results = api.get_searched_snaps(
                quote_plus(snap_searched),
                category=snap_category,
                size=size,
                page=page,
            )
        except ApiError as api_error:
            status_code, error_info = _handle_errors(api_error)

        total_pages = None

        if "total" in searched_results:
            total_results_count = searched_results["total"]
            total_pages = ceil(total_results_count / size)
        else:
            total_results_count = None

        snaps_results = logic.get_searched_snaps(searched_results)

        links = {}

        if page > 1:
            links["first"] = logic.build_pagination_link(
                snap_searched=snap_searched,
                snap_category=snap_category,
                page=1,
            )
            links["prev"] = logic.build_pagination_link(
                snap_searched=snap_searched,
                snap_category=snap_category,
                page=page - 1,
            )

        if not total_pages or page < total_pages:
            links["next"] = logic.build_pagination_link(
                snap_searched=snap_searched,
                snap_category=snap_category,
                page=page + 1,
            )
            if total_pages:
                links["last"] = logic.build_pagination_link(
                    snap_searched=snap_searched,
                    snap_category=snap_category,
                    page=total_pages,
                )

        featured_snaps = []

        # These are the hand-selected "featured snaps" in each category.
        # We don't have this information on the API, so it's hardcoded.
        number_of_featured_snaps = 19

        if snap_category_display and page == 1:
            if snaps_results and snaps_results[0]:
                if snaps_results[0]["icon_url"] == "":
                    snaps_results = logic.promote_snap_with_icon(snaps_results)

                snaps_results[0] = logic.get_snap_banner_url(snaps_results[0])

                if (
                    snap_category == "featured"
                    or len(snaps_results) < number_of_featured_snaps
                ):
                    featured_snaps = snaps_results
                    snaps_results = []
                else:
                    featured_snaps = snaps_results[:number_of_featured_snaps]
                    snaps_results = snaps_results[number_of_featured_snaps:]

        context = {
            "query": snap_searched,
            "category": snap_category,
            "category_display": snap_category_display,
            "searched_snaps": snaps_results,
            "featured_snaps": featured_snaps,
            "total": total_results_count,
            "links": links,
            "page": page,
            "error_info": error_info,
        }

        return (
            flask.render_template("store/search.html", **context),
            status_code,
        )

    def brand_search_snap():
        status_code = 200
        snap_searched = flask.request.args.get("q", default="", type=str)

        if not snap_searched:
            return flask.redirect(flask.url_for(".homepage"))

        size = flask.request.args.get("limit", default=25, type=int)
        offset = flask.request.args.get("offset", default=0, type=int)

        try:
            page = floor(offset / size) + 1
        except ZeroDivisionError:
            size = 10
            page = floor(offset / size) + 1

        error_info = {}
        searched_results = []

        try:
            searched_results = api.get_searched_snaps(
                quote_plus(snap_searched), size=size, page=page
            )
        except ApiError as api_error:
            status_code, error_info = _handle_errors(api_error)

        snaps_results = logic.get_searched_snaps(searched_results)
        links = logic.get_pages_details(
            flask.request.base_url,
            (
                searched_results["_links"]
                if "_links" in searched_results
                else []
            ),
        )

        context = {
            "query": snap_searched,
            "snaps": snaps_results,
            "links": links,
            "error_info": error_info,
        }

        return (
            flask.render_template("brand-store/search.html", **context),
            status_code,
        )

    @store.route("/publisher/<regex('[a-z0-9-]*[a-z][a-z0-9-]*'):publisher>")
    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)

    @store.route("/store/categories/<category>")
    def store_category(category):
        status_code = 200
        error_info = {}
        category_results = []

        try:
            category_results = api.get_searched_snaps(
                snap_searched="", category=category, size=10, page=1
            )
        except ApiError as api_error:
            status_code, error_info = _handle_errors(api_error)

        snaps_results = logic.get_searched_snaps(category_results)

        # if the first snap (banner snap) doesn't have an icon, remove the last
        # snap from the list to avoid a hanging snap (grid of 9)
        if len(snaps_results) == 10 and snaps_results[0]["icon_url"] == "":
            snaps_results = snaps_results[:-1]

        for index in range(len(snaps_results)):
            snaps_results[index] = logic.get_snap_banner_url(
                snaps_results[index]
            )

        context = {
            "category": category,
            "has_featured": True,
            "snaps": snaps_results,
            "error_info": error_info,
        }

        return (
            flask.render_template("store/_category-partial.html", **context),
            status_code,
        )

    if store_query:
        store.add_url_rule("/", "homepage", brand_store_view)
        store.add_url_rule("/search", "search", brand_search_snap)
    else:
        store.add_url_rule("/store", "homepage", store_view)
        store.add_url_rule("/search", "search", search_snap)

    return store
예제 #5
0
def store_blueprint(store_query=None):
    api = StoreApi(store_query)

    store = flask.Blueprint('store',
                            __name__,
                            template_folder='/templates',
                            static_folder='/static')

    def _handle_errors(api_error: ApiError):
        status_code = 502
        error = {'message': str(api_error)}

        if type(api_error) is ApiTimeoutError:
            status_code = 504
        elif type(api_error) is ApiResponseDecodeError:
            status_code = 502
        elif type(api_error) is ApiResponseErrorList:
            error['errors'] = api_error.errors
            status_code = 502
        elif type(api_error) is ApiResponseError:
            status_code = 502
        elif type(api_error) is ApiConnectionError:
            status_code = 502

        return status_code, error

    @store.route('/snaps')
    def snaps_view():
        return flask.redirect(flask.url_for('.homepage'))

    @store.route('/discover')
    def discover():
        return flask.redirect(flask.url_for('.homepage'))

    def store_view():
        error_info = {}
        status_code = 200

        try:
            categories_results = api.get_categories()
        except ApiError as api_error:
            categories_results = []
            status_code, error_info = _handle_errors(api_error)

        categories = logic.get_categories(categories_results)

        try:
            featured_snaps_results = api.get_featured_snaps()
        except ApiError as api_error:
            featured_snaps_results = []
            status_code, error_info = _handle_errors(api_error)

        featured_snaps = logic.get_searched_snaps(featured_snaps_results)

        return flask.render_template('store/store.html',
                                     featured_snaps=featured_snaps,
                                     categories=categories,
                                     error_info=error_info), status_code

    def brand_store_view():
        error_info = {}
        status_code = 200

        try:
            snaps_results = api.get_all_snaps(size=12)
        except ApiError as api_error:
            snaps_results = []
            status_code, error_info = _handle_errors(api_error)

        snaps = logic.get_searched_snaps(snaps_results)

        return flask.render_template('brand-store/store.html',
                                     snaps=snaps,
                                     error_info=error_info), status_code

    def search_snap():
        status_code = 200
        snap_searched = flask.request.args.get('q', default='', type=str)
        snap_category = flask.request.args.get('category',
                                               default='',
                                               type=str)

        if snap_category:
            snap_category_display = snap_category.capitalize().replace(
                '-', ' ')
        else:
            snap_category_display = None

        if not snap_searched and not snap_category:
            return flask.redirect(flask.url_for('.homepage'))

        size = flask.request.args.get('limit', default=25, type=int)
        offset = flask.request.args.get('offset', default=0, type=int)

        try:
            page = floor(offset / size) + 1
        except ZeroDivisionError:
            size = 10
            page = floor(offset / size) + 1

        error_info = {}
        featured_snaps = []
        categories_results = []
        searched_results = []

        try:
            categories_results = api.get_categories()
        except ApiError as api_error:
            status_code, error_info = _handle_errors(api_error)

        categories = logic.get_categories(categories_results)

        try:
            searched_results = api.get_searched_snaps(
                quote_plus(snap_searched),
                category=snap_category,
                size=size,
                page=page)
        except ApiError as api_error:
            status_code, error_info = _handle_errors(api_error)

        snaps_results = logic.get_searched_snaps(searched_results)
        links = logic.get_pages_details(flask.request.base_url,
                                        (searched_results['_links'] if '_links'
                                         in searched_results else []))

        if not snaps_results:
            featured_snaps_results = []
            try:
                featured_snaps_results = api.get_featured_snaps()
            except ApiError as api_error:
                status_code, error_info = _handle_errors(api_error)

            featured_snaps = logic.get_searched_snaps(featured_snaps_results)

        context = {
            "query": snap_searched,
            "category": snap_category,
            "category_display": snap_category_display,
            "categories": categories,
            "snaps": snaps_results,
            "links": links,
            "featured_snaps": featured_snaps,
            "error_info": error_info
        }

        return flask.render_template('store/search.html',
                                     **context), status_code

    def brand_search_snap():
        status_code = 200
        snap_searched = flask.request.args.get('q', default='', type=str)

        if not snap_searched:
            return flask.redirect(flask.url_for('.homepage'))

        size = flask.request.args.get('limit', default=25, type=int)
        offset = flask.request.args.get('offset', default=0, type=int)

        try:
            page = floor(offset / size) + 1
        except ZeroDivisionError:
            size = 10
            page = floor(offset / size) + 1

        error_info = {}
        searched_results = []

        try:
            searched_results = api.get_searched_snaps(
                quote_plus(snap_searched), size=size, page=page)
        except ApiError as api_error:
            status_code, error_info = _handle_errors(api_error)

        snaps_results = logic.get_searched_snaps(searched_results)
        links = logic.get_pages_details(flask.request.base_url,
                                        (searched_results['_links'] if '_links'
                                         in searched_results else []))

        context = {
            "query": snap_searched,
            "snaps": snaps_results,
            "links": links,
            "error_info": error_info
        }

        return flask.render_template('brand-store/search.html',
                                     **context), status_code

    @store.route('/<regex("[a-z0-9-]*[a-z][a-z0-9-]*"):snap_name>')
    def snap_details(snap_name):
        """
        A view to display the snap details page for specific snaps.

        This queries the snapcraft API (api.snapcraft.io) and passes
        some of the data through to the snap-details.html template,
        with appropriate sanitation.
        """

        error_info = {}

        try:
            details = api.get_snap_details(snap_name)
        except ApiTimeoutError as api_timeout_error:
            flask.abort(504, str(api_timeout_error))
        except ApiResponseDecodeError as api_response_decode_error:
            flask.abort(502, str(api_response_decode_error))
        except ApiResponseErrorList as api_response_error_list:
            if api_response_error_list.status_code == 404:
                flask.abort(404, 'No snap named {}'.format(snap_name))
            else:
                error_messages = ', '.join(
                    api_response_error_list.errors.key())
                flask.abort(502, error_messages)
        except ApiResponseError as api_response_error:
            flask.abort(502, str(api_response_error))
        except ApiError as api_error:
            flask.abort(502, str(api_error))

        # When removing all the channel maps of an exsting 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))

        formatted_paragraphs = logic.split_description_into_paragraphs(
            details['snap']['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']

        end = metrics_helper.get_last_metrics_processed_date()
        country_metric_name = 'weekly_installed_base_by_country_percent'
        os_metric_name = 'weekly_installed_base_by_operating_system_normalized'

        metrics_query_json = [
            metrics_helper.get_filter(metric_name=country_metric_name,
                                      snap_id=details['snap-id'],
                                      start=end,
                                      end=end),
            metrics_helper.get_filter(metric_name=os_metric_name,
                                      snap_id=details['snap-id'],
                                      start=end,
                                      end=end)
        ]

        status_code = 200
        try:
            metrics_response = api.get_public_metrics(snap_name,
                                                      metrics_query_json)
        except ApiError as api_error:
            status_code, error_info = _handle_errors(api_error)
            metrics_response = None

        os_metrics = None
        country_devices = None
        if metrics_response:
            oses = metrics_helper.find_metric(metrics_response, os_metric_name)
            os_metrics = metrics.OsMetric(name=oses['metric_name'],
                                          series=oses['series'],
                                          buckets=oses['buckets'],
                                          status=oses['status'])

            territories = metrics_helper.find_metric(metrics_response,
                                                     country_metric_name)
            country_devices = metrics.CountryDevices(
                name=territories['metric_name'],
                series=territories['series'],
                buckets=territories['buckets'],
                status=territories['status'],
                private=False)

        # filter out banner and banner-icon images from screenshots
        screenshots = [
            m['url'] for m in details['snap']['media']
            if m['type'] == "screenshot" and "banner" not in m['url']
        ]
        icons = [
            m['url'] for m in details['snap']['media'] if m['type'] == "icon"
        ]

        # until default tracks are supported by the API we special case node
        # to use 10, rather then latest
        default_track = '10' if details['name'] == 'node' 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)

        context = {
            # Data direct from details API
            'snap_title':
            details['snap']['title'],
            'package_name':
            details['name'],
            'icon_url':
            icons[0] if icons else None,
            'version':
            last_version,
            'license':
            details['snap']['license'],
            'publisher':
            details['snap']['publisher']['display-name'],
            'screenshots':
            screenshots,
            'prices':
            details['snap']['prices'],
            'contact':
            details['snap'].get('contact'),
            'website':
            details['snap'].get('website'),
            'summary':
            details['snap']['summary'],
            'description_paragraphs':
            formatted_paragraphs,
            '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,

            # Transformed API data
            'filesize':
            humanize.naturalsize(binary_filesize),
            'last_updated': (humanize.naturaldate(parser.parse(last_updated))),
            'last_updated_raw':
            last_updated,

            # Data from metrics API
            'countries':
            (country_devices.country_data if country_devices else None),
            'normalized_os':
            os_metrics.os if os_metrics else None,

            # Context info
            'is_linux':
            ('Linux' in flask.request.headers.get('User-Agent', '')
             and 'Android' not in flask.request.headers.get('User-Agent', '')),
            'error_info':
            error_info
        }

        return flask.render_template('store/snap-details.html',
                                     **context), status_code

    @store.route('/<regex("[A-Za-z0-9-]*[A-Za-z][A-Za-z0-9-]*"):snap_name>')
    def snap_details_case_sensitive(snap_name):
        return flask.redirect(
            flask.url_for('.snap_details', snap_name=snap_name.lower()))

    @store.route('/store/categories/<category>')
    def store_category(category):
        status_code = 200
        error_info = {}
        category_results = []

        try:
            category_results = api.get_searched_snaps(snap_searched='',
                                                      category=category,
                                                      size=24,
                                                      page=1)
        except ApiError as api_error:
            status_code, error_info = _handle_errors(api_error)

        snaps_results = logic.get_searched_snaps(category_results)

        context = {
            "category": category,
            "snaps": snaps_results,
            "error_info": error_info
        }

        return flask.render_template('store/_category-partial.html',
                                     **context), status_code

    if store_query:
        store.add_url_rule('/', 'homepage', brand_store_view)
        store.add_url_rule('/search', 'search', brand_search_snap)
    else:
        store.add_url_rule('/store', 'homepage', store_view)
        store.add_url_rule('/search', 'search', search_snap)

    return store
예제 #6
0
def snapcraft_blueprint():
    api = StoreApi()

    snapcraft = flask.Blueprint(
        'snapcraft', __name__,
        template_folder='/templates', static_folder='/static')

    def _handle_errors(api_error: ApiError):
        status_code = 502
        error = {
            'message': str(api_error)
        }

        if type(api_error) is ApiTimeoutError:
            status_code = 504
        elif type(api_error) is ApiResponseDecodeError:
            status_code = 502
        elif type(api_error) is ApiResponseErrorList:
            error['errors'] = api_error.errors
            status_code = 502
        elif type(api_error) is ApiResponseError:
            status_code = 502
        elif type(api_error) is ApiConnectionError:
            status_code = 502
        return status_code, error

    @snapcraft.route('/')
    def homepage():
        featured_snaps = []
        error_info = {}
        status_code = 200
        try:
            featured_snaps = logic.get_searched_snaps(
                api.get_featured_snaps()
            )
        except ApiError as api_error:
            status_code, error_info = _handle_errors(api_error)

        return flask.render_template(
            'index.html',
            featured_snaps=featured_snaps,
            error_info=error_info
        ), status_code

    @snapcraft.route('/docs', defaults={'path': ''})
    @snapcraft.route('/docs/<path:path>')
    def docs_redirect(path):
        return flask.redirect('https://docs.snapcraft.io/' + path)

    @snapcraft.route('/community')
    def community_redirect():
        return flask.redirect('/')

    @snapcraft.route('/create')
    def create_redirect():
        return flask.redirect('https://docs.snapcraft.io/build-snaps')

    @snapcraft.route('/favicon.ico')
    def favicon():
        return flask.redirect(
            'https://assets.ubuntu.com/v1/fdc99abe-ico_16px.png')

    return snapcraft