Merge pull request #26395 from barredterra/address-autocomplete

feat: address autocomplete dialog
This commit is contained in:
Akhil Narang 2024-09-06 15:04:01 +05:30 committed by GitHub
commit 4eac35081c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 372 additions and 6 deletions

View file

@ -111,6 +111,9 @@ def get_bootinfo():
bootinfo.subscription_conf = add_subscription_conf()
bootinfo.marketplace_apps = get_marketplace_apps()
bootinfo.changelog_feed = get_changelog_feed_items()
bootinfo.enable_address_autocompletion = frappe.db.get_single_value(
"Geolocation Settings", "enable_address_autocompletion"
)
if sentry_dsn := get_sentry_dsn():
bootinfo.sentry_dsn = sentry_dsn

View file

@ -0,0 +1,8 @@
// Copyright (c) 2024, Frappe Technologies and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Geolocation Settings", {
// refresh(frm) {
// },
// });

View file

@ -0,0 +1,64 @@
{
"actions": [],
"creation": "2024-04-09 23:41:49.747820",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"enable_address_autocompletion",
"provider",
"base_url",
"api_key"
],
"fields": [
{
"fieldname": "provider",
"fieldtype": "Select",
"label": "Provider",
"mandatory_depends_on": "enable_address_autocompletion",
"options": "Geoapify\nNomatim\nHERE"
},
{
"depends_on": "eval: [\"Geoapify\", \"HERE\"].includes(doc.provider)",
"fieldname": "api_key",
"fieldtype": "Password",
"label": "API Key",
"mandatory_depends_on": "eval: doc.enable_address_autocompletion && [\"Geoapify\", \"HERE\"].includes(doc.provider)"
},
{
"default": "0",
"fieldname": "enable_address_autocompletion",
"fieldtype": "Check",
"label": "Enable Address Autocompletion"
},
{
"depends_on": "eval: doc.provider === \"Nomatim\"",
"fieldname": "base_url",
"fieldtype": "Data",
"label": "Base URL",
"mandatory_depends_on": "eval: doc.provider === \"Nomatim\" && doc.enable_address_autocompletion",
"options": "URL"
}
],
"issingle": 1,
"links": [],
"modified": "2024-06-22 09:18:34.306542",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Geolocation Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View file

