def test_save_bundles_to_db(self):
        """
        Test the method to save a Bundle Manager of an App to the database, using
        the revamped DB.
        :return:
        """

        testBundle = Bundle("all", "ALL", "ALL")
        testBundle._msgs["key.one"] = "One"
        testBundle._msgs["key.two"] = "Two"

        bm = BundleManager()
        bm._bundles = {}
        bm._bundles["all_ALL_ALL"] = testBundle

        save_bundles_to_db(self.tapp, bm)

        # Retrieve the bundles from the DB.
        bundle = db.session.query(appcomposer.models.Bundle).filter_by(app=self.tapp, lang="all_ALL", target="ALL").first()
        assert bundle is not None

        self.assertIsNotNone(bundle)
        self.assertEquals("all_ALL", bundle.lang)
        self.assertEquals("ALL", bundle.target)

        messages = db.session.query(appcomposer.models.Message).filter_by(bundle=bundle).all()
        messages = {m.key: m.value for m in messages}

        self.assertEquals(2, len(messages))
        self.assertEquals("One", messages["key.one"])
        self.assertEquals("Two", messages["key.two"])
示例#2
0
def app_translation_serve_list():
    """
    Serves a list of translated apps, so that a cache can be updated.
    Aims to be SHINDIG-compatible, though it doesn't implement this feature yet.

    This is the new version (for the new ownership system). It is somewhat inefficient
    and the current etag scheme doesn't make much sense anymore.
    """

    # Get a list of distinct XMLs.
    specs = _db_get_diff_specs()

    output = {}

    for spec in specs:
        # For each spec we get the ownerships.
        ownerships = _db_get_ownerships(spec)

        bundles = []

        for ownership in ownerships:
            lang = ownership.value
            bm = BundleManager.create_from_existing_app(ownership.app.data)
            keys = [key for key in bm._bundles.keys() if BundleManager.fullcode_to_partialcode(key) == lang]

            etag = str(ownership.app.modification_date)
            bundles.append({"keys": keys, "etag": etag})

        output[spec] = {"bundles": bundles}

    response = make_response(json.dumps(output, indent=True))
    response.mimetype = "application/json"
    return response
示例#3
0
def app_xml(appid, group):
    """
    app_xml(appid, group)

    Provided for end-users. This is the function that provides hosting for the
    gadget specs for a specified App. The gadget specs are actually dynamically
    generated, as every time a request is made the original XML is obtained and
    modified.

    @param appid: Identifier of the App.
    @param group: Group that will act as a filter. If, for instance, it is set to 14-18, then only
    Bundles that belong to that group will be shown.
    @return: XML of the modified Gadget Spec with the Locales injected, or an HTTP error code
    if an error occurs.
    """
    app = get_app(appid)

    if app is None:
        return render_template("composers/errors.html", message="Error 404: App doesn't exist"), 404

    # The composer MUST be 'translate'
    if app.composer != "translate":
        return render_template("composers/errors.html",
                               message="Error 500: The composer for the specified App is not Translate"), 500

    bm = BundleManager.create_from_existing_app(app.data)
    output_xml = bm.do_render_app_xml(appid, group)

    response = make_response(output_xml)
    response.mimetype = "application/xml"
    return response
    def test_translate_local_sync_creation_selectlang(self):
        """
        Ensure that we can create an app normally through a synchronous POST request to SELECTLANG.
        Note that this test relies on the accessibility of a local i18n.xml file.
        """
        url = "appcomposer/tests_data/googleExample/i18n.xml"
        rv = self.flask_app.post("/composers/translate/selectlang", data={"appname": "UTApp", "appurl": url}, follow_redirects=True)

        # Check whether it seems to be the page we expect.
        assert rv.status_code == 200  # Page found code.
        assert rv.data.count("option") > 100  # Lots of them, because of the languages list.
        assert "submit" in rv.data

        # Check that we did indeed create the app properly.
        with self.flask_app:
            self.flask_app.get("/")
            app = api.get_app_by_name("UTApp")

            assert app is not None
            appdata = app.data

            data = json.loads(appdata)

            assert "spec" in data
            assert url == data["spec"]

            full_app_data = load_appdata_from_db(app)
            bm = BundleManager.create_from_existing_app(full_app_data)

            assert bm.get_gadget_spec() == url
            assert len(bm._bundles) > 3

            defaultBundle = bm.get_bundle("all_ALL_ALL")
            assert defaultBundle is not None
            assert len(defaultBundle.get_msgs()) > 6
