Esempio n. 1
0
def post_build(snap_name):
    try:
        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)

    try:
        if launchpad.is_snap_building(details["snap_name"]):
            launchpad.cancel_snap_builds(details["snap_name"])

        launchpad.build_snap(details["snap_name"])
    except HTTPError as e:
        # Timeout or not found from Launchpad
        if e.response.status_code in [408, 404]:
            flask.flash("An error occurred, please try again.", "negative")
            return flask.redirect(
                flask.url_for(".get_snap_builds", snap_name=snap_name))
        raise e

    flask.flash("Build triggered", "positive")

    return flask.redirect(
        flask.url_for(".get_snap_builds", snap_name=snap_name))
Esempio n. 2
0
def get_publicise_badges(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)

    if snap_details["private"]:
        return flask.abort(404, "No snap named {}".format(snap_name))

    try:
        snap_public_details = store_api.get_item_details(snap_name,
                                                         api_version=2)
    except StoreApiError as api_error:
        return _handle_error(api_error)

    context = {
        "snap_name": snap_details["snap_name"],
        "snap_title": snap_details["title"],
        "snap_id": snap_details["snap_id"],
        "trending": snap_public_details["snap"]["trending"],
    }

    return flask.render_template("publisher/publicise/github_badges.html",
                                 **context)
Esempio n. 3
0
def post_update_gh_webhooks(snap_name):
    try:
        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)

    lp_snap = launchpad.get_snap_by_store_name(details["snap_name"])
    gh_link = lp_snap["git_repository_url"][19:]
    gh_owner, gh_repo = gh_link.split("/")

    github = GitHub(flask.session.get("github_auth_secret"))
    old_url = f"https://build.snapcraft.io/{gh_owner}/{gh_repo}/webhook/notify"
    old_hook = github.get_hook_by_url(gh_owner, gh_repo, old_url)

    if old_hook:
        github.update_hook_url(
            gh_owner,
            gh_repo,
            old_hook["id"],
            f"https://snapcraft.io/{snap_name}/webhook/notify",
        )

    return flask.redirect(
        flask.url_for(".get_snap_builds", snap_name=snap_name))
Esempio n. 4
0
def get_publicise_cards(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_errors(api_error)

    if snap_details["private"]:
        return flask.abort(404, "No snap named {}".format(snap_name))

    context = {
        "snap_name": snap_details["snap_name"],
        "snap_title": snap_details["title"],
        "snap_id": snap_details["snap_id"],
        "available": {},
        "download_version": "v1.2",
    }

    return flask.render_template(
        "publisher/publicise/embedded_cards.html", **context
    )
Esempio n. 5
0
def get_publicise_cards(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)

    if snap_details["private"]:
        return flask.abort(404, "No snap named {}".format(snap_name))

    screenshots = filter_screenshots(snap_details["media"])
    has_screenshot = True if screenshots else False

    context = {
        "has_screenshot": has_screenshot,
        "snap_name": snap_details["snap_name"],
        "snap_title": snap_details["title"],
        "snap_id": snap_details["snap_id"],
    }

    return flask.render_template("publisher/publicise/embedded_cards.html",
                                 **context)
Esempio n. 6
0
def get_release_history(snap_name):
    try:
        release_history = api.snap_release_history(flask.session, snap_name)
    except ApiResponseErrorList as api_response_error_list:
        return _handle_error_list(api_response_error_list.errors)
    except ApiError as api_error:
        return _handle_errors(api_error)

    try:
        info = 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_errors(api_error)

    context = {
        "snap_name": snap_name,
        "release_history": release_history,
        "private": info.get("private"),
        "channel_maps_list": info.get("channel_maps_list"),
    }

    return flask.render_template("publisher/release-history.html", **context)
Esempio n. 7
0
def get_release_history(snap_name):
    try:
        release_history = api.snap_release_history(flask.session, snap_name)
    except ApiResponseErrorList as api_response_error_list:
        return _handle_error_list(api_response_error_list.errors)
    except ApiError as api_error:
        return _handle_error(api_error)

    try:
        info = 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)

    context = {
        "snap_name": snap_name,
        "release_history": release_history,
        "private": info.get("private"),
        "channel_maps_list": info.get("channel_maps_list"),
        "default_track": info.get("default_track"),
    }

    return flask.render_template("publisher/release-history.html", **context)
