예제 #1
0
def paginate_releases(request, domain, app_id):
    limit = request.GET.get('limit')
    only_show_released = json.loads(
        request.GET.get('only_show_released', 'false'))
    build_comment = request.GET.get('build_comment')
    page = int(request.GET.get('page', 1))
    page = max(page, 1)
    try:
        limit = int(limit)
    except (TypeError, ValueError):
        limit = 10

    timezone = get_timezone_for_user(request.couch_user, domain)

    app_es = (AppES().start((page - 1) * limit).size(limit).sort(
        'version', desc=True).domain(domain).is_build().app_id(app_id))
    if only_show_released:
        app_es = app_es.is_released()
    if build_comment:
        app_es = app_es.build_comment(build_comment)
    results = app_es.exclude_source().run()
    app_ids = results.doc_ids
    apps = get_docs(Application.get_db(), app_ids)
    for app in apps:
        app.pop('translations')
    saved_apps = [
        SavedAppBuild.wrap(
            app, scrap_old_conventions=False).to_saved_build_json(timezone)
        for app in apps
    ]

    j2me_enabled_configs = CommCareBuildConfig.j2me_enabled_config_labels()
    for app in saved_apps:
        app['include_media'] = app['doc_type'] != 'RemoteApp'
        app['j2me_enabled'] = app['menu_item_label'] in j2me_enabled_configs
        app['target_commcare_flavor'] = (
            SavedAppBuild.get(app['_id']).target_commcare_flavor
            if toggles.TARGET_COMMCARE_FLAVOR.enabled(domain) else 'none')

    if toggles.APPLICATION_ERROR_REPORT.enabled(request.couch_user.username):
        versions = [app['version'] for app in saved_apps]
        num_errors_dict = _get_error_counts(domain, app_id, versions)
        for app in saved_apps:
            app['num_errors'] = num_errors_dict.get(app['version'], 0)

    total_apps = results.total
    num_pages = int(ceil(total_apps / limit))

    return json_response({
        'apps': saved_apps,
        'pagination': {
            'total': total_apps,
            'num_pages': num_pages,
            'current_page': page,
        }
    })
예제 #2
0
def paginate_releases(request, domain, app_id):
    limit = request.GET.get('limit')
    only_show_released = json.loads(
        request.GET.get('only_show_released', 'false'))
    try:
        limit = int(limit)
    except (TypeError, ValueError):
        limit = 10
    start_build_param = request.GET.get('start_build')
    if start_build_param and json.loads(start_build_param):
        start_build = json.loads(start_build_param)
        assert isinstance(start_build, int)
    else:
        start_build = {}
    timezone = get_timezone_for_user(request.couch_user, domain)

    saved_apps = []
    batch = [None]
    while len(saved_apps) < limit and len(batch):
        batch = Application.get_db().view(
            'app_manager/saved_app',
            startkey=[domain, app_id, start_build],
            endkey=[domain, app_id],
            descending=True,
            limit=limit,
            wrapper=lambda x: SavedAppBuild.wrap(x['value']).
            to_saved_build_json(timezone),
        ).all()
        if len(batch):
            start_build = batch[-1]['version'] - 1
        saved_apps = saved_apps + [
            app
            for app in batch if not only_show_released or app['is_released']
        ]
    saved_apps = saved_apps[:limit]

    j2me_enabled_configs = CommCareBuildConfig.j2me_enabled_config_labels()
    for app in saved_apps:
        app['include_media'] = app['doc_type'] != 'RemoteApp'
        app['j2me_enabled'] = app['menu_item_label'] in j2me_enabled_configs
        app['target_commcare_flavor'] = (
            SavedAppBuild.get(app['_id']).target_commcare_flavor
            if toggles.TARGET_COMMCARE_FLAVOR.enabled(domain) else 'none')

    if toggles.APPLICATION_ERROR_REPORT.enabled(request.couch_user.username):
        versions = [app['version'] for app in saved_apps]
        num_errors_dict = _get_error_counts(domain, app_id, versions)
        for app in saved_apps:
            app['num_errors'] = num_errors_dict.get(app['version'], 0)

    return json_response(saved_apps)