def transfer_ownership():

    # No matter if we are handling a GET or POST, we require these parameters.
    appid = request.values.get("appid")

    # Retrieve the application we want to view or edit.
    app = get_app(appid)
    if app is None:
        return render_template("composers/errors.html", message=gettext("App not found")), 404

    # Make sure the logged in user owns the app.
    user = current_user()
    if app.owner != user:
        return render_template("composers/errors.html", message=gettext("Not Authorized: User does not own app")), 403

    # Get the XMLSPEC
    bm = BundleManager.create_from_existing_app(app.data)
    spec = bm.get_gadget_spec()

    # Get the language
    lang = request.values.get("lang")
    if lang is None:
        return render_template("composers/errors.html", message=gettext("Lang not specified")), 400

    # Verify that we are the owners of the language we are trying to transfer.
    owner_app = _db_get_lang_owner_app(spec, lang)
    if owner_app != app:
        return render_template("composers/errors.html", message=gettext("Not Authorized: App does not own language")), 403

    # Get the possible apps to which we can concede ownership.
    apps = _db_get_spec_apps(spec)
    apps = [a for a in apps if a != app]

    # We received a POST request. We need to transfer the ownership.
    if request.method == "POST":

        # Protect against CSRF attacks.
        if not verify_csrf(request):
            return render_template("composers/errors.html",
                                   message=gettext("Request does not seem to come from the right source (csrf check)")), 400

        # Verify that we were passed the target app.
        targetapp = get_app(request.values.get("transfer"))
        if targetapp is None:
            return render_template("composers/errors.html", message=gettext("Target app not specified")), 400

        # Verify that the target app is of the same spec.
        targetappdata = json.loads(targetapp.data)
        targetspec = targetappdata["spec"]
        if targetspec != spec:
            return render_template("composers/errors.html", message=gettext("Target app does not have the same spec")), 400

        # Carry out the transfer.
        _db_transfer_ownership(lang, app, targetapp)

        # Redirect to selectlang.
        return redirect(url_for("translate.translate_selectlang", appid=app.unique_id))

    # For GET
    return render_template("composers/translate/transfer_ownership.html", app=app, apps=apps, xmlspec=spec, lang=lang)
    def test_load_appdata_from_db(self):
        """
        Test the method to load the appdata as if it was the old legacy JSON object from the database.
        :return:
        """
        data = {
            "spec": "http://justatest.com",
            "random_setting": 1,
            "bundles": {
                "all_ALL_ALL": {
                    "lang": "all",
                    "country": "ALL",
                    "target": "ALL",
                    "messages" : {
                        "key.one": "One"
                    }
                }
            }
        }
        self.tapp.data = json.dumps(data)
        db.session.add(self.tapp)
        db.session.commit()

        appdata = load_appdata_from_db(self.tapp)

        # The bundles should now be empty, because load_appdata_from_db should not rely on the data's bundles, but
        # on the db information (and for now there is none). The settings should be respected.
        self.assertIsNotNone(appdata)
        self.assertIn("random_setting", appdata)
        self.assertIn("bundles", appdata)
        self.assertTrue(len(appdata["bundles"]) == 0)


        # To further test info retrieval, we need to add info to the db.
        testBundle = Bundle("all", "ALL", "ALL")
        testBundle._msgs["key.three"] = "Three"
        testBundle._msgs["key.four"] = "Four"
        bm = BundleManager()
        bm._bundles = {}
        bm._bundles["all_ALL_ALL"] = testBundle
        save_bundles_to_db(self.tapp, bm)


        appdata = load_appdata_from_db(self.tapp)
        self.assertTrue(appdata["bundles"]["all_ALL_ALL"]["messages"]["key.three"] == "Three")
        self.assertTrue(appdata["bundles"]["all_ALL_ALL"]["messages"]["key.four"] == "Four")
    def test_translate_default_autoaccept(self):
        with self.flask_app:
            rv = self.login("testuser", "password")
            app = api.create_app("UTApp", "translate", '{"spec":"http://justatest.com", "bundles":{}}')
            api.add_var(app, "spec", "http://justatest.com")

            # Test that autoaccept is True (it's the default).
            bm = BundleManager.create_from_existing_app(json.loads(app.data))
            assert bm.get_autoaccept() == True
    def test_translate_default_autoaccept(self):
        with self.flask_app:
            rv = self.login("testuser", "password")
            app = api.create_app("UTApp", "translate", "http://justatest.com", '{"spec":"http://justatest.com", "bundles":{}}')

            # Test that autoaccept is True (it's the default).
            full_app_data = load_appdata_from_db(app)
            bm = BundleManager.create_from_existing_app(full_app_data)
            assert bm.get_autoaccept() == True
    def test_translate_create_app_with_empty_default(self):
        """
        [REGRESSION TEST: If the DEFAULT XML is an invalid fail or empty, it was not being handled nicely]
        Ensure that we can create an app normally through a synchronous POST request to SELECTLANG.
        """
        url = "appcomposer/tests_data/relativeExampleEmptyDefault/i18n.xml"
        rv = self.flask_app.post("/composers/translate/selectlang", data={"appname": "UTApp", "appurl": url}, follow_redirects=True)

        # Check whether it seems to be the page we expect.
        assert rv.status_code == 200  # Page found code.
        assert rv.data.count("option") > 100  # Lots of them, because of the languages list.
        assert "submit" in rv.data
        assert "Localise" in rv.data

        # Check that we did indeed create the app properly.
        with self.flask_app:
            self.flask_app.get("/")
            app = api.get_app_by_name("UTApp")

            assert app is not None
            appdata = app.data
            assert len(appdata) > 500

            data = json.loads(appdata)

            assert "spec" in data
            assert url == data["spec"]

            bm = BundleManager.create_from_existing_app(appdata)

            assert bm.get_gadget_spec() == url

            # The bundles should be 3 (DEFAULT - copied from English, English, German).
            assert len(bm._bundles) == 3

            defaultBundle = bm.get_bundle("all_ALL_ALL")
            assert defaultBundle is not None
            assert len(defaultBundle.get_msgs()) > 6

            gerBundle = bm.get_bundle("de_ALL_ALL")
            assert gerBundle is not None
            assert len(gerBundle.get_msgs()) > 6

            enBundle = bm.get_bundle("en_ALL_ALL")
            assert enBundle is not None
            assert len(enBundle.get_msgs()) > 6

            # Ensure that the language that got copied as DEFAULT is really english
            assert enBundle.get_msgs()["hello_world"] == "Hello World."
            assert defaultBundle.get_msgs()["hello_world"] == "Hello World."
    def test_translate_create_with_multi_bundle(self):
        """
        Ensure that we can create an app where the xml contains two different bundles for the same language.
        """
        url = "appcomposer/tests_data/relativeMultibundleExample/i18n.xml"
        rv = self.flask_app.post("/composers/translate/selectlang", data={"appname": "UTApp", "appurl": url}, follow_redirects=True)

        # Check whether it seems to be the page we expect.
        assert rv.status_code == 200  # Page found code.
        assert rv.data.count("option") > 100  # Lots of them, because of the languages list.
        assert "submit" in rv.data
        assert "Localise" in rv.data

        # Check that we did indeed create the app properly.
        with self.flask_app:
            self.flask_app.get("/")
            app = api.get_app_by_name("UTApp")

            assert app is not None
            appdata = app.data
            assert len(appdata) > 500

            data = json.loads(appdata)

            assert "spec" in data
            assert url == data["spec"]

            bm = BundleManager.create_from_existing_app(appdata)

            assert bm.get_gadget_spec() == url
            assert len(bm._bundles) == 2

            defaultBundle = bm.get_bundle("all_ALL_ALL")
            assert defaultBundle is not None
            assert len(defaultBundle.get_msgs()) > 6

            gerBundle = bm.get_bundle("de_ALL_ALL")
            assert gerBundle is not None
            assert len(gerBundle.get_msgs()) > 6
            msgs = gerBundle.get_msgs()

            # Ensure the translations in the first file are present
            assert msgs["hello_world"] == "Hallo Welt."

            # Ensure the translations in the second file are present as well
            assert msgs["gray"] == "Grau"
