diff --git a/frappe/boot.py b/frappe/boot.py
index fc09dd3dd5..d23d0fb440 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -24,6 +24,7 @@ from frappe.social.doctype.energy_point_settings.energy_point_settings import (
)
from frappe.utils import add_user_info, cstr, get_system_timezone
from frappe.utils.change_log import get_versions, has_app_update_notifications
+from frappe.utils.frappecloud import on_frappecloud
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled
@@ -445,7 +446,7 @@ def get_marketplace_apps():
apps = []
cache_key = "frappe_marketplace_apps"
- if frappe.conf.developer_mode:
+ if frappe.conf.developer_mode or not on_frappecloud():
return apps
def get_apps_from_fc():
diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py
index 11e9653372..7846ecd262 100644
--- a/frappe/tests/test_utils.py
+++ b/frappe/tests/test_utils.py
@@ -48,7 +48,7 @@ from frappe.utils import (
)
from frappe.utils.change_log import (
check_release_on_github,
- get_remote_url,
+ get_source_url,
parse_github_url,
)
from frappe.utils.data import (
@@ -1334,9 +1334,7 @@ class TestChangeLog(FrappeTestCase):
self.assertRaises(ValueError, check_release_on_github, owner="frappe", repo=None)
def test_get_remote_url(self):
- self.assertIsInstance(get_remote_url("frappe"), str)
- self.assertRaises(ValueError, get_remote_url, app=None)
- self.assertRaises(ValueError, get_remote_url, app="this_doesnt_exist")
+ self.assertIsInstance(get_source_url("frappe"), str)
def test_parse_github_url(self):
# using erpnext as repo in order to be different from the owner
diff --git a/frappe/utils/change_log.py b/frappe/utils/change_log.py
index 1517032ee9..5262178456 100644
--- a/frappe/utils/change_log.py
+++ b/frappe/utils/change_log.py
@@ -10,6 +10,7 @@ from semantic_version import Version
import frappe
from frappe import _, safe_decode
from frappe.utils import cstr
+from frappe.utils.frappecloud import on_frappecloud
def get_change_log(user=None):
@@ -166,7 +167,7 @@ def check_for_update():
apps = get_versions()
for app in apps:
- remote_url = get_remote_url(app)
+ remote_url = get_source_url(app)
if not remote_url:
continue
@@ -200,6 +201,7 @@ def check_for_update():
break
add_message_to_redis(updates)
+ return updates
def has_app_update_notifications() -> bool:
@@ -258,26 +260,11 @@ def parse_github_url(remote_url: str) -> tuple[str, str] | tuple[None, None]:
return (match[1], match[2]) if match else (None, None)
-def get_remote_url(app: str) -> str | None:
+def get_source_url(app: str) -> str | None:
"""Get the remote URL of the app."""
- if not app:
- raise ValueError("App cannot be empty")
-
- if app not in frappe.get_installed_apps(_ensure_on_bench=True):
- raise ValueError("This app is not installed")
-
- app_path = frappe.get_app_path(app, "..")
- try:
- # Check if repo has a remote URL
- remote_url = subprocess.check_output(f"cd {app_path} && git ls-remote --get-url", shell=True)
- except subprocess.CalledProcessError:
- # Some apps may not have git initialized or not hosted somewhere
- return None
-
- if isinstance(remote_url, bytes):
- remote_url = remote_url.decode()
-
- return remote_url
+ pyproject = get_pyproject(app)
+ if remote_url := pyproject.get("project", {}).get("urls", {}).get("Repository"):
+ return remote_url.rstrip("/")
def add_message_to_redis(update_json):
@@ -306,12 +293,13 @@ def show_update_popup():
release_links = ""
for app in updates[update_type]:
app = frappe._dict(app)
- release_links += "{title}: v{available_version}
".format(
- available_version=app.available_version,
- org_name=app.org_name,
- app_name=app.app_name,
- title=app.title,
- )
+ release_links += f"""
+ {app.title}:
+
+ v{app.available_version}
+
+ """
if release_links:
message = _("New {} releases for the following apps are available").format(_(update_type))
update_message += (
@@ -320,6 +308,37 @@ def show_update_popup():
)
)
+ primary_action = None
+ if on_frappecloud():
+ primary_action = {
+ "label": _("Update from Frappe Cloud"),
+ "client_action": "window.open",
+ "args": f"https://frappecloud.com/dashboard/sites/{frappe.local.site}",
+ }
+
if update_message:
- frappe.msgprint(update_message, title=_("New updates are available"), indicator="green")
+ frappe.msgprint(
+ update_message,
+ title=_("New updates are available"),
+ indicator="green",
+ primary_action=primary_action,
+ )
frappe.cache.srem("update-user-set", user)
+
+
+def get_pyproject(app: str) -> dict | None:
+ pyproject_path = frappe.get_app_path(app, "..", "pyproject.toml")
+
+ if not os.path.exists(pyproject_path):
+ return None
+
+ try:
+ from tomli import load
+ except ImportError:
+ try:
+ from tomllib import load
+ except ImportError:
+ return None
+
+ with open(pyproject_path, "rb") as f:
+ return load(f)
diff --git a/frappe/utils/frappecloud.py b/frappe/utils/frappecloud.py
new file mode 100644
index 0000000000..5244429f18
--- /dev/null
+++ b/frappe/utils/frappecloud.py
@@ -0,0 +1,11 @@
+import frappe
+
+FRAPPE_CLOUD_DOMAINS = ("frappe.cloud", "erpnext.com", "frappehr.com")
+
+
+def on_frappecloud() -> bool:
+ """Returns true if running on Frappe Cloud.
+
+
+ Useful for modifying few features for better UX."""
+ return frappe.local.site.endswith(FRAPPE_CLOUD_DOMAINS)
diff --git a/pyproject.toml b/pyproject.toml
index a484f5207e..d06238ef9a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -73,6 +73,7 @@ dependencies = [
"terminaltables~=3.1.10",
"traceback-with-variables~=2.0.4",
"typing_extensions>=4.6.1,<5",
+ "tomli~=2.0.1",
"uuid-utils~=0.6.1",
"xlrd~=2.0.1",
"zxcvbn~=4.4.28",
@@ -87,6 +88,11 @@ dependencies = [
"posthog~=3.0.1",
]
+[project.urls]
+Homepage = "https://frappeframework.com/"
+Repository = "https://github.com/frappe/frappe.git"
+"Bug Reports" = "https://github.com/frappe/frappe/issues"
+
[build-system]
requires = ["flit_core >=3.4,<4"]
build-backend = "flit_core.buildapi"