예제 #3
0
def save_copy(request, domain, app_id):
    """
    Saves a copy of the app to a new doc.
    See ApplicationBase.save_copy

    """
    track_built_app_on_hubspot.delay(request.couch_user)
    comment = request.POST.get('comment')
    app = get_app(domain, app_id)
    try:
        errors = app.validate_app()
    except ModuleIdMissingException:
        # For apps (mainly Exchange apps) that lost unique_id attributes on Module
        app.ensure_module_unique_ids(should_save=True)
        errors = app.validate_app()

    if not errors:
        try:
            user_id = request.couch_user.get_id
            timer = datadog_bucket_timer('commcare.app_build.new_release',
                                         tags=[],
                                         timing_buckets=(1, 10, 30, 60, 120,
                                                         240))
            with timer:
                copy = make_app_build(app, comment, user_id)
            CouchUser.get(user_id).set_has_built_app()
        except BuildConflictException:
            return JsonResponse(
                {
                    'error':
                    _("There is already a version build in progress. Please wait."
                      )
                },
                status=400)
        finally:
            # To make a RemoteApp always available for building
            if app.is_remote_app():
                app.save(increment_version=True)

        _track_build_for_app_preview(domain, request.couch_user, app_id,
                                     'User created a build')

    else:
        copy = None
    copy = copy and SavedAppBuild.wrap(copy.to_json()).releases_list_json(
        get_timezone_for_user(request.couch_user, domain))
    lang, langs = get_langs(request, app)

    return json_response({
        "saved_app":
        copy,
        "error_html":
        render_to_string(
            "app_manager/partials/build_errors.html", {
                'app': get_app(domain, app_id),
                'build_errors': errors,
                'domain': domain,
                'langs': langs,
            }),
    })
예제 #4
0
def paginate_releases(request, domain, app_id):
    limit = request.GET.get('limit')
    try:
        limit = int(limit)
    except (TypeError, ValueError):
        limit = 10
    start_build_param = request.GET.get('start_build')
    if start_build_param and json.loads(start_build_param):
        start_build = json.loads(start_build_param)
        assert isinstance(start_build, int)
    else:
        start_build = {}
    timezone = get_timezone_for_user(request.couch_user, domain)
    saved_apps = Application.get_db().view(
        'app_manager/saved_app',
        startkey=[domain, app_id, start_build],
        endkey=[domain, app_id],
        descending=True,
        limit=limit,
        wrapper=lambda x: SavedAppBuild.wrap(x['value']).to_saved_build_json(
            timezone),
    ).all()
    j2me_enabled_configs = CommCareBuildConfig.j2me_enabled_config_labels()
    for app in saved_apps:
        app['include_media'] = app['doc_type'] != 'RemoteApp'
        app['j2me_enabled'] = app['menu_item_label'] in j2me_enabled_configs

    if toggles.APPLICATION_ERROR_REPORT.enabled(request.couch_user.username):
        versions = [app['version'] for app in saved_apps]
        num_errors_dict = _get_error_counts(domain, app_id, versions)
        for app in saved_apps:
            app['num_errors'] = num_errors_dict.get(app['version'], 0)

    return json_response(saved_apps)
예제 #5
0
def save_copy(request, domain, app_id):
    """
    Saves a copy of the app to a new doc.
    See VersionedDoc.save_copy

    """
    track_built_app_on_hubspot.delay(request.couch_user)
    comment = request.POST.get('comment')
    app = get_app(domain, app_id)
    try:
        errors = app.validate_app()
    except ModuleIdMissingException:
        # For apps (mainly Exchange apps) that lost unique_id attributes on Module
        app.ensure_module_unique_ids(should_save=True)
        errors = app.validate_app()

    if not errors:
        try:
            copy = app.make_build(
                comment=comment,
                user_id=request.couch_user.get_id,
                previous_version=app.get_latest_app(released_only=False))
            copy.save(increment_version=False)
        finally:
            # To make a RemoteApp always available for building
            if app.is_remote_app():
                app.save(increment_version=True)

        _track_build_for_app_preview(domain, request.couch_user, app_id,
                                     'User created a build')

    else:
        copy = None
    copy = copy and SavedAppBuild.wrap(copy.to_json()).to_saved_build_json(
        get_timezone_for_user(request.couch_user, domain))
    lang, langs = get_langs(request, app)
    if copy:
        # Set if build is supported for Java Phones
        j2me_enabled_configs = CommCareBuildConfig.j2me_enabled_config_labels()
        copy['j2me_enabled'] = copy['menu_item_label'] in j2me_enabled_configs

    template = get_app_manager_template(
        request.user,
        "app_manager/v1/partials/build_errors.html",
        "app_manager/v2/partials/build_errors.html",
    )
    return json_response({
        "saved_app":
        copy,
        "error_html":
        render_to_string(
            template, {
                'request': request,
                'app': get_app(domain, app_id),
                'build_errors': errors,
                'domain': domain,
                'langs': langs,
                'lang': lang
            }),
    })