示例#11
0
def translate_edit():
    """
    Translation editor for the selected language.

    @note: Returns error 400 if the source language or group don't exist.
    """

    # No matter if we are handling a GET or POST, we require these parameters.
    appid = request.values.get("appid")
    srclang = request.values.get("srclang")
    targetlang = request.values.get("targetlang")
    srcgroup = request.values.get("srcgroup")
    targetgroup = request.values.get("targetgroup")

    # Retrieve the application we want to view or edit.
    app = get_app(appid)
    if app is None:
        return render_template("composers/errors.html", message=gettext("App not found")), 404

    bm = BundleManager.create_from_existing_app(app.data)
    spec = bm.get_gadget_spec()

    # Retrieve the bundles for our lang. For this, we build the code from the info we have.
    srcbundle_code = BundleManager.partialcode_to_fullcode(srclang, srcgroup)
    targetbundle_code = BundleManager.partialcode_to_fullcode(targetlang, targetgroup)

    srcbundle = bm.get_bundle(srcbundle_code)

    # Ensure the existence of the source bundle.
    if srcbundle is None:
        return render_template("composers/errors.html",
                               message=gettext("The source language and group combination does not exist")), 400

    targetbundle = bm.get_bundle(targetbundle_code)

    # The target bundle doesn't exist yet. We need to create it ourselves.
    if targetbundle is None:
        splits = targetlang.split("_")
        if len(splits) == 2:
            lang, country = splits
            targetbundle = Bundle(lang, country, targetgroup)
            bm.add_bundle(targetbundle_code, targetbundle)

    # Get the owner for this target language.
    owner_app = _db_get_lang_owner_app(spec, targetlang)

    # If the language has no owner, we declare ourselves as owners.
    if owner_app is None:
        _db_declare_ownership(app, targetlang)
        owner_app = app

    # We override the standard Ownership's system is_owner.
    # TODO: Verify that this doesn't break anything.
    is_owner = owner_app == app

    # Get the language names
    target_translation_name = targetbundle.get_readable_name()
    source_translation_name = srcbundle.get_readable_name()

    # This is a GET request. We are essentially viewing-only.
    if request.method == "GET":
        pass

    # This is a POST request. We need to save the entries.
    else:

        # Protect against CSRF attacks.
        if not verify_csrf(request):
            return render_template("composers/errors.html",
                                   message=gettext("Request does not seem to come from the right source (csrf check)")), 400

        # Retrieve a list of all the key-values to save. That is, the parameters which start with _message_.
        messages = [(k[len("_message_"):], v) for (k, v) in request.values.items() if k.startswith("_message_")]

        # Save all the messages we retrieved from the POST or GET params into the Bundle.
        for identifier, msg in messages:
            if len(msg) > 0:  # Avoid adding empty messages.
                targetbundle.add_msg(identifier, msg)

        # Now we need to save the changes into the database.
        json_str = bm.to_json()
        update_app_data(app, json_str)

        flash(gettext("Changes have been saved."), "success")

        propose_to_owner = request.values.get("proposeToOwner")
        if propose_to_owner is not None and owner_app != app:

            # Normally we will add the proposal to the queue. However, sometimes the owner wants to auto-accept
            # all proposals. We check for this. If the autoaccept mode is enabled on the app, we do the merge
            # right here and now.
            obm = BundleManager.create_from_existing_app(owner_app.data)
            if obm.get_autoaccept():
                flash(gettext("Changes are being applied instantly because the owner has auto-accept enabled"))

                # Merge into the owner app.
                obm.merge_bundle(targetbundle_code, targetbundle)

                # Now we need to update the owner app's data. Because we aren't the owners, we can't use the appstorage
                # API directly.
                owner_app.data = obm.to_json()
                db.session.add(owner_app)
                db.session.commit()

                # [Context: We are not the leading Bundles, but our changes are merged directly into the leading Bundle]
                # We report the change to a "leading" bundle.
                on_leading_bundle_updated(spec, targetbundle)

            else:

                # We need to propose this Bundle to the owner.
                # Note: May be confusing: app.owner.login refers to the generic owner of the App, and not the owner
                # we are talking about in the specific Translate composer.
                proposal_data = {"from": app.owner.login, "timestamp": time.time(), "bundle_code": targetbundle_code,
                                 "bundle_contents": targetbundle.to_jsonable()}

                proposal_json = json.dumps(proposal_data)

                # Link the proposal with the Owner app.
                add_var(owner_app, "proposal", proposal_json)

                flash(gettext("Changes have been proposed to the owner"))

        # If we are the owner app.
        if owner_app == app:
            # [Context: We are the leading Bundle]
            # We report the change.
            on_leading_bundle_updated(spec, targetbundle)

        # Check whether the user wants to exit or to continue editing.
        if "save_exit" in request.values:
            return redirect(url_for("user.apps.index"))




    return render_template("composers/translate/edit.html", is_owner=is_owner, app=app, srcbundle=srcbundle,
                           targetbundle=targetbundle, spec=spec, target_translation_name=target_translation_name,
                           source_translation_name=source_translation_name)