@ -0,0 +1,54 @@
# Copyright (c) 2024, Frappe Technologies and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import get_url
from .providers.geoapify import Geoapify
from .providers.here import Here
from .providers.nomatim import Nomatim
class GeolocationSettings(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
api_key: DF.Password | None
base_url: DF.Data | None
enable_address_autocompletion: DF.Check
provider: DF.Literal["Geoapify", "Nomatim", "HERE"]
# end: auto-generated types
pass
@frappe.whitelist()
def autocomplete(txt: str) -> list[dict]:
if not txt:
return []
settings = frappe.get_single("Geolocation Settings")
if not settings.enable_address_autocompletion:
return []
if settings.provider == "Geoapify":
provider = Geoapify(settings.get_password("api_key"), frappe.local.lang)
elif settings.provider == "Nomatim":
provider = Nomatim(
base_url=settings.base_url,
referer=get_url(),
lang=frappe.local.lang,
)
elif settings.provider == "HERE":
provider = Here(settings.get_password("api_key"), frappe.local.lang)
else:
frappe.throw(_("This geolocation provider is not supported yet."))
return list(provider.autocomplete(txt))

View file

@ -0,0 +1,36 @@
import json
import requests
class Geoapify:
def __init__(self, api_key: str, lang: str | None = None):
self.api_key = api_key
self.lang = lang
self.base_url = "https://api.geoapify.com"
def autocomplete(self, query: str):
params = {
"text": query,
"apiKey": self.api_key,
"limit": 5,
"format": "json",
"lang": self.lang,
}
response = requests.get(f"{self.base_url}/v1/geocode/autocomplete", params=params)
response.raise_for_status()
results = response.json()["results"]
for result in results:
yield {
"label": result["formatted"],
"value": json.dumps(
{
"address_line1": result.get("address_line1"),
"city": result.get("city"),
"state": result.get("state"),
"pincode": result.get("postcode"),
"country": result.get("country"),
}
),
}

View file

@ -0,0 +1,44 @@
import json
import pycountry
import requests
import frappe
class Here:
def __init__(self, api_key: str, lang: str | None = None):
self.lang = lang
self.api_key = api_key
self.base_url = "https://autocomplete.search.hereapi.com/v1"
def autocomplete(self, query: str):
params = {
"q": query,
"apiKey": self.api_key,
"limit": 5,
"lang": self.lang,
}
response = requests.get(
f"{self.base_url}/autocomplete",
params=params,
)
response.raise_for_status()
results = response.json()["items"]
for result in results:
address = result["address"]
py_country = pycountry.countries.get(alpha_3=address.get("countryCode"))
frappe_country = frappe.db.get_value("Country", {"code": py_country.alpha_2.lower()})
yield {
"label": address["label"],
"value": json.dumps(
{
"address_line1": f'{address.get("street", "")} {address.get("houseNumber", "")}'.strip(),
"city": address.get("city", ""),
"state": address.get("state", ""),
"pincode": address.get("postalCode", ""),
"country": frappe_country or "",
}
),
}

View file

@ -0,0 +1,47 @@
import json
import requests
import frappe
class Nomatim:
def __init__(self, base_url: str, referer: str, lang: str | None = None):
self.lang = lang
self.referer = referer
self.base_url = base_url
def autocomplete(self, query: str):
params = {
"q": query,
"format": "json",
"limit": 5,
"addressdetails": 1,
"accept-language": self.lang,
"layer": "address",
}
response = requests.get(
f"{self.base_url}/search",
params=params,
headers={"Referer": self.referer},
)
response.raise_for_status()
results = response.json()
for result in results:
if "address" not in result:
continue
address = result["address"]
yield {
"label": result["display_name"],
"value": json.dumps(
{
"address_line1": f'{address.get("road")} {address.get("house_number", "")}'.strip(),
"city": address.get("city") or address.get("town") or address.get("village"),
"state": address.get("state"),
"pincode": address.get("postcode"),
"country": frappe.db.get_value("Country", {"code": address.get("country_code")}),
}
),
}

View file

@ -0,0 +1,9 @@
# Copyright (c) 2024, Frappe Technologies and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestGeolocationSettings(FrappeTestCase):
pass

View file

@ -104,3 +104,5 @@ import "./frappe/ui/chart.js";
import "./frappe/ui/datatable.js";
import "./frappe/ui/driver.js";
import "./frappe/scanner";
import "./frappe/ui/address_autocomplete/autocomplete_dialog.js";

View file

@ -0,0 +1,87 @@
frappe.provide("frappe.ui");
frappe.ui.AddressAutocompleteDialog = class AddressAutocompleteDialog {
constructor(opts) {
this.title = opts?.title || __("New Address");
this.link_doctype = opts?.link_doctype;
this.link_name = opts?.link_name;
this.after_insert = opts?.after_insert;
this.dialog = this._get_dialog();
}
_get_dialog() {
// sourcery skip: inline-immediately-returned-variable
const dialog = new frappe.ui.Dialog({
title: this.title,
fields: [
{
fieldname: "search",
fieldtype: "Autocomplete",
label: __("Search"),
reqd: 1,
get_query:
"frappe.integrations.doctype.geolocation_settings.geolocation_settings.autocomplete",
onchange: () => {
// Disable "Create Address" button if mandatory fields are missing
frappe.model.with_doctype("Address", () => {
const address = this.parse_selected_value();
const mandatory_fields = frappe
.get_meta("Address")
.fields.filter(
(field) =>
field.reqd &&
!field.default &&
field.fieldname !== "address_type"
);
const missing_fields = mandatory_fields.filter(
(field) => !address[field.fieldname]
);
const is_valid = missing_fields.length === 0;
if (is_valid) {
dialog.enable_primary_action();
} else {
dialog.disable_primary_action();
}
});
},
},
],
primary_action_label: __("Create Address"),
primary_action: () => {
// Insert the address into the database
dialog.hide();
const address = this.parse_selected_value();
address["doctype"] = "Address";
address["links"] = [
{
link_doctype: this.link_doctype,
link_name: this.link_name,
},
];
frappe.db.insert(address).then((doc) => {
this.after_insert && this.after_insert(doc);
});
},
secondary_action_label: __("Edit Address in Form"),
secondary_action: () => {
// Open the address in the form view
dialog.hide();
const address = this.parse_selected_value();
frappe.new_doc("Address", address);
},
});
return dialog;
}
parse_selected_value() {
const data = this.dialog.get_values();
return JSON.parse(data.search);
}
show() {
this.dialog.show();
}
};

View file

@ -12,7 +12,7 @@ $.extend(frappe.contacts, {
$(frm.fields_dict["address_html"].wrapper)
.html(frappe.render_template("address_list", frm.doc.__onload))
.find(".btn-address")
.on("click", () => new_record("Address", frm.doc));
.on("click", () => new_record("Address", frm));
}
// render contact
@ -20,7 +20,7 @@ $.extend(frappe.contacts, {
$(frm.fields_dict["contact_html"].wrapper)
.html(frappe.render_template("contact_list", frm.doc.__onload))
.find(".btn-contact")
.on("click", () => new_record("Contact", frm.doc));
.on("click", () => new_record("Contact", frm));
}
},
get_last_doc: function (frm) {
@ -59,12 +59,23 @@ $.extend(frappe.contacts, {
},
});
function new_record(doctype, source_doc) {
function new_record(doctype, frm) {
frappe.dynamic_link = {
doctype: source_doc.doctype,
doc: source_doc,
doctype: frm.doc.doctype,
doc: frm.doc,
fieldname: "name",
};
return frappe.new_doc(doctype);
if (frappe.boot.enable_address_autocompletion === 1 && doctype === "Address") {
new frappe.ui.AddressAutocompleteDialog({
title: __("New Address"),
link_doctype: frm.doc.doctype,
link_name: frm.doc.name,
after_insert: function (doc) {
frm.reload_doc();
},
}).show();
} else {
frappe.new_doc(doctype);
}
}

View file

@ -88,6 +88,7 @@ dependencies = [
"google-auth~=1.29.0",
"posthog~=3.0.1",
"vobject~=0.9.7",
"pycountry~=24.6.1",
]
[project.urls]