예제 #6
0
def paginate_releases(request, domain, app_id):
    limit = request.GET.get('limit')
    try:
        limit = int(limit)
    except (TypeError, ValueError):
        limit = 10
    start_build_param = request.GET.get('start_build')
    if start_build_param and json.loads(start_build_param):
        start_build = json.loads(start_build_param)
        assert isinstance(start_build, int)
    else:
        start_build = {}
    timezone = get_timezone_for_user(request.couch_user, domain)
    saved_apps = Application.get_db().view('app_manager/saved_app',
        startkey=[domain, app_id, start_build],
        endkey=[domain, app_id],
        descending=True,
        limit=limit,
        wrapper=lambda x: SavedAppBuild.wrap(x['value']).to_saved_build_json(timezone),
    ).all()
    for app in saved_apps:
        app['include_media'] = app['doc_type'] != 'RemoteApp'

    if toggles.APPLICATION_ERROR_REPORT.enabled(request.couch_user.username):
        versions = [app['version'] for app in saved_apps]
        num_errors_dict = _get_error_counts(domain, app_id, versions)
        for app in saved_apps:
            app['num_errors'] = num_errors_dict.get(app['version'], 0)

    return json_response(saved_apps)
예제 #7
0
def paginate_releases(request, domain, app_id):
    limit = request.GET.get("limit")
    try:
        limit = int(limit)
    except (TypeError, ValueError):
        limit = 10
    start_build_param = request.GET.get("start_build")
    if start_build_param and json.loads(start_build_param):
        start_build = json.loads(start_build_param)
        assert isinstance(start_build, int)
    else:
        start_build = {}
    timezone = get_timezone_for_user(request.couch_user, domain)
    saved_apps = (
        Application.get_db()
        .view(
            "app_manager/saved_app",
            startkey=[domain, app_id, start_build],
            endkey=[domain, app_id],
            descending=True,
            limit=limit,
            wrapper=lambda x: SavedAppBuild.wrap(x["value"]).to_saved_build_json(timezone),
        )
        .all()
    )
    for app in saved_apps:
        app["include_media"] = app["doc_type"] != "RemoteApp"
    return json_response(saved_apps)
예제 #8
0
def update_build_comment(request, domain, app_id):
    build_id = request.POST.get("build_id")
    try:
        build = SavedAppBuild.get(build_id)
    except ResourceNotFound:
        raise Http404()
    build.build_comment = request.POST.get("comment")
    build.save()
    return json_response({"status": "success"})
예제 #9
0
def update_build_comment(request, domain, app_id):
    build_id = request.POST.get('build_id')
    try:
        build = SavedAppBuild.get(build_id)
    except ResourceNotFound:
        raise Http404()
    build.build_comment = request.POST.get('comment')
    build.save()
    return json_response({'status': 'success'})
예제 #10
0
def save_copy(request, domain, app_id):
    """
    Saves a copy of the app to a new doc.
    See VersionedDoc.save_copy

    """
    track_built_app_on_hubspot_v2.delay(request.couch_user)
    comment = request.POST.get('comment')
    app = get_app(domain, app_id)
    try:
        errors = app.validate_app()
    except ModuleIdMissingException:
        # For apps (mainly Exchange apps) that lost unique_id attributes on Module
        app.ensure_module_unique_ids(should_save=True)
        errors = app.validate_app()

    if not errors:
        try:
            user_id = request.couch_user.get_id
            timer = datadog_bucket_timer('commcare.app_build.new_release', tags=[],
                                         timing_buckets=(1, 10, 30, 60, 120, 240))
            with timer:
                copy = app.make_build(
                    comment=comment,
                    user_id=user_id,
                )
                copy.save(increment_version=False)
            CouchUser.get(user_id).set_has_built_app()
        finally:
            # To make a RemoteApp always available for building
            if app.is_remote_app():
                app.save(increment_version=True)

        _track_build_for_app_preview(domain, request.couch_user, app_id, 'User created a build')

    else:
        copy = None
    copy = copy and SavedAppBuild.wrap(copy.to_json()).releases_list_json(
        get_timezone_for_user(request.couch_user, domain)
    )
    lang, langs = get_langs(request, app)
    if copy:
        # Set if build is supported for Java Phones
        j2me_enabled_configs = CommCareBuildConfig.j2me_enabled_config_labels()
        copy['j2me_enabled'] = copy['menu_item_label'] in j2me_enabled_configs

    return json_response({
        "saved_app": copy,
        "error_html": render_to_string("app_manager/partials/build_errors.html", {
            'request': request,
            'app': get_app(domain, app_id),
            'build_errors': errors,
            'domain': domain,
            'langs': langs,
            'lang': lang
        }),
    })