示例#12
0
def translate_proposed_list():
    """
    Displays the list of proposed translations.
    """

    # No matter if we are handling a GET or POST, we require these parameters.
    appid = request.values["appid"]
    app = get_app(appid)

    appdata = json.loads(app.data)

    # TODO: !!!!! THIS IS MISSING AND IS IMPORTANT !!!!!

    # Ensure that only the app owner can carry out these operations.
    # owner_app = _db_get_owner_app(appdata["spec"])
    # if app != owner_app:
    #    return render_template("composers/errors.html",
    #                           message="Not Authorized: You don't seem to be the owner of this app")

    # Get the list of proposed translations.
    proposal_vars = _db_get_proposals(app)
    proposed_translations = []
    for prop in proposal_vars:
        propdata = json.loads(prop.value)
        propdata["id"] = str(prop.var_id)
        proposed_translations.append(propdata)

    # If we received a POST with acceptButton set then we will need to merge the
    # proposal.
    if request.method == "POST" and request.values.get("acceptButton") is not None:
        proposal_id = request.values.get("proposals")
        if proposal_id is None:
            return render_template("composers/errors.html", message=gettext("Proposal not selected"))

        merge_data = request.values.get("data")
        if merge_data is None:
            return render_template("composers/errors.html", message=gettext("Merge data was not provided"))
        merge_data = json.loads(merge_data)

        # TODO: Optimize this. We already have the vars.
        proposal = AppVar.query.filter_by(app=app, var_id=proposal_id).first()
        if proposal is None:
            return render_template("composers/errors.html", message=gettext("Proposals not found"))

        data = json.loads(proposal.value)
        bundle_code = data["bundle_code"]

        proposed_bundle = Bundle.from_messages(merge_data, bundle_code)

        bm = BundleManager.create_from_existing_app(app.data)
        bm.merge_bundle(bundle_code, proposed_bundle)

        update_app_data(app, bm.to_json())

        flash(gettext("Merge done."), "success")

        # Remove the proposal from the DB.
        remove_var(proposal)

        # Remove it from our current proposal list as well, so that it isn't displayed anymore.
        proposed_translations = [prop for prop in proposed_translations if prop["id"] != proposal_id]

    # The DENY button was pressed. We have to discard the whole proposal.
    elif request.method == "POST" and request.values.get("denyButton") is not None:
        proposal_id = request.values.get("proposals")
        if proposal_id is None:
            return render_template("composers/errors.html", message=gettext("Proposal not selected"))

        proposal = AppVar.query.filter_by(app=app, var_id=proposal_id).first()
        if proposal is None:
            return render_template("composers/errors.html", message=gettext("Proposal not found"))

        remove_var(proposal)

        # Remove it from our current proposal list as well, so that it isn't displayed anymore.
        proposed_translations = [prop for prop in proposed_translations if prop["id"] != proposal_id]

    return render_template("composers/translate/proposed_list.html", app=app, proposals=proposed_translations)