Esempio n. 8
0
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_errors(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 = [
        m["url"] for m in snap_details["media"] if m["type"] == "icon"
    ]
    screenshot_urls = [
        m["url"] for m in snap_details["media"] if m["type"] == "screenshot"
    ]

    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"

    context = {
        "snap_id": snap_details["snap_id"],
        "snap_name": snap_details["snap_name"],
        "snap_title": snap_details["title"],
        "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,
        "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,
    }

    return flask.render_template("publisher/listing.html", **context)
Esempio n. 9
0
def get_publicise(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_errors(api_error)

    if snap_details["private"]:
        return flask.abort(404, "No snap named {}".format(snap_name))

    available_languages = {
        "en": {
            "title": "English",
            "text": "Get it from the Snap Store"
        },
        "de": {
            "title": "Deutsch",
            "text": "Installieren vom Snap Store"
        },
        "es": {
            "title": "Español",
            "text": "Instalar desde Snap Store"
        },
        "fr": {
            "title": "Français",
            "text": "Installer à partir du Snap Store",
        },
        "jp": {
            "title": "日本語",
            "text": "Snap Store から入手ください"
        },
        "ru": {
            "title": "русский язык",
            "text": "Загрузите из Snap Store"
        },
        "tw": {
            "title": "中文(台灣)",
            "text": "安裝軟體敬請移駕 Snap Store"
        },
    }

    context = {
        "snap_name": snap_details["snap_name"],
        "snap_title": snap_details["title"],
        "snap_id": snap_details["snap_id"],
        "available": available_languages,
        "download_version": "v1.2",
    }

    return flask.render_template("publisher/publicise.html", **context)
Esempio n. 10
0
def get_settings(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)

    if "whitelist_country_codes" in snap_details:
        whitelist_country_codes = (
            snap_details["whitelist_country_codes"]
            if len(snap_details["whitelist_country_codes"]) > 0 else [])
    else:
        whitelist_country_codes = []

    if "blacklist_country_codes" in snap_details:
        blacklist_country_codes = (
            snap_details["blacklist_country_codes"]
            if len(snap_details["blacklist_country_codes"]) > 0 else [])
    else:
        blacklist_country_codes = []

    countries = []
    for country in pycountry.countries:
        countries.append({"key": country.alpha_2, "name": country.name})

    is_on_lp = False
    lp_snap = launchpad.get_snap_by_store_name(snap_name)
    if lp_snap:
        is_on_lp = True

    context = {
        "snap_name": snap_details["snap_name"],
        "snap_title": snap_details["title"],
        "snap_id": snap_details["snap_id"],
        "publisher_name": snap_details["publisher"]["display-name"],
        "license": license,
        "private": snap_details["private"],
        "unlisted": snap_details["unlisted"],
        "countries": countries,
        "whitelist_country_codes": whitelist_country_codes,
        "blacklist_country_codes": blacklist_country_codes,
        "price": snap_details["price"],
        "store": snap_details["store"],
        "keywords": snap_details["keywords"],
        "status": snap_details["status"],
        "is_on_lp": is_on_lp,
    }

    return flask.render_template("publisher/settings.html", **context)
Esempio n. 11
0
def get_snap_builds(snap_name):
    try:
        details = api.get_snap_info(snap_name, flask.session)

        # API call to make users without needed permissions refresh the session
        # Users needs package_upload_request permission to use this feature
        api.get_package_upload_macaroon(session=flask.session,
                                        snap_name=snap_name,
                                        channels=["edge"])
    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)

    context = {
        "snap_id": details["snap_id"],
        "snap_name": details["snap_name"],
        "snap_title": details["title"],
        "snap_builds_enabled": False,
        "snap_builds": [],
        "total_builds": 0,
    }

    # Get built snap in launchpad with this store name
    github = GitHub(flask.session.get("github_auth_secret"))
    lp_snap = launchpad.get_snap_by_store_name(details["snap_name"])

    if lp_snap:
        # Git repository without GitHub hostname
        context["github_repository"] = lp_snap["git_repository_url"][19:]
        github_owner, github_repo = context["github_repository"].split("/")

        context["yaml_file_exists"] = github.get_snapcraft_yaml_location(
            github_owner, github_repo)

        if not context["yaml_file_exists"]:
            flask.flash("This repository doesn't contain a snapcraft.yaml",
                        "negative")
        context = get_builds(lp_snap, context, slice(0, BUILDS_PER_PAGE))
    else:
        try:
            context["github_user"] = github.get_user()
        except Unauthorized:
            context["github_user"] = None

        if context["github_user"]:
            context["github_orgs"] = github.get_orgs()

    return flask.render_template("publisher/builds.html", **context)
Esempio n. 12
0
def post_preview(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_errors(api_error)

    context = {
        "publisher": snap_details["publisher"]["display-name"],
        "username": snap_details["publisher"]["username"],
        "developer_validation": snap_details["publisher"]["validation"],
    }

    state = loads(flask.request.form["state"])

    for item in state:
        if item == "description":
            context[item] = parse_markdown_description(
                bleach.clean(state[item])
            )
        else:
            context[item] = state[item]

    context["is_preview"] = True
    context["package_name"] = context["snap_name"]
    context["snap_title"] = context["title"]

    # Images
    icons = get_icon(context["images"])
    context["screenshots"] = filter_screenshots(context["images"])
    context["icon_url"] = icons[0] if icons else None
    context["videos"] = get_videos(context["images"])

    # Channel map
    context["default_track"] = "latest"
    context["lowest_risk_available"] = "stable"
    context["version"] = "test"
    context["has_stable"] = True

    # metadata
    context["last_updated"] = "Preview"
    context["filesize"] = "1mb"

    # maps
    context["countries"] = preview_data.get_countries()
    context["normalized_os"] = preview_data.get_normalised_oses()

    return flask.render_template("store/snap-details.html", **context)
Esempio n. 13
0
def post_preview(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_errors(api_error)

    context = {
        "publisher": snap_details["publisher"]["display-name"],
        "username": snap_details["publisher"]["username"],
        "developer_validation": snap_details["publisher"]["validation"],
    }

    state = loads(flask.request.form["state"])

    for item in state:
        if item == "description":
            context[item] = parse_markdown_description(
                bleach.clean(state[item], tags=[])
            )
        else:
            context[item] = state[item]

    context["is_preview"] = True
    context["package_name"] = context["snap_name"]
    context["snap_title"] = context["title"]

    # Images
    icons = get_icon(context["images"])
    context["screenshots"] = filter_screenshots(context["images"])
    context["icon_url"] = icons[0] if icons else None
    context["videos"] = get_videos(context["images"])

    # Channel map
    context["default_track"] = "latest"
    context["lowest_risk_available"] = "stable"
    context["version"] = "test"
    context["has_stable"] = True

    # metadata
    context["last_updated"] = "Preview"
    context["filesize"] = "1mb"

    # maps
    context["countries"] = preview_data.get_countries()
    context["normalized_os"] = preview_data.get_normalised_oses()

    return flask.render_template("store/snap-details.html", **context)
Esempio n. 14
0
def get_settings(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_errors(api_error)

    if "whitelist_country_codes" in snap_details:
        whitelist_country_codes = (
            snap_details["whitelist_country_codes"]
            if len(snap_details["whitelist_country_codes"]) > 0
            else []
        )
    else:
        whitelist_country_codes = []

    if "blacklist_country_codes" in snap_details:
        blacklist_country_codes = (
            snap_details["blacklist_country_codes"]
            if len(snap_details["blacklist_country_codes"]) > 0
            else []
        )
    else:
        blacklist_country_codes = []

    countries = []
    for country in pycountry.countries:
        countries.append({"key": country.alpha_2, "name": country.name})

    context = {
        "snap_name": snap_details["snap_name"],
        "snap_title": snap_details["title"],
        "snap_id": snap_details["snap_id"],
        "license": license,
        "private": snap_details["private"],
        "unlisted": snap_details["unlisted"],
        "countries": countries,
        "whitelist_country_codes": whitelist_country_codes,
        "blacklist_country_codes": blacklist_country_codes,
        "price": snap_details["price"],
        "store": snap_details["store"],
        "keywords": snap_details["keywords"],
        "status": snap_details["status"],
    }

    return flask.render_template("publisher/settings.html", **context)
Esempio n. 15
0
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_errors(api_error)

    is_on_stable = logic.is_snap_on_stable(snap_details["channel_maps_list"])

    # Filter icon & screenshot urls from the media set.
    icon_urls = [
        m["url"] for m in snap_details["media"] if m["type"] == "icon"
    ]
    screenshot_urls = [
        m["url"] for m in snap_details["media"] if m["type"] == "screenshot"
    ]

    licenses = get_licenses()
    countries = []
    for country in pycountry.countries:
        countries.append({"alpha_2": country.alpha_2, "name": country.name})

    context = {
        "snap_id": snap_details["snap_id"],
        "snap_name": snap_details["snap_name"],
        "snap_title": snap_details["title"],
        "summary": snap_details["summary"],
        "description": snap_details["description"],
        "license": snap_details["license"],
        "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,
        "contact": snap_details["contact"],
        "private": snap_details["private"],
        "website": snap_details["website"] or "",
        "public_metrics_enabled": snap_details["public_metrics_enabled"],
        "public_metrics_blacklist": snap_details["public_metrics_blacklist"],
        "is_on_stable": is_on_stable,
        "licenses": licenses,
        "countries": countries,
    }

    return flask.render_template("publisher/listing.html", **context)
def get_snap_build(snap_name, build_id):
    try:
        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)

    context = {
        "snap_id": details["snap_id"],
        "snap_name": details["snap_name"],
        "snap_title": details["title"],
        "snap_build": {},
    }

    # Get build by snap name and build_id
    lp_build = launchpad.get_snap_build(details["snap_name"], build_id)

    if lp_build:
        status = map_build_and_upload_states(
            lp_build["buildstate"], lp_build["store_upload_status"]
        )
        context["snap_build"] = {
            "id": lp_build["self_link"].split("/")[-1],
            "arch_tag": lp_build["arch_tag"],
            "datebuilt": lp_build["datebuilt"],
            "duration": lp_build["duration"],
            "logs": lp_build["build_log_url"],
            "revision_id": lp_build["revision_id"],
            "status": status,
            "title": lp_build["title"],
        }

        if context["snap_build"]["logs"]:
            context["raw_logs"] = launchpad.get_snap_build_log(
                details["snap_name"], build_id
            )

    return flask.render_template("publisher/build.html", **context)
Esempio n. 17
0
def get_validate_repo(snap_name):
    try:
        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)

    owner, repo = flask.request.args.get("repo").split("/")

    return flask.jsonify(
        validate_repo(
            flask.session.get("github_auth_secret"),
            details["snap_name"],
            owner,
            repo,
        ))
Esempio n. 18
0
def post_build(snap_name):
    try:
        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)

    if launchpad.is_snap_building(details["snap_name"]):
        launchpad.cancel_snap_builds(details["snap_name"])

    launchpad.build_snap(details["snap_name"])

    flask.flash("Build triggered", "positive")

    return flask.redirect(
        flask.url_for(".get_snap_builds", snap_name=snap_name))
Esempio n. 19
0
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_errors(api_error)

    is_on_stable = logic.is_snap_on_stable(snap_details['channel_maps_list'])

    # Filter icon & screenshot urls from the media set.
    icon_urls = [
        m['url'] for m in snap_details['media'] if m['type'] == 'icon'
    ]
    screenshot_urls = [
        m['url'] for m in snap_details['media'] if m['type'] == 'screenshot'
    ]

    context = {
        "snap_id": snap_details['snap_id'],
        "snap_name": snap_details['snap_name'],
        "snap_title": snap_details['title'],
        "summary": snap_details['summary'],
        "description": snap_details['description'],
        "license": snap_details['license'],
        "icon_url": icon_urls[0] if icon_urls else None,
        "publisher_name": snap_details['publisher']['display-name'],
        "screenshot_urls": screenshot_urls,
        "contact": snap_details['contact'],
        "private": snap_details['private'],
        "website": snap_details['website'] or '',
        "public_metrics_enabled": snap_details['public_metrics_enabled'],
        "public_metrics_blacklist": snap_details['public_metrics_blacklist'],
        "is_on_stable": is_on_stable,
    }

    return flask.render_template('publisher/listing.html', **context)
Esempio n. 20
0
def post_disconnect_repo(snap_name):
    try:
        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)

    lp_snap = launchpad.get_snap_by_store_name(snap_name)
    launchpad.delete_snap(details["snap_name"])

    # Try to remove the GitHub webhook if possible
    if flask.session.get("github_auth_secret"):
        github = GitHub(flask.session.get("github_auth_secret"))

        try:
            gh_owner, gh_repo = lp_snap["git_repository_url"][19:].split("/")

            old_hook = github.get_hook_by_url(
                gh_owner,
                gh_repo,
                f"https://snapcraft.io/{snap_name}/webhook/notify",
            )

            if old_hook:
                github.remove_hook(
                    gh_owner,
                    gh_repo,
                    old_hook["id"],
                )
        except HTTPError:
            pass

    return flask.redirect(
        flask.url_for(".get_snap_builds", snap_name=snap_name))