예제 #11
0
def update_build_comment(request, domain, app_id):
    build_id = request.POST.get('build_id')
    try:
        build = SavedAppBuild.get(build_id)
    except ResourceNotFound:
        raise Http404()
    build.build_comment = request.POST.get('comment')
    build.save()
    return json_response({'status': 'success'})
예제 #12
0
 def _get_batch(start_build=None, skip=None):
     start_build = {} if start_build is None else start_build
     return Application.get_db().view('app_manager/saved_app',
         startkey=[domain, app_id, start_build],
         endkey=[domain, app_id],
         descending=True,
         limit=limit,
         skip=skip,
         wrapper=lambda x: SavedAppBuild.wrap(x['value'],
                                              scrap_old_conventions=False).releases_list_json(timezone),
     ).all()
예제 #13
0
 def _get_batch(start_build=None, skip=None):
     start_build = {} if start_build is None else start_build
     return Application.get_db().view('app_manager/saved_app',
         startkey=[domain, app_id, start_build],
         endkey=[domain, app_id],
         descending=True,
         limit=limit,
         skip=skip,
         wrapper=lambda x: SavedAppBuild.wrap(x['value'],
                                              scrap_old_conventions=False).releases_list_json(timezone),
     ).all()
예제 #14
0
def save_copy(request, domain, app_id):
    """
    Saves a copy of the app to a new doc.
    """
    track_built_app_on_hubspot.delay(request.couch_user.get_id)
    comment = request.POST.get('comment')
    app = get_app(domain, app_id)
    try:
        user_id = request.couch_user.get_id
        with report_build_time(domain, app._id, 'new_release'):
            copy = make_app_build(app, comment, user_id)
        CouchUser.get(user_id).set_has_built_app()
    except AppValidationError as e:
        lang, langs = get_langs(request, app)
        return JsonResponse({
            "saved_app":
            None,
            "error_html":
            render_to_string(
                "app_manager/partials/build_errors.html", {
                    'app': get_app(domain, app_id),
                    'build_errors': e.errors,
                    'domain': domain,
                    'langs': langs,
                    'toggles': toggles_enabled_for_request(request),
                }),
        })
    except BuildConflictException:
        return JsonResponse(
            {
                'error':
                _("There is already a version build in progress. Please wait.")
            },
            status=400)
    except XFormValidationFailed:
        return JsonResponse({'error': _("Unable to validate forms.")},
                            status=400)
    finally:
        # To make a RemoteApp always available for building
        if app.is_remote_app():
            app.save(increment_version=True)

    _track_build_for_app_preview(domain, request.couch_user, app_id,
                                 'User created a build')

    copy_json = copy and SavedAppBuild.wrap(copy.to_json()).releases_list_json(
        get_timezone_for_user(request.couch_user, domain))

    return JsonResponse({
        "saved_app": copy_json,
        "error_html": "",
    })
예제 #15
0
def save_copy(request, domain, app_id):
    """
    Saves a copy of the app to a new doc.
    See VersionedDoc.save_copy

    """
    track_built_app_on_hubspot.delay(request.couch_user)
    comment = request.POST.get("comment")
    app = get_app(domain, app_id)
    try:
        errors = app.validate_app()
    except ModuleIdMissingException:
        # For apps (mainly Exchange apps) that lost unique_id attributes on Module
        app.ensure_module_unique_ids(should_save=True)
        errors = app.validate_app()

    if not errors:
        try:
            copy = app.make_build(
                comment=comment,
                user_id=request.couch_user.get_id,
                previous_version=app.get_latest_app(released_only=False),
            )
            copy.save(increment_version=False)
        finally:
            # To make a RemoteApp always available for building
            if app.is_remote_app():
                app.save(increment_version=True)
    else:
        copy = None
    copy = copy and SavedAppBuild.wrap(copy.to_json()).to_saved_build_json(
        get_timezone_for_user(request.couch_user, domain)
    )
    lang, langs = get_langs(request, app)
    return json_response(
        {
            "saved_app": copy,
            "error_html": render_to_string(
                "app_manager/partials/build_errors.html",
                {
                    "app": get_app(domain, app_id),
                    "build_errors": errors,
                    "domain": domain,
                    "langs": langs,
                    "lang": lang,
                },
            ),
        }
    )