示例#13
0
def app_translation_serve():
    """
    Serves a translation through the API that SHINDIG expects.
    GET parameters expected:
        app_url
        lang
        target
    """
    app_xml = request.values.get("app_url")
    if app_xml is None:
        return render_template("composers/errors.html",
                               message="Error 400: Bad Request: Parameter app_url is missing."), 400
    try:
        # XXX FIXME
        # TODO: this makes this method to call twice the app_xml. We shouldn't need
        # that. We should have the contents here downloaded for later.
        if app_xml.startswith(('http://', 'https://')):
            print app_xml
            xmldoc = minidom.parseString(urllib2.urlopen(app_xml).read())
            app_xml = get_original_url(xmldoc, app_xml)
            print "New app xml:", app_xml
    except:
        traceback.print_exc()
        pass


    lang = request.values.get("lang")
    if lang is None:
        return render_template("composers/errors.html",
                               message="Error 400: Bad Request: Parameter lang is missing."), 400
    if len(lang) == 2:
        lang = '%s_ALL' % lang

    target = request.values.get("target")
    if target is None:
        return render_template("composers/errors.html",
                               message="Error 400: Bad Request: Parameter target is missing."), 400

    owner_app = _db_get_lang_owner_app(app_xml, lang)

    if owner_app is None:
        return render_template("composers/errors.html", message="Error 404: App not found."), 404


    # Parse the app's data.
    bm = BundleManager.create_from_existing_app(owner_app.data)

    # Build the name to request.
    bundle_name = "%s_%s" % (lang, target)
    bundle = bm.get_bundle(bundle_name)

    if bundle is None:
        dbg_info = str(bm._bundles.keys())
        return render_template("composers/errors.html",
                               message="Error 404: Could not find such language for the specified app. Available keys are: " + dbg_info), 404

    output_xml = bundle.to_xml()

    response = make_response(output_xml)
    response.mimetype = "application/xml"
    return response