Esempio n. 21
0
def get_publicise(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_errors(api_error)

    if snap_details["private"]:
        return flask.abort(404, "No snap named {}".format(snap_name))

    available_languages = {
        "en": {"title": "English", "text": "Get it from the Snap Store"},
        "de": {"title": "Deutsch", "text": "Installieren vom Snap Store"},
        "es": {"title": "Español", "text": "Instalar desde Snap Store"},
        "fr": {
            "title": "Français",
            "text": "Installer à partir du Snap Store",
        },
        "jp": {"title": "日本語", "text": "Snap Store から入手ください"},
        "ru": {"title": "русский язык", "text": "Загрузите из Snap Store"},
        "tw": {"title": "中文(台灣)", "text": "安裝軟體敬請移駕 Snap Store"},
    }

    context = {
        "snap_name": snap_details["snap_name"],
        "snap_title": snap_details["title"],
        "snap_id": snap_details["snap_id"],
        "available": available_languages,
        "download_version": "v1.2",
    }

    return flask.render_template(
        "publisher/publicise/store_buttons.html", **context
    )
Esempio n. 22
0
def get_snap_builds_json(snap_name):
    try:
        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)

    context = {"snap_builds": []}

    start = flask.request.args.get("start", 0, type=int)
    size = flask.request.args.get("size", 15, type=int)
    build_slice = slice(start, start + size)

    # Get built snap in launchpad with this store name
    lp_snap = launchpad.get_snap_by_store_name(details["snap_name"])

    if lp_snap:
        context.update(get_builds(lp_snap, build_slice))

    return flask.jsonify(context)