예제 #16
0
def paginate_releases(request, domain, app_id):
    limit = request.GET.get('limit')
    only_show_released = json.loads(request.GET.get('only_show_released', 'false'))
    query = request.GET.get('query')
    page = int(request.GET.get('page', 1))
    page = max(page, 1)
    try:
        limit = int(limit)
    except (TypeError, ValueError):
        limit = 10
    skip = (page - 1) * limit
    timezone = get_timezone_for_user(request.couch_user, domain)

    def _get_batch(start_build=None, skip=None):
        start_build = {} if start_build is None else start_build
        return Application.get_db().view('app_manager/saved_app',
            startkey=[domain, app_id, start_build],
            endkey=[domain, app_id],
            descending=True,
            limit=limit,
            skip=skip,
            wrapper=lambda x: (
                SavedAppBuild.wrap(x['value'], scrap_old_conventions=False)
                .releases_list_json(timezone)
            ),
        ).all()

    if not bool(only_show_released or query):
        # If user is limiting builds by released status or build comment, it's much
        # harder to be performant with couch. So if they're not doing so, take shortcuts.
        total_apps = len(get_built_app_ids_for_app_id(domain, app_id))
        saved_apps = _get_batch(skip=skip)
    else:
        app_es = (
            AppES()
            .start((page - 1) * limit)
            .size(limit)
            .sort('version', desc=True)
            .domain(domain)
            .is_build()
            .app_id(app_id)
        )
        if only_show_released:
            app_es = app_es.is_released()
        if query:
            app_es = app_es.add_query(build_comment(query), queries.SHOULD)
            try:
                app_es = app_es.add_query(version(int(query)), queries.SHOULD)
            except ValueError:
                pass

        results = app_es.exclude_source().run()
        total_apps = results.total
        app_ids = results.doc_ids
        apps = get_docs(Application.get_db(), app_ids)

        saved_apps = [
            SavedAppBuild.wrap(app, scrap_old_conventions=False).releases_list_json(timezone)
            for app in apps
        ]

    if toggles.APPLICATION_ERROR_REPORT.enabled(request.couch_user.username):
        versions = [app['version'] for app in saved_apps]
        num_errors_dict = _get_error_counts(domain, app_id, versions)
        for app in saved_apps:
            app['num_errors'] = num_errors_dict.get(app['version'], 0)

    num_pages = int(ceil(total_apps / limit))

    return json_response({
        'apps': saved_apps,
        'pagination': {
            'total': total_apps,
            'num_pages': num_pages,
            'current_page': page,
            'more': page * limit < total_apps,  # needed when select2 uses this endpoint
        }
    })