示例#14
0
def translate_selectlang():
    """
    Source language & target language selection.

    Different cases when creating an App:
        - Default translation exists; other translations exist -> Created normally
        - Default translation DOES NOT exist or is invalid; english translations exist or is invalid -> English is the default
        - Default translation DOES NOT exist or is invalid; english translation DOES NOT exist or is invalid; other translations exist -> First Other translation is the default
        - Default translation, english translation, and other translations DO NOT exist or are invalid -> NoValidTranslations page; App is not created
    """

    # Note: The name pcode refers to the fact that the codes we deal with here are partial (do not include
    # the group).

    # We will build a list of possible languages using the babel library.
    languages = babel.core.Locale("en", "US").languages.items()
    languages.sort(key=lambda it: it[1])
    # TODO: Currently, we filter languages which contain "_" in their code so as to simplify.
    # Because we use _ throughout the composer as a separator character, trouble is caused otherwise.
    # Eventually we should consider whether we need to support special languages with _
    # on its code.
    targetlangs_codes = [lang[0] + "_ALL" for lang in languages if "_" not in lang[0]]

    targetlangs_list = [{"pcode": code, "repr": BundleManager.get_locale_english_name(
        *BundleManager.get_locale_info_from_code(code))} for code in targetlangs_codes]

    full_groups_list = [("ALL", "ALL"), ("10-13", "Preadolescence (age 10-13)"), ("14-18", "Adolescence (age 14-18)")]

    # As of now (may change in the future) if it is a POST we are creating the app for the first time.
    # Hence, we will need to carry out a full spec retrieval.
    if request.method == "POST":

        # Protect against CSRF attacks.
        if not verify_csrf(request):
            return render_template("composers/errors.html",
                                   message=gettext(
                                       "Request does not seem to come from the right source (csrf check)")), 400

        # URL to the XML spec of the gadget.
        appurl = request.form.get("appurl")
        spec = appurl
        if appurl is None or len(appurl) == 0:
            flash(gettext("An application URL is required"), "error")
            return redirect(url_for("translate.translate_index"))

        base_appname = request.values.get("appname")
        if base_appname is None:
            return render_template("composers/errors.html", message=gettext("An appname was not specified"))

        try:
            # XXX FIXME
            # TODO: this makes this method to call twice the app_xml. We shouldn't need
            # that. We should have the contents here downloaded for later.
            if appurl.startswith(('http://', 'https://')):
                print appurl
                xmldoc = minidom.parseString(urllib2.urlopen(appurl).read())
                appurl = get_original_url(xmldoc, appurl)
                print "New app xml:", appurl
        except:
            traceback.print_exc()
            pass

        # Generates a unique (for the current user) name for the App,
        # based on the base name that the user himself chose. Note that
        # this method can actually return None under certain conditions.
        appname = _find_unique_name_for_app(base_appname)
        if appname is None:
            return render_template("composers/errors.html",
                                   message=gettext("Too many Apps with the same name. Please, choose another."))

        # Create a fully new App. It will be automatically generated from a XML.
        try:
            bm = BundleManager.create_new_app(appurl)
        except NoValidTranslationsException:
            return render_template("composers/errors.html",
                                   message=gettext(
                                       "The App you have chosen does not seem to have any translation. At least a base translation is required, which will often be"
                                       " prepared by the original developer.")
            ), 400
        except InvalidXMLFileException:
            # TODO: As of now, not sure that this exception can actually ever arise. Maybe it should be merged with NoValidTranslationsException.
            return render_template("composers/errors.html",
                                   message=gettext(
                                       "Invalid XML in either the XML specification file or the XML translation bundles that it links to.")), 400
        except MissingSchema:
            return render_template("composers/errors.html",
                                   message=gettext(
                                       "Failed to retrieve the XML spec. The URL was maybe invalid or not available.")), 400

        if len(bm._bundles) == 0:
            # TODO: Consider adding a "go-back" screen / button.
            return render_template("composers/errors.html",
                                   message=gettext(
                                       "The App you have chosen does not seem to have a base translation. The original developer needs to prepare it for internationalization first.")), 400

        spec = bm.get_gadget_spec()  # For later

        # Build JSON data
        js = bm.to_json()

        # Create a new App from the specified XML
        app = create_app(appname, "translate", js)

        # Register our appurl as the "spec" in an app-specific variable in the DB. This will let us search later, for
        # certain advanced features.
        set_var(app, "spec", appurl)


        # Handle Ownership-related logic here.
        # Locate the owner for the App's DEFAULT language.
        ownerApp = _db_get_lang_owner_app(appurl, "all_ALL")
        # If there isn't already an owner for the default languages, we declare ourselves
        # as the owner for this App's default language.
        if ownerApp is None:
            _db_declare_ownership(app, "all_ALL")
            ownerApp = app

            # Report initial bundle creation. Needed for the MongoDB replica.
            for bundle in bm.get_bundles("all_ALL"):
                on_leading_bundle_updated(spec, bundle)

        # We do the same for the other included languages which are not owned already.
        # If the other languages have a translation but no owner, then we declare ourselves as their owner.
        for partialcode in bm.get_langs_list():
            otherOwner = _db_get_lang_owner_app(appurl, partialcode)
            if otherOwner is None:
                _db_declare_ownership(app, partialcode)

                # Report initial bundle creation. Needed for the MongoDB replica.
                for bundle in bm.get_bundles(partialcode):
                    on_leading_bundle_updated(spec, bundle)

        # Advanced merge. Merge owner languages into our bundles.
        do_languages_initial_merge(app, bm)


        # Find out which locales does the app provide (for now).
        translated_langs = bm.get_locales_list()

        # We do a redirect rather than rendering in the POST. This way we can get proper
        # URL.
        return redirect(url_for('translate.translate_selectlang', appid=app.unique_id))

    # This was a GET, the app should exist already somehow, we will try to retrieve it.
    elif request.method == "GET":

        appid = request.args.get("appid")
        if appid is None:
            flash(gettext("appid not received"), "error")

            # An appid is required.
            return redirect(url_for("user.apps.index"))

        app = get_app(appid)
        if app is None:
            return render_template("composers/errors.html",
                                   message=gettext("Specified App doesn't exist")), 404

        # Load a BundleManager from the app data.
        bm = BundleManager.create_from_existing_app(app.data)

        spec = bm.get_gadget_spec()

        # Check whether an owner exists. If it doesn't, it means the original owner was deleted.
        # In that case, we make ourselves a new owner.
        # Locate the owner for the App's DEFAULT language.
        ownerApp = _db_get_lang_owner_app(spec, "all_ALL")
        # If there isn't already an owner for the default languages, we declare ourselves
        # as the owner for this App's default language.
        # TODO: NOTE: With the latest changes, this should never happen, because owners are declared
        # for all existing languages by the user who creates the app. It may, however, happen in the future,
        # when we take deletions and such into account.
        if ownerApp is None:
            _db_declare_ownership(app, "all_ALL")
            ownerApp = app

        translated_langs = bm.get_locales_list()


    # The following is again common for both GET (view) and POST (edit).

    # Check ownership. Probably eventually we will remove the ownership check above.
    ownerApp = _db_get_lang_owner_app(spec, "all_ALL")
    if ownerApp == app:
        is_owner = True
    else:
        is_owner = False

    owner = ownerApp.owner
    if not is_owner and owner is None:
        # TODO: Improve this error handling. This should NEVER happen.
        flash(gettext("Error: Language Owner is None"), "error")
        return render_template("composers/errors.html",
                               message=gettext("Internal Error: Language owner is None")), 500


    # Just for the count of proposals
    proposal_num = len(_db_get_proposals(app))

    # Build a dictionary. For each source lang, a list of source groups.
    src_groups_dict = defaultdict(list)
    for loc in translated_langs:
        src_groups_dict[loc["pcode"]].append(loc["group"])

    locales_codes = [tlang["pcode"] for tlang in translated_langs]

    # Remove from the suggested targetlangs those langs which are already present on the bundle manager,
    # because those will be added to the targetlangs by default.
    suggested_target_langs = [elem for elem in targetlangs_list if elem["pcode"] not in locales_codes]

    # We pass the autoaccept data var so that it can be rendered.
    # TODO: Optimize this once we have this fully covered with tests.
    data = json.loads(app.data)
    autoaccept = data.get("autoaccept",
                          True)  # We autoaccept by default. Problems may arise if this value changes, because it is used in a couple of places.

    # We pass some parameters as JSON strings because they are generated dynamically
    # through JavaScript in the template.
    return render_template("composers/translate/selectlang.html",
                           app=app,  # Current app object.
                           xmlspec=spec,  # URL to the App XML.
                           autoaccept=autoaccept,  # Whether the app is configured to autoaccept proposals or not.
                           suggested_target_langs=suggested_target_langs,  # Suggested (not already translated) langs
                           source_groups_json=json.dumps(src_groups_dict),  # Source groups in a JSON string
                           full_groups_json=json.dumps(full_groups_list),  # (To find names etc)
                           target_groups=full_groups_list,  # Target groups in a JSON string
                           translated_langs=translated_langs,  # Already translated langs
                           is_owner=is_owner,  # Whether the loaded app has the "Owner" status
                           owner=owner,  # Reference to the Owner
                           proposal_num=proposal_num)  # Number of pending translation proposals