Esempio n. 23
0
def publisher_snap_metrics(snap_name):
    """
    A view to display the snap metrics page for specific snaps.

    This queries the snapcraft API (api.snapcraft.io) and passes
    some of the data through to the publisher/metrics.html template,
    with appropriate sanitation.
    """
    try:
        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_errors(api_error)

    metric_requested = logic.extract_metrics_period(
        flask.request.args.get("period", default="30d", type=str)
    )

    installed_base_metric = logic.verify_base_metrics(
        flask.request.args.get("active-devices", default="version", type=str)
    )

    installed_base = logic.get_installed_based_metric(installed_base_metric)
    metrics_query_json = metrics_helper.build_metrics_json(
        snap_id=details["snap_id"],
        installed_base=installed_base,
        metric_period=metric_requested["int"],
        metric_bucket=metric_requested["bucket"],
    )

    try:
        metrics_response = api.get_publisher_metrics(
            flask.session, json=metrics_query_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:
            return _handle_error_list(api_response_error_list.errors)
    except ApiError as api_error:
        return _handle_errors(api_error)

    active_metrics = metrics_helper.find_metric(
        metrics_response["metrics"], installed_base
    )
    active_devices = metrics.ActiveDevices(
        name=active_metrics["metric_name"],
        series=active_metrics["series"],
        buckets=active_metrics["buckets"],
        status=active_metrics["status"],
    )

    latest_active = 0
    if active_devices:
        latest_active = active_devices.get_number_latest_active_devices()

    country_metric = metrics_helper.find_metric(
        metrics_response["metrics"], "weekly_installed_base_by_country"
    )
    country_devices = metrics.CountryDevices(
        name=country_metric["metric_name"],
        series=country_metric["series"],
        buckets=country_metric["buckets"],
        status=country_metric["status"],
        private=True,
    )

    territories_total = 0
    if country_devices:
        territories_total = country_devices.get_number_territories()

    nodata = not any([country_devices, active_devices])

    context = {
        # Data direct from details API
        "snap_name": snap_name,
        "snap_title": details["title"],
        "metric_period": metric_requested["period"],
        "active_device_metric": installed_base_metric,
        # Metrics data
        "nodata": nodata,
        "latest_active_devices": latest_active,
        "active_devices": dict(active_devices),
        "territories_total": territories_total,
        "territories": country_devices.country_data,
        # Context info
        "is_linux": "Linux" in flask.request.headers["User-Agent"],
    }

    return flask.render_template("publisher/metrics.html", **context)
Esempio n. 24
0
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)
Esempio n. 25
0
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))
Esempio n. 26
0
def post_snap_builds(snap_name):
    try:
        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)

    redirect_url = flask.url_for(".get_snap_builds", snap_name=snap_name)

    # Get built snap in launchpad with this store name
    github = GitHub(flask.session.get("github_auth_secret"))
    owner, repo = flask.request.form.get("github_repository").split("/")

    if not github.check_permissions_over_repo(owner, repo):
        flask.flash(
            "Your GitHub account doesn't have permissions in the repository",
            "negative",
        )
        return flask.redirect(redirect_url)

    repo_validation = validate_repo(flask.session.get("github_auth_secret"),
                                    snap_name, owner, repo)

    if not repo_validation["success"]:
        flask.flash(repo_validation["error"]["message"], "negative")
        return flask.redirect(redirect_url)

    lp_snap = launchpad.get_snap_by_store_name(details["snap_name"])
    git_url = f"https://github.com/{owner}/{repo}"

    if not lp_snap:
        lp_snap_name = md5(git_url.encode("UTF-8")).hexdigest()

        try:
            repo_exist = bool(launchpad.get_snap(lp_snap_name))
        except HTTPError as e:
            if e.response.status_code == 404:
                repo_exist = False
            else:
                raise e

        if repo_exist:
            error_msg = (
                "The specified GitHub repository is being used by another snap"
            )
            flask.flash(error_msg, "negative")
            return flask.redirect(redirect_url)

        macaroon = api.get_package_upload_macaroon(session=flask.session,
                                                   snap_name=snap_name,
                                                   channels=["edge"
                                                             ])["macaroon"]

        launchpad.create_snap(snap_name, git_url, macaroon)

        # Create webhook in the repo, it should also trigger the first build
        github.create_hook(owner, repo,
                           f"https://snapcraft.io/{snap_name}/webhook/notify")

        flask.flash("The GitHub repository was linked correctly.", "positive")
    elif lp_snap["git_repository_url"] != git_url:
        # In the future, create a new record, delete the old one
        raise AttributeError(
            f"Snap {snap_name} already has a build repository associated")

    return flask.redirect(redirect_url)