예제 #17
0
def paginate_releases(request, domain, app_id):
    limit = request.GET.get('limit')
    only_show_released = json.loads(
        request.GET.get('only_show_released', 'false'))
    query = request.GET.get('query')
    page = int(request.GET.get('page', 1))
    page = max(page, 1)
    try:
        limit = int(limit)
    except (TypeError, ValueError):
        limit = 10
    skip = (page - 1) * limit
    timezone = get_timezone_for_user(request.couch_user, domain)

    def _get_batch(start_build=None, skip=None):
        start_build = {} if start_build is None else start_build
        return Application.get_db().view(
            'app_manager/saved_app',
            startkey=[domain, app_id, start_build],
            endkey=[domain, app_id],
            descending=True,
            limit=limit,
            skip=skip,
            wrapper=lambda x:
            (SavedAppBuild.wrap(x['value'], scrap_old_conventions=False).
             releases_list_json(timezone)),
        ).all()

    if not bool(only_show_released or query):
        # If user is limiting builds by released status or build comment, it's much
        # harder to be performant with couch. So if they're not doing so, take shortcuts.
        total_apps = len(get_built_app_ids_for_app_id(domain, app_id))
        saved_apps = _get_batch(skip=skip)
    else:
        app_es = (AppES().start((page - 1) * limit).size(limit).sort(
            'version', desc=True).domain(domain).is_build().app_id(app_id))
        if only_show_released:
            app_es = app_es.is_released()
        if query:
            app_es = app_es.add_query(build_comment(query), queries.SHOULD)
            try:
                app_es = app_es.add_query(version(int(query)), queries.SHOULD)
            except ValueError:
                pass

        results = app_es.exclude_source().run()
        total_apps = results.total
        app_ids = results.doc_ids
        apps = get_docs(Application.get_db(), app_ids)

        saved_apps = [
            SavedAppBuild.wrap(
                app, scrap_old_conventions=False).releases_list_json(timezone)
            for app in apps
        ]

    if toggles.APPLICATION_ERROR_REPORT.enabled(request.couch_user.username):
        versions = [app['version'] for app in saved_apps]
        num_errors_dict = _get_error_counts(domain, app_id, versions)
        for app in saved_apps:
            app['num_errors'] = num_errors_dict.get(app['version'], 0)

    num_pages = int(ceil(total_apps / limit))

    return json_response({
        'apps': saved_apps,
        'pagination': {
            'total': total_apps,
            'num_pages': num_pages,
            'current_page': page,
            'more': page * limit <
            total_apps,  # needed when select2 uses this endpoint
        }
    })
예제 #18
0
def paginate_releases(request, domain, app_id):
    limit = request.GET.get('limit')
    only_show_released = json.loads(request.GET.get('only_show_released', 'false'))
    build_comment = request.GET.get('build_comment')
    page = int(request.GET.get('page', 1))
    page = max(page, 1)
    try:
        limit = int(limit)
    except (TypeError, ValueError):
        limit = 10
    skip = (page - 1) * limit
    timezone = get_timezone_for_user(request.couch_user, domain)

    def _get_batch(start_build=None, skip=None):
        start_build = {} if start_build is None else start_build
        return Application.get_db().view('app_manager/saved_app',
            startkey=[domain, app_id, start_build],
            endkey=[domain, app_id],
            descending=True,
            limit=limit,
            skip=skip,
            wrapper=lambda x: SavedAppBuild.wrap(x['value'],
                                                 scrap_old_conventions=False).releases_list_json(timezone),
        ).all()

    if not bool(only_show_released or build_comment):
        # If user is limiting builds by released status or build comment, it's much
        # harder to be performant with couch. So if they're not doing so, take shortcuts.
        total_apps = len(get_built_app_ids_for_app_id(domain, app_id))
        saved_apps = _get_batch(skip=skip)
    else:
        app_es = (
            AppES()
            .start((page - 1) * limit)
            .size(limit)
            .sort('version', desc=True)
            .domain(domain)
            .is_build()
            .app_id(app_id)
        )
        if only_show_released:
            app_es = app_es.is_released()
        if build_comment:
            app_es = app_es.build_comment(build_comment)
        results = app_es.exclude_source().run()
        app_ids = results.doc_ids
        apps = get_docs(Application.get_db(), app_ids)
        saved_apps = [
            SavedAppBuild.wrap(app, scrap_old_conventions=False).releases_list_json(timezone)
            for app in apps
        ]
        total_apps = results.total

    j2me_enabled_configs = CommCareBuildConfig.j2me_enabled_config_labels()
    for app in saved_apps:
        app['include_media'] = app['doc_type'] != 'RemoteApp'
        app['j2me_enabled'] = app['menu_item_label'] in j2me_enabled_configs
        app['target_commcare_flavor'] = (
            SavedAppBuild.get(app['_id']).target_commcare_flavor
            if toggles.TARGET_COMMCARE_FLAVOR.enabled(domain)
            else 'none'
        )

    if toggles.APPLICATION_ERROR_REPORT.enabled(request.couch_user.username):
        versions = [app['version'] for app in saved_apps]
        num_errors_dict = _get_error_counts(domain, app_id, versions)
        for app in saved_apps:
            app['num_errors'] = num_errors_dict.get(app['version'], 0)

    num_pages = int(ceil(total_apps / limit))

    return json_response({
        'apps': saved_apps,
        'pagination': {
            'total': total_apps,
            'num_pages': num_pages,
            'current_page': page,
        }
    })