Esempio n. 27
0
def publisher_snap_metrics(snap_name):
    """
    A view to display the snap metrics page for specific snaps.

    This queries the snapcraft API (api.snapcraft.io) and passes
    some of the data through to the publisher/metrics.html template,
    with appropriate sanitation.
    """
    try:
        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)

    metric_requested = logic.extract_metrics_period(
        flask.request.args.get("period", default="30d", type=str))

    installed_base_metric = logic.verify_base_metrics(
        flask.request.args.get("active-devices", default="version", type=str))

    installed_base = logic.get_installed_based_metric(installed_base_metric)
    metrics_query_json = metrics_helper.build_metrics_json(
        snap_id=details["snap_id"],
        installed_base=installed_base,
        metric_period=metric_requested["int"],
        metric_bucket=metric_requested["bucket"],
    )

    try:
        metrics_response = api.get_publisher_metrics(flask.session,
                                                     json=metrics_query_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:
            return _handle_error_list(api_response_error_list.errors)
    except ApiError as api_error:
        return _handle_error(api_error)

    active_metrics = metrics_helper.find_metric(metrics_response["metrics"],
                                                installed_base)
    active_devices = metrics.ActiveDevices(
        name=active_metrics["metric_name"],
        series=active_metrics["series"],
        buckets=active_metrics["buckets"],
        status=active_metrics["status"],
    )

    latest_active = 0
    if active_devices:
        latest_active = active_devices.get_number_latest_active_devices()

    country_metric = metrics_helper.find_metric(
        metrics_response["metrics"], "weekly_installed_base_by_country")
    country_devices = metrics.CountryDevices(
        name=country_metric["metric_name"],
        series=country_metric["series"],
        buckets=country_metric["buckets"],
        status=country_metric["status"],
        private=True,
    )

    territories_total = 0
    if country_devices:
        territories_total = country_devices.get_number_territories()

    nodata = not any([country_devices, active_devices])

    # until default tracks are supported by the API we special case node
    # to use 10, rather then latest
    default_track = helpers.get_default_track(snap_name)

    annotations = {"name": "annotations", "series": [], "buckets": []}

    for category in details["categories"]["items"]:
        date = category["since"].split("T")[0]
        new_date = logic.convert_date(category["since"])

        if date not in annotations["buckets"]:
            annotations["buckets"].append(date)

        index_of_date = annotations["buckets"].index(date)

        single_series = {
            "values": [0] * (len(annotations)),
            "name": category["name"],
            "display_name": category["name"].capitalize().replace("-", " "),
            "display_date": new_date,
            "date": date,
        }

        single_series["values"][index_of_date] = 1

        annotations["series"].append(single_series)

    annotations["series"] = sorted(annotations["series"],
                                   key=lambda k: k["date"])

    context = {
        # Data direct from details API
        "snap_name": snap_name,
        "snap_title": details["title"],
        "metric_period": metric_requested["period"],
        "active_device_metric": installed_base_metric,
        "default_track": default_track,
        "private": details["private"],
        # Metrics data
        "nodata": nodata,
        "latest_active_devices": latest_active,
        "active_devices": dict(active_devices),
        "territories_total": territories_total,
        "territories": country_devices.country_data,
        "active_devices_annotations": annotations,
        # Context info
        "is_linux": "Linux" in flask.request.headers["User-Agent"],
    }

    return flask.render_template("publisher/metrics.html", **context)
Esempio n. 28
0
def post_settings(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 = []

        body_json = logic.filter_changes_data(changes)

        if body_json:
            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)

            countries = []
            for country in pycountry.countries:
                countries.append({
                    "key": country.alpha_2,
                    "name": country.name
                })

            if "whitelist_country_codes" in snap_details:
                whitelist_country_codes = (
                    snap_details["whitelist_country_codes"] if
                    len(snap_details["whitelist_country_codes"]) > 0 else [])
            else:
                whitelist_country_codes = []

            if "blacklist_country_codes" in snap_details:
                blacklist_country_codes = (
                    snap_details["blacklist_country_codes"] if
                    len(snap_details["blacklist_country_codes"]) > 0 else [])
            else:
                blacklist_country_codes = []

            context = {
                # read-only values from details API
                "snap_name": snap_details["snap_name"],
                "snap_title": snap_details["title"],
                "snap_id": snap_details["snap_id"],
                "private": snap_details["private"],
                "unlisted": snap_details["unlisted"],
                "countries": countries,
                "whitelist_country_codes": whitelist_country_codes,
                "blacklist_country_codes": blacklist_country_codes,
                "price": snap_details["price"],
                "store": snap_details["store"],
                "keywords": snap_details["keywords"],
                "status": snap_details["status"],
                # errors
                "error_list": error_list,
                "field_errors": field_errors,
                "other_errors": other_errors,
            }

            return flask.render_template("publisher/settings.html", **context)

        flask.flash("Changes applied successfully.", "positive")
    else:
        flask.flash("No changes to save.", "information")

    return flask.redirect(flask.url_for(".get_settings", snap_name=snap_name))
Esempio n. 29
0
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_errors(api_error)

            images_json, images_files = logic.build_changed_images(
                changes["images"],
                current_screenshots,
                flask.request.files.get("icon"),
                flask.request.files.getlist("screenshots"),
            )

            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_errors(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_errors(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_errors(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 = [
                m["url"] for m in snap_details["media"] if m["type"] == "icon"
            ]
            screenshot_urls = [
                m["url"]
                for m in snap_details["media"]
                if m["type"] == "screenshot"
            ]

            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 ApiError:
                categories_results = []

            categories = get_categories(categories_results)

            snap_categories = logic.replace_reserved_categories_key(
                snap_details["categories"]
            )

            snap_categories = logic.filter_categories(snap_categories)

            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,
                "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,
            }

            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)
    )
Esempio n. 30
0
def publisher_snap_metrics(snap_name):
    """
    A view to display the snap metrics page for specific snaps.

    This queries the snapcraft API (api.snapcraft.io) and passes
    some of the data through to the publisher/metrics.html template,
    with appropriate sanitation.
    """
    try:
        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_errors(api_error)

    metric_requested = logic.extract_metrics_period(
        flask.request.args.get("period", default="30d", type=str)
    )

    installed_base_metric = logic.verify_base_metrics(
        flask.request.args.get("active-devices", default="version", type=str)
    )

    installed_base = logic.get_installed_based_metric(installed_base_metric)
    metrics_query_json = metrics_helper.build_metrics_json(
        snap_id=details["snap_id"],
        installed_base=installed_base,
        metric_period=metric_requested["int"],
        metric_bucket=metric_requested["bucket"],
    )

    try:
        metrics_response = api.get_publisher_metrics(
            flask.session, json=metrics_query_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:
            return _handle_error_list(api_response_error_list.errors)
    except ApiError as api_error:
        return _handle_errors(api_error)

    active_metrics = metrics_helper.find_metric(
        metrics_response["metrics"], installed_base
    )
    active_devices = metrics.ActiveDevices(
        name=active_metrics["metric_name"],
        series=active_metrics["series"],
        buckets=active_metrics["buckets"],
        status=active_metrics["status"],
    )

    latest_active = 0
    if active_devices:
        latest_active = active_devices.get_number_latest_active_devices()

    country_metric = metrics_helper.find_metric(
        metrics_response["metrics"], "weekly_installed_base_by_country"
    )
    country_devices = metrics.CountryDevices(
        name=country_metric["metric_name"],
        series=country_metric["series"],
        buckets=country_metric["buckets"],
        status=country_metric["status"],
        private=True,
    )

    territories_total = 0
    if country_devices:
        territories_total = country_devices.get_number_territories()

    nodata = not any([country_devices, active_devices])

    context = {
        # Data direct from details API
        "snap_name": snap_name,
        "snap_title": details["title"],
        "metric_period": metric_requested["period"],
        "active_device_metric": installed_base_metric,
        "private": details["private"],
        # Metrics data
        "nodata": nodata,
        "latest_active_devices": latest_active,
        "active_devices": dict(active_devices),
        "territories_total": territories_total,
        "territories": country_devices.country_data,
        # Context info
        "is_linux": "Linux" in flask.request.headers["User-Agent"],
    }

    return flask.render_template("publisher/metrics.html", **context)
Esempio n. 31
0
def publisher_snap_metrics(snap_name):
    """
    A view to display the snap metrics page for specific snaps.

    This queries the snapcraft API (api.snapcraft.io) and passes
    some of the data through to the publisher/metrics.html template,
    with appropriate sanitation.
    """
    try:
        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_errors(api_error)

    metric_requested = logic.extract_metrics_period(
        flask.request.args.get("period", default="30d", type=str)
    )

    installed_base_metric = logic.verify_base_metrics(
        flask.request.args.get("active-devices", default="version", type=str)
    )

    installed_base = logic.get_installed_based_metric(installed_base_metric)
    metrics_query_json = metrics_helper.build_metrics_json(
        snap_id=details["snap_id"],
        installed_base=installed_base,
        metric_period=metric_requested["int"],
        metric_bucket=metric_requested["bucket"],
    )

    try:
        metrics_response = api.get_publisher_metrics(
            flask.session, json=metrics_query_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:
            return _handle_error_list(api_response_error_list.errors)
    except ApiError as api_error:
        return _handle_errors(api_error)

    active_metrics = metrics_helper.find_metric(
        metrics_response["metrics"], installed_base
    )
    active_devices = metrics.ActiveDevices(
        name=active_metrics["metric_name"],
        series=active_metrics["series"],
        buckets=active_metrics["buckets"],
        status=active_metrics["status"],
    )

    latest_active = 0
    if active_devices:
        latest_active = active_devices.get_number_latest_active_devices()

    country_metric = metrics_helper.find_metric(
        metrics_response["metrics"], "weekly_installed_base_by_country"
    )
    country_devices = metrics.CountryDevices(
        name=country_metric["metric_name"],
        series=country_metric["series"],
        buckets=country_metric["buckets"],
        status=country_metric["status"],
        private=True,
    )

    territories_total = 0
    if country_devices:
        territories_total = country_devices.get_number_territories()

    nodata = not any([country_devices, active_devices])

    # until default tracks are supported by the API we special case node
    # to use 10, rather then latest
    default_track = helpers.get_default_track(snap_name)

    annotations = {"name": "annotations", "series": [], "buckets": []}

    for category in details["categories"]["items"]:
        date = category["since"].split("T")[0]
        new_date = logic.convert_date(category["since"])

        if date not in annotations["buckets"]:
            annotations["buckets"].append(date)

        index_of_date = annotations["buckets"].index(date)

        single_series = {
            "values": [0] * (len(annotations)),
            "name": category["name"],
            "display_name": category["name"].capitalize().replace("-", " "),
            "display_date": new_date,
            "date": date,
        }

        single_series["values"][index_of_date] = 1

        annotations["series"].append(single_series)

    annotations["series"] = sorted(
        annotations["series"], key=lambda k: k["date"]
    )

    context = {
        # Data direct from details API
        "snap_name": snap_name,
        "snap_title": details["title"],
        "metric_period": metric_requested["period"],
        "active_device_metric": installed_base_metric,
        "default_track": default_track,
        "private": details["private"],
        # Metrics data
        "nodata": nodata,
        "latest_active_devices": latest_active,
        "active_devices": dict(active_devices),
        "territories_total": territories_total,
        "territories": country_devices.country_data,
        "active_devices_annotations": annotations,
        # Context info
        "is_linux": "Linux" in flask.request.headers["User-Agent"],
    }

    return flask.render_template("publisher/metrics.html", **context)
Esempio n. 32
0
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_errors(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 = [
        m["url"] for m in snap_details["media"] if m["type"] == "icon"
    ]
    screenshot_urls = [
        m["url"] for m in snap_details["media"] if m["type"] == "screenshot"
    ]

    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 ApiError:
        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)

    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,
        "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,
    }

    return flask.render_template("publisher/listing.html", **context)
Esempio n. 33
0
def post_snap_builds(snap_name):
    try:
        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)

    # Don't allow changes from Admins that are no contributors
    account_snaps = api.get_account_snaps(flask.session)

    if snap_name not in account_snaps:
        flask.flash("You do not have permissions to modify this Snap",
                    "negative")
        return flask.redirect(
            flask.url_for(".get_snap_builds", snap_name=snap_name))

    redirect_url = flask.url_for(".get_snap_builds", snap_name=snap_name)

    # Get built snap in launchpad with this store name
    github = GitHub(flask.session.get("github_auth_secret"))
    owner, repo = flask.request.form.get("github_repository").split("/")

    if not github.check_permissions_over_repo(owner, repo):
        flask.flash(
            "Your GitHub account doesn't have permissions in the repository",
            "negative",
        )
        return flask.redirect(redirect_url)

    repo_validation = validate_repo(flask.session.get("github_auth_secret"),
                                    snap_name, owner, repo)

    if not repo_validation["success"]:
        flask.flash(repo_validation["error"]["message"], "negative")
        return flask.redirect(redirect_url)

    lp_snap = launchpad.get_snap_by_store_name(details["snap_name"])
    git_url = f"https://github.com/{owner}/{repo}"

    if not lp_snap:
        lp_snap_name = md5(git_url.encode("UTF-8")).hexdigest()

        try:
            repo_exist = launchpad.get_snap(lp_snap_name)
        except HTTPError as e:
            if e.response.status_code == 404:
                repo_exist = False
            else:
                raise e

        if repo_exist:
            # The user registered the repo in BSI but didn't register a name
            # We can remove it and continue with the normal process
            if not repo_exist["store_name"]:
                # This conditional should be removed when issue 2657 is solved
                launchpad._request(path=repo_exist["self_link"][32:],
                                   method="DELETE")
            else:
                flask.flash(
                    "The specified repository is being used by another snap:"
                    f" {repo_exist['store_name']}",
                    "negative",
                )
                return flask.redirect(redirect_url)

        macaroon = api.get_package_upload_macaroon(session=flask.session,
                                                   snap_name=snap_name,
                                                   channels=["edge"
                                                             ])["macaroon"]

        launchpad.create_snap(snap_name, git_url, macaroon)

        flask.flash("The GitHub repository was linked correctly.", "positive")

        # Create webhook in the repo, it should also trigger the first build
        github_hook_url = f"https://snapcraft.io/{snap_name}/webhook/notify"

        try:
            hook = github.get_hook_by_url(owner, repo, github_hook_url)

            # We create the webhook if doesn't exist already in this repo
            if not hook:
                github.create_hook(owner, repo, github_hook_url)
        except HTTPError:
            flask.flash(
                "The GitHub Webhook could not be created. "
                "Please trigger a new build manually.",
                "caution",
            )

    elif lp_snap["git_repository_url"] != git_url:
        # In the future, create a new record, delete the old one
        raise AttributeError(
            f"Snap {snap_name} already has a build repository associated")

    return flask.redirect(redirect_url)
Esempio n. 34
0
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_errors(api_error)

            images_json, images_files = logic.build_changed_images(
                changes['images'], current_screenshots,
                flask.request.files.get('icon'),
                flask.request.files.getlist('screenshots'))

            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_errors(api_error)

        body_json = logic.filter_changes_data(changes)

        if body_json:
            if 'public_metrics_blacklist' in body_json:
                converted_metrics = logic.convert_metrics_blacklist(
                    body_json['public_metrics_blacklist'])
                body_json['public_metrics_blacklist'] = converted_metrics

            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_errors(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_errors(api_error)

            details_metrics_enabled = snap_details['public_metrics_enabled']
            details_blacklist = snap_details['public_metrics_blacklist']

            field_errors, other_errors = logic.invalid_field_errors(error_list)

            is_on_stable = logic.is_snap_on_stable(
                snap_details['channel_maps_list'])

            # Filter icon & screenshot urls from the media set.
            icon_urls = [
                m['url'] for m in snap_details['media'] if m['type'] == 'icon'
            ]
            screenshot_urls = [
                m['url'] for m in snap_details['media']
                if m['type'] == 'screenshot'
            ]

            context = {
                # read-only values from details API
                "snap_id":
                snap_details['snap_id'],
                "snap_name":
                snap_details['snap_name'],
                "license":
                snap_details['license'],
                "icon_url":
                icon_urls[0] if icon_urls else None,
                "publisher_name":
                snap_details['publisher']['display-name'],
                "screenshot_urls":
                screenshot_urls,
                "public_metrics_enabled":
                details_metrics_enabled,
                "public_metrics_blacklist":
                details_blacklist,
                "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 ''),
                "is_on_stable":
                is_on_stable,
                # errors
                "error_list":
                error_list,
                "field_errors":
                field_errors,
                "other_errors":
                other_errors
            }

            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))
Esempio n. 35
0
def post_settings(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 = []

        body_json = logic.filter_changes_data(changes)

        if body_json:
            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_errors(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_errors(api_error)

            field_errors, other_errors = logic.invalid_field_errors(error_list)

            countries = []
            for country in pycountry.countries:
                countries.append(
                    {"key": country.alpha_2, "name": country.name}
                )

            if "whitelist_country_codes" in snap_details:
                whitelist_country_codes = (
                    snap_details["whitelist_country_codes"]
                    if len(snap_details["whitelist_country_codes"]) > 0
                    else []
                )
            else:
                whitelist_country_codes = []

            if "blacklist_country_codes" in snap_details:
                blacklist_country_codes = (
                    snap_details["blacklist_country_codes"]
                    if len(snap_details["blacklist_country_codes"]) > 0
                    else []
                )
            else:
                blacklist_country_codes = []

            context = {
                # read-only values from details API
                "snap_name": snap_details["snap_name"],
                "snap_title": snap_details["title"],
                "snap_id": snap_details["snap_id"],
                "private": snap_details["private"],
                "unlisted": snap_details["unlisted"],
                "countries": countries,
                "whitelist_country_codes": whitelist_country_codes,
                "blacklist_country_codes": blacklist_country_codes,
                "price": snap_details["price"],
                "store": snap_details["store"],
                "keywords": snap_details["keywords"],
                "status": snap_details["status"],
                # errors
                "error_list": error_list,
                "field_errors": field_errors,
                "other_errors": other_errors,
            }

            return flask.render_template("publisher/settings.html", **context)

        flask.flash("Changes applied successfully.", "positive")
    else:
        flask.flash("No changes to save.", "information")

    return flask.redirect(flask.url_for(".get_settings", snap_name=snap_name))
Esempio n. 36
0
def publisher_snap_metrics(snap_name):
    """
    A view to display the snap metrics page for specific snaps.

    This queries the snapcraft API (api.snapcraft.io) and passes
    some of the data through to the publisher/metrics.html template,
    with appropriate sanitation.
    """
    try:
        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_errors(api_error)

    metric_requested = logic.extract_metrics_period(
        flask.request.args.get('period', default='30d', type=str))

    installed_base_metric = logic.verify_base_metrics(
        flask.request.args.get('active-devices', default='version', type=str))

    installed_base = logic.get_installed_based_metric(installed_base_metric)
    metrics_query_json = metrics_helper.build_metrics_json(
        snap_id=details['snap_id'],
        installed_base=installed_base,
        metric_period=metric_requested['int'],
        metric_bucket=metric_requested['bucket'])

    try:
        metrics_response = api.get_publisher_metrics(flask.session,
                                                     json=metrics_query_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:
            return _handle_error_list(api_response_error_list.errors)
    except ApiError as api_error:
        return _handle_errors(api_error)

    active_metrics = metrics_helper.find_metric(metrics_response['metrics'],
                                                installed_base)
    active_devices = metrics.ActiveDevices(name=active_metrics['metric_name'],
                                           series=active_metrics['series'],
                                           buckets=active_metrics['buckets'],
                                           status=active_metrics['status'])

    latest_active = 0
    if active_devices:
        latest_active = active_devices.get_number_latest_active_devices()

    country_metric = metrics_helper.find_metric(
        metrics_response['metrics'], "weekly_installed_base_by_country")
    country_devices = metrics.CountryDevices(
        name=country_metric['metric_name'],
        series=country_metric['series'],
        buckets=country_metric['buckets'],
        status=country_metric['status'],
        private=True)

    territories_total = 0
    if country_devices:
        territories_total = country_devices.get_number_territories()

    nodata = not any([country_devices, active_devices])

    context = {
        # Data direct from details API
        'snap_name': snap_name,
        'snap_title': details['title'],
        'metric_period': metric_requested['period'],
        'active_device_metric': installed_base_metric,

        # Metrics data
        'nodata': nodata,
        'latest_active_devices': latest_active,
        'active_devices': dict(active_devices),
        'territories_total': territories_total,
        'territories': country_devices.country_data,

        # Context info
        'is_linux': 'Linux' in flask.request.headers['User-Agent']
    }

    return flask.render_template('publisher/metrics.html', **context)