diff --git a/.gitignore b/.gitignore
index 9a8f819..b1d16ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,4 +12,4 @@ commit/public/node_modules/
commit/www/commit.html
commit/www/commit-docs.html
commit/public/commit
-commit/public/docs
\ No newline at end of file
+commit/public/docs
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..9a174e1
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,23 @@
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.6.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: check-yaml
+ - id: check-json
+ - id: check-added-large-files
+
+ # Enable basic Python formatting and import sorting if available
+ - repo: https://github.com/psf/black
+ rev: 24.8.0
+ hooks:
+ - id: black
+ language_version: python3
+
+ - repo: https://github.com/PyCQA/isort
+ rev: 5.13.2
+ hooks:
+ - id: isort
+ name: isort (python)
+ args: ["--profile", "black"]
diff --git a/MANIFEST.in b/MANIFEST.in
index fc6bd84..04f99fb 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -15,4 +15,4 @@ recursive-include commit *.png
recursive-include commit *.py
recursive-include commit *.svg
recursive-include commit *.txt
-recursive-exclude commit *.pyc
\ No newline at end of file
+recursive-exclude commit *.pyc
diff --git a/README.md b/README.md
index fde67ea..26e8471 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
Developer tooling for the Frappeverse 🪐
- Install on Frappe Cloud»
+ Install on Frappe Cloud »
Learn More »
@@ -26,17 +26,79 @@
+
# [commit](https://commit.frappe.cloud/)
-Born out of a need to improve developer tooling for Frappe, "Commit" allows you to visualize your app's database schema and view all it's APIs - improving developer productivity and security of your critical applications.
+Born out of a need to improve developer tooling for Frappe, "Commit" allows you to visualize your app's database schema and view all its APIs, generate documentation for the APIs, and manage your documentation through Commit Docs - improving developer productivity and security of your critical applications.
+
## Basic Installation
The below guide assumes that you already have a working Frappe and Bench installation. If you do not have it, then please head over to [Official Installation Guide](https://frappeframework.com/docs/user/en/installation).
-Go ahead and create a fresh new bench
+Go ahead and create a fresh new bench:
+```bash
+# Initialize a new bench
+bench init commit
+```
+
+```bash
+# Get the Commit app
+bench get-app https://github.com/The-commit-company/commit
+```
+
+```bash
+# Create a new site
+bench new-site my-site.localhost
+```
+
+```bash
+# Install the Commit app on the site
+bench --site my-site.localhost install-app commit
+```
+
+## Tech Stack
+
+### Common Across Web and Mobile
+- **Frappe Framework**: An open-source full-stack development framework using Python, MariaDB/Postgres, socket.io, and Redis.
+- **React**: A JavaScript library for building user interfaces.
+- **Frappe React SDK**: A React Hooks library for handling auth, data fetching, and API calls to the Frappe Framework backend.
+- **Tailwind CSS**: A utility-first CSS framework.
+- **MDX**: A Markdown format that allows you to use JSX components in your Markdown files.
+- **OpenAI API**: For AI-assisted documentation generation and editing.
+
+---
+
+## Production Setup
+
+### Managed Hosting
+You can try **Frappe Cloud**, a simple, user-friendly, and sophisticated open-source platform to host Frappe applications with peace of mind.
+
+It takes care of installation, setup, upgrades, monitoring, maintenance, and support of your Frappe deployments. It is a fully featured developer platform with the ability to manage and control multiple Frappe deployments.
+
+[Try on Frappe Cloud](https://frappecloud.com/)
+
+
+
+## Key Features
+
+- **Database Schema Visualization**: View and analyze your application's database structure graphically.
+
+- **API Explorer**: Easily access and test API endpoints within the Frappe framework.
+
+
+- **Command Reference**: Get an overview of available Frappe commands.
+-
+
+- **Documentation Generation**: Auto-generate API documentation.
+
+- **OpenAI Integration**: Edit and refine documentation using AI assistance.
+- **Commit Docs**: Manage and publish documentation for your applications.
+
+- **Docs Dashboard**: View and manage all your documentation in one place.
+-
+
+- **MDX Support**: Write JavaScript and React components in your Markdown files.
+-
-- `bench init commit`
-- `bench get-app https://github.com/The-commit-company/commit`
-- `bench new-site `
-- `bench --site install-app commit`
+- **Customizable**: Tailor the tool to fit your specific needs and preferences.
diff --git a/commit/__init__.py b/commit/__init__.py
index 88a7e16..5becc17 100644
--- a/commit/__init__.py
+++ b/commit/__init__.py
@@ -1,3 +1 @@
-
-__version__ = '1.0.0'
-
+__version__ = "1.0.0"
diff --git a/commit/api/api_explorer.py b/commit/api/api_explorer.py
index 9d9a830..fe04490 100644
--- a/commit/api/api_explorer.py
+++ b/commit/api/api_explorer.py
@@ -1,18 +1,28 @@
-import frappe
import json
import os
+
+import frappe
+
from commit.commit.code_analysis.apis import find_all_occurrences_of_whitelist
@frappe.whitelist(allow_guest=True)
def get_apis_for_project(project_branch: str):
- '''
- Gets the Project Branch document with the organization and app name
- '''
- branch_doc = frappe.get_doc("Commit Project Branch", project_branch)
+ """
+ Gets the Project Branch document with the organization and app name
+ """
+ branch_doc = frappe.get_cached_doc("Commit Project Branch", project_branch)
- apis = json.loads(branch_doc.whitelisted_apis).get("apis", []) if branch_doc.whitelisted_apis else []
- documentation = json.loads(branch_doc.documentation).get("apis", []) if branch_doc.documentation else []
+ apis = (
+ json.loads(branch_doc.whitelisted_apis).get("apis", [])
+ if branch_doc.whitelisted_apis
+ else []
+ )
+ documentation = (
+ json.loads(branch_doc.documentation).get("apis", [])
+ if branch_doc.documentation
+ else []
+ )
for api in apis:
# find the documentation for the api whose function_name equals to name and path same as path
for doc in documentation:
@@ -20,21 +30,30 @@ def get_apis_for_project(project_branch: str):
try:
doc = json.loads(doc)
except json.JSONDecodeError:
- frappe.log_error(f"Invalid JSON format in documentation entry: {doc}", "Commit Docs Error")
+ frappe.log_error(
+ f"Invalid JSON format in documentation entry: {doc}",
+ "Commit Docs Error",
+ )
continue
- if doc.get("function_name") == api.get("name") and doc.get("path") == api.get("api_path"):
+ if doc.get("function_name") == api.get("name") and doc.get(
+ "path"
+ ) == api.get("api_path"):
api["documentation"] = doc.get("documentation")
api["last_updated"] = doc.get("last_updated")
api["is_published"] = doc.get("is_published", 0)
api["published_on"] = doc.get("published_on", None)
api["published_by"] = doc.get("published_by", None)
- api['publish_id'] = doc.get('publish_id', None)
- api['published_route'] = doc.get('published_route', None)
+ api["publish_id"] = doc.get("publish_id", None)
+ api["published_route"] = doc.get("published_route", None)
break
-
- app_name, organization, app_logo = frappe.db.get_value("Commit Project", branch_doc.project, ["app_name", "org", "image"])
- organization_name, org_logo, organization_id = frappe.db.get_value("Commit Organization", organization, ["organization_name", "image", "name"])
+
+ app_name, organization, app_logo = frappe.db.get_value(
+ "Commit Project", branch_doc.project, ["app_name", "org", "image"]
+ )
+ organization_name, org_logo, organization_id = frappe.db.get_value(
+ "Commit Organization", organization, ["organization_name", "image", "name"]
+ )
return {
"apis": apis,
@@ -46,30 +65,36 @@ def get_apis_for_project(project_branch: str):
"branch_name": branch_doc.branch_name,
"project_branch": branch_doc.name,
"last_updated": branch_doc.last_fetched,
- 'path_to_folder':branch_doc.path_to_folder
+ "path_to_folder": branch_doc.path_to_folder,
}
@frappe.whitelist(allow_guest=True)
-def get_file_content_from_path(project_branch: str, file_path: str,block_start: int, block_end: int,viewer_type: str):
- '''
- Gets the Project Branch document with the organization and app name
- '''
+def get_file_content_from_path(
+ project_branch: str,
+ file_path: str,
+ block_start: int,
+ block_end: int,
+ viewer_type: str,
+):
+ """
+ Gets the Project Branch document with the organization and app name
+ """
if viewer_type == "project":
- branch_doc = frappe.get_doc("Commit Project Branch", project_branch)
+ branch_doc = frappe.get_cached_doc("Commit Project Branch", project_branch)
- api_data = json.loads(branch_doc.whitelisted_apis)['apis']
+ api_data = json.loads(branch_doc.whitelisted_apis)["apis"]
else:
app_path = frappe.get_app_path(project_branch)
# remove last part of the path which is the app name
- app_path = app_path.rsplit('/', 1)[0]
- api_data = find_all_occurrences_of_whitelist(app_path,project_branch)
+ app_path = app_path.rsplit("/", 1)[0]
+ api_data = find_all_occurrences_of_whitelist(app_path, project_branch)
found = False
for api in api_data:
- if api['file'] == file_path:
+ if api["file"] == file_path:
found = True
break
@@ -77,12 +102,10 @@ def get_file_content_from_path(project_branch: str, file_path: str,block_start:
frappe.throw("File not found-")
else:
if os.path.isfile(file_path):
- file_content = open(file_path, 'r')
+ file_content = open(file_path, "r")
file_content = file_content.readlines()
# fetch the block
file_content = file_content[block_start:block_end]
- return {
- "file_content": file_content
- }
+ return {"file_content": file_content}
else:
- frappe.throw("File not found")
\ No newline at end of file
+ frappe.throw("File not found")
diff --git a/commit/api/bruno.py b/commit/api/bruno.py
index 88eb96a..3cd56d8 100644
--- a/commit/api/bruno.py
+++ b/commit/api/bruno.py
@@ -1,12 +1,13 @@
import frappe
+
@frappe.whitelist(allow_guest=True)
-def generate_bruno_file(data, return_type='download'):
+def generate_bruno_file(data, return_type="download"):
request_data = frappe.parse_json(data)
"""
Generates .bru file content for a single request based on the provided request data.
-
- :param request_data: A dictionary containing request information.
+
+ :param request_data: A dictionary containing request information.
Expected keys are:
- name: The name of the request.
- arguments: A list of dictionaries containing argument information.
@@ -24,15 +25,21 @@ def generate_bruno_file(data, return_type='download'):
:return: A dictionary where keys are request types and values are the content of the corresponding .bru files.
"""
base_url_template = "{{baseUrl}}/api/method"
-
+
def format_name(name):
- return ' '.join(word.capitalize() for word in name.split('_'))
-
- name = format_name(request_data.get('name', 'Request'))
- api_path = request_data.get('api_path', '')
- request_types = request_data.get('request_types', ['GET']) or ['GET'] # Default to GET if empty
- params = {arg['argument']: arg['default'] for arg in request_data.get('arguments', []) if arg['argument']}
-
+ return " ".join(word.capitalize() for word in name.split("_"))
+
+ name = format_name(request_data.get("name", "Request"))
+ api_path = request_data.get("api_path", "")
+ request_types = request_data.get("request_types", ["GET"]) or [
+ "GET"
+ ] # Default to GET if empty
+ params = {
+ arg["argument"]: arg["default"]
+ for arg in request_data.get("arguments", [])
+ if arg["argument"]
+ }
+
bru_files = {}
request_type = request_types[0]
@@ -40,30 +47,34 @@ def format_name(name):
request_type_upper = request_type.upper()
request_type_lower = request_type.lower()
url = f"{base_url_template}/{api_path}"
-
- query_string = '&'.join([f'{k}={v}' for k, v in params.items() if v])
- full_url = f'{url}?{query_string}' if query_string else url
+
+ query_string = "&".join([f"{k}={v}" for k, v in params.items() if v])
+ full_url = f"{url}?{query_string}" if query_string else url
bru_content = []
# Meta section
- bru_content.append(f'meta {{\n name: {name}\n type: http\n seq: {seq}\n}}\n')
+ bru_content.append(f"meta {{\n name: {name}\n type: http\n seq: {seq}\n}}\n")
# Request section
- bru_content.append(f'{request_type_lower} {{\n url: {full_url}\n body: none\n auth: none\n}}\n')
+ bru_content.append(
+ f"{request_type_lower} {{\n url: {full_url}\n body: none\n auth: none\n}}\n"
+ )
# Params section
if params:
- bru_content.append(f'params:query {{\n')
+ bru_content.append(f"params:query {{\n")
for k, v in params.items():
if v:
- bru_content.append(f' {k}: {v}\n')
- bru_content.append('}\n')
-
- bru_files[request_type_upper] = '\n'.join(bru_content)
- if return_type == 'download':
- frappe.local.response.filename = f'{name} {request_type}.bru' if len(request_types) > 1 else f'{name}.bru'
+ bru_content.append(f" {k}: {v}\n")
+ bru_content.append("}\n")
+
+ bru_files[request_type_upper] = "\n".join(bru_content)
+ if return_type == "download":
+ frappe.local.response.filename = (
+ f"{name} {request_type}.bru" if len(request_types) > 1 else f"{name}.bru"
+ )
frappe.local.response.filecontent = bru_files[request_type_upper]
- frappe.local.response.type = 'download'
+ frappe.local.response.type = "download"
else:
- return bru_files[request_type_upper]
\ No newline at end of file
+ return bru_files[request_type_upper]
diff --git a/commit/api/code_analysis.py b/commit/api/code_analysis.py
index ef81590..b497f50 100644
--- a/commit/api/code_analysis.py
+++ b/commit/api/code_analysis.py
@@ -1,20 +1,29 @@
import frappe
-from commit.api.github import get_file_in_repo, get_all_files_in_repo, search_for_file_in_repo
-from commit.utils.conversions import convert_module_name
+
+from commit.api.github import (
+ get_all_files_in_repo,
+ get_file_in_repo,
+ search_for_file_in_repo,
+)
from commit.utils.api_analysis import get_api_details_from_file_contents
+from commit.utils.conversions import convert_module_name
+
access_token = "*"
+
@frappe.whitelist(allow_guest=True)
def get_name_of_app(organization, repo):
- '''
+ """
Get name of app from repo
- '''
+ """
file_type = None
app_name = None
root_files = get_all_files_in_repo(access_token, organization, repo)
if type(root_files) == dict and root_files.get("message", "") == "Not Found":
- return frappe.throw(f'Repository {repo} not found in organization {organization}')
-
+ return frappe.throw(
+ f"Repository {repo} not found in organization {organization}"
+ )
+
for file in root_files:
if file["name"] == "pyproject.toml":
file_type = "pyproject.toml"
@@ -22,62 +31,83 @@ def get_name_of_app(organization, repo):
elif file["name"] == "setup.py":
file_type = "setup.py"
break
-
+
if file_type == "pyproject.toml":
app_name = get_app_name_from_pyproject_toml(organization, repo)
elif file_type == "setup.py":
- app_name = get_app_name_from_setup_py(organization, repo)
+ app_name = get_app_name_from_setup_py(organization, repo)
return app_name
+
def get_app_name_from_setup_py(organization, repo):
- '''
+ """
Get app name from setup.py
- '''
+ """
setup_py = get_file_in_repo(access_token, organization, repo, "setup.py")
- app_name = setup_py.split("name=")[1].split(",")[0].strip().replace("'", "").replace('"', '')
+ app_name = (
+ setup_py.split("name=")[1]
+ .split(",")[0]
+ .strip()
+ .replace("'", "")
+ .replace('"', "")
+ )
return app_name
def get_app_name_from_pyproject_toml(organization, repo):
- '''
+ """
Get app name from pyproject.toml
- '''
- pyproject_toml = get_file_in_repo(access_token, organization, repo, "pyproject.toml")
+ """
+ pyproject_toml = get_file_in_repo(
+ access_token, organization, repo, "pyproject.toml"
+ )
split_result = pyproject_toml.split("name = ")
if len(split_result) > 1:
- app_name = pyproject_toml.split("name = ")[1].split("\n")[0].strip().replace("'", "").replace('"', '')
+ app_name = (
+ pyproject_toml.split("name = ")[1]
+ .split("\n")[0]
+ .strip()
+ .replace("'", "")
+ .replace('"', "")
+ )
else:
app_name = get_app_name_from_setup_py(organization, repo)
return app_name
+
# TODO: Function to get app version
# def get_app_version
# TODO: Function to get list of all dependencies in Python app
# def get_list_of_dependencies
+
@frappe.whitelist(allow_guest=True)
def get_list_of_modules(organization, repo, app_name):
- '''
+ """
Get list of modules for a Frappe app
- '''
- modules = get_file_in_repo(access_token, organization, repo, app_name + "/modules.txt")
+ """
+ modules = get_file_in_repo(
+ access_token, organization, repo, app_name + "/modules.txt"
+ )
return modules.split("\n")
@frappe.whitelist(allow_guest=True)
def get_list_of_doctypes_in_module(organization, repo, app_name, module: str):
- '''
+ """
Get list of doctypes in a module
- '''
+ """
module_pathname = convert_module_name(module)
query = f"path:{app_name}/{module_pathname}/doctype+{module} in:file"
- search_results = search_for_file_in_repo(access_token, organization, repo, query, 'json')
+ search_results = search_for_file_in_repo(
+ access_token, organization, repo, query, "json"
+ )
if search_results.get("total_count", 0) == 0:
return []
-
+
doctypes = []
for result in search_results["items"]:
path = result["path"]
@@ -85,53 +115,48 @@ def get_list_of_doctypes_in_module(organization, repo, app_name, module: str):
doctype_json = frappe.parse_json(doctype_json_content)
if doctype_json.get("doctype", "") == "DocType":
doctypes.append(doctype_json)
- return {
- "module": module,
- "doctypes": doctypes,
- "count": len(doctypes)
- }
+ return {"module": module, "doctypes": doctypes, "count": len(doctypes)}
+
@frappe.whitelist(allow_guest=True)
def get_customized_doctypes_in_module(organization, repo, app_name, module: str):
- '''
+ """
Get list of all customized doctypes for a Frappe app
- '''
+ """
module_pathname = convert_module_name(module)
query = f"path:{app_name}/{module_pathname}/custom+custom_fields in:file"
- search_results = search_for_file_in_repo(access_token, organization, repo, query, 'json')
+ search_results = search_for_file_in_repo(
+ access_token, organization, repo, query, "json"
+ )
if search_results.get("total_count", 0) == 0:
return []
-
+
doctypes = []
for result in search_results["items"]:
path = result["path"]
doctype_json_content = get_file_in_repo(access_token, organization, repo, path)
doctype_json = frappe.parse_json(doctype_json_content)
doctypes.append(doctype_json)
- return {
- "module": module,
- "doctypes": doctypes,
- "count": len(doctypes)
- }
+ return {"module": module, "doctypes": doctypes, "count": len(doctypes)}
@frappe.whitelist(allow_guest=True)
def get_all_whitelisted_api_in_app(organization, repo):
- '''
+ """
Get list of all whitelisted API in a Frappe app with:
1. Type
2. Path
3. Method name
4. Arguments
5. Python code snippet
- '''
+ """
query = f"@frappe.whitelist in:file+language:python"
search_results = search_for_file_in_repo(access_token, organization, repo, query)
if search_results.get("total_count", 0) == 0:
return []
-
+
apis = []
for result in search_results["items"]:
path = result["path"]
@@ -145,8 +170,5 @@ def get_all_whitelisted_api_in_app(organization, repo):
# api = get_whitelisted_api_in_file(path)
# if api:
# apis.append(api)
-
- return {
- "count": len(apis),
- "apis": apis
- }
\ No newline at end of file
+
+ return {"count": len(apis), "apis": apis}
diff --git a/commit/api/commit_project/commit_project.py b/commit/api/commit_project/commit_project.py
index 95f568b..f55a152 100644
--- a/commit/api/commit_project/commit_project.py
+++ b/commit/api/commit_project/commit_project.py
@@ -7,15 +7,47 @@ def get_project_list_with_branches():
Get list of projects with branches for each organization
"""
- organizations = frappe.get_all("Commit Organization", fields=[
- "name", 'organization_name', 'github_org', 'image', 'about', 'creation'])
+ organizations = frappe.get_all(
+ "Commit Organization",
+ fields=[
+ "name",
+ "organization_name",
+ "github_org",
+ "image",
+ "about",
+ "creation",
+ ],
+ )
for organization in organizations:
- projects = frappe.get_all("Commit Project", filters={
- "org": organization.get("name")}, fields=["name", "display_name", "repo_name", "app_name", "image", "banner_image", "path_to_folder", 'description'], order_by="creation desc")
+ projects = frappe.get_all(
+ "Commit Project",
+ filters={"org": organization.get("name")},
+ fields=[
+ "name",
+ "display_name",
+ "repo_name",
+ "app_name",
+ "image",
+ "banner_image",
+ "path_to_folder",
+ "description",
+ ],
+ order_by="creation desc",
+ )
# organization["projects"] = projects
for project in projects:
- branches = frappe.get_all("Commit Project Branch", filters={"project": project.get(
- "name")}, fields=["branch_name", "last_fetched", "modules", "whitelisted_apis", "name", "frequency"])
+ branches = frappe.get_all(
+ "Commit Project Branch",
+ filters={"project": project.get("name")},
+ fields=[
+ "branch_name",
+ "last_fetched",
+ "modules",
+ "whitelisted_apis",
+ "name",
+ "frequency",
+ ],
+ )
project["branches"] = branches
organization["projects"] = projects
diff --git a/commit/api/convert_to_webp.py b/commit/api/convert_to_webp.py
index f9406f9..33b5e38 100644
--- a/commit/api/convert_to_webp.py
+++ b/commit/api/convert_to_webp.py
@@ -1,101 +1,108 @@
-import frappe
-from frappe.core.doctype.file.utils import delete_file
+import os
from urllib.parse import unquote
+
+import frappe
import requests
-from PIL import Image
from frappe.core.doctype.file.file import get_local_image
+from frappe.core.doctype.file.utils import delete_file
from frappe.model.document import Document
-import os
+from PIL import Image
+
@frappe.whitelist()
-def convert_to_webp(image_url: str | None = None, file_doc: Document | None = None) -> str:
- """BETA: Convert image to webp format"""
-
- CONVERTIBLE_IMAGE_EXTENSIONS = ["png", "jpeg", "jpg"]
-
- def can_convert_image(extn):
- return extn.lower() in CONVERTIBLE_IMAGE_EXTENSIONS
-
- def get_extension(filename):
- return filename.split(".")[-1].lower()
-
- def convert_and_save_image(image, path):
- image.save(path, "WEBP")
- return path
-
- def update_file_doc_with_webp(file_doc, image, extn):
- webp_path = file_doc.get_full_path().replace(extn, "webp")
- convert_and_save_image(image, webp_path)
- delete_file(file_doc.get_full_path())
- file_doc.file_url = f"{file_doc.file_url.replace(extn, 'webp')}"
- file_doc.save()
- return file_doc.file_url
-
- def create_new_webp_file_doc(file_url, image, extn):
- files = frappe.get_all("File", filters={"file_url": file_url}, fields=["name"], limit=1)
- if files:
- _file = frappe.get_doc("File", files[0].name)
- webp_path = _file.get_full_path().replace(extn, "webp")
- convert_and_save_image(image, webp_path)
- new_file = frappe.copy_doc(_file)
- new_file.file_name = f"{_file.file_name.replace(extn, 'webp')}"
- new_file.file_url = f"{_file.file_url.replace(extn, 'webp')}"
- new_file.save()
- return new_file.file_url
- return file_url
-
- def handle_image_from_url(image_url):
- image_url = unquote(image_url)
- response = requests.get(image_url)
- image = Image.open(io.BytesIO(response.content))
- filename = image_url.split("/")[-1]
- extn = get_extension(filename)
- if can_convert_image(extn):
- _file = frappe.get_doc(
- {
- "doctype": "File",
- "file_name": f"{filename.replace(extn, 'webp')}",
- "file_url": f"/files/{filename.replace(extn, 'webp')}",
- }
- )
- webp_path = _file.get_full_path()
- convert_and_save_image(image, webp_path)
- _file.save()
- return _file.file_url
- return image_url
-
- if not image_url and not file_doc:
- return ""
-
- if file_doc:
- if file_doc.file_url.startswith("/files"):
- image, filename, extn = get_local_image(file_doc.file_url)
- if can_convert_image(extn):
- return update_file_doc_with_webp(file_doc, image, extn)
- if file_doc.file_url.startswith("/private"):
- image, filename, extn = get_local_image(file_doc.file_url)
- if can_convert_image(extn):
- return update_file_doc_with_webp(file_doc, image, extn)
-
- return file_doc.file_url
-
- if image_url.startswith("/files"):
- image, filename, extn = get_local_image(image_url)
- if can_convert_image(extn):
- return create_new_webp_file_doc(image_url, image, extn)
- return image_url
- if image_url.startswith("/private"):
- image, filename, extn = get_local_image(image_url)
- if can_convert_image(extn):
- return create_new_webp_file_doc(image_url, image, extn)
- return image_url
- if image_url.startswith("http"):
- return handle_image_from_url(image_url)
-
- return image_url
-
-def save_webp_image(doctype:str,docname:str,image_field:str):
- file_url = frappe.db.get_value(doctype,docname,image_field)
- if file_url:
- webp_url = convert_to_webp(file_url)
- frappe.db.set_value(doctype,docname,image_field,webp_url)
\ No newline at end of file
+def convert_to_webp(
+ image_url: str | None = None, file_doc: Document | None = None
+) -> str:
+ """BETA: Convert image to webp format"""
+
+ CONVERTIBLE_IMAGE_EXTENSIONS = ["png", "jpeg", "jpg"]
+
+ def can_convert_image(extn):
+ return extn.lower() in CONVERTIBLE_IMAGE_EXTENSIONS
+
+ def get_extension(filename):
+ return filename.split(".")[-1].lower()
+
+ def convert_and_save_image(image, path):
+ image.save(path, "WEBP")
+ return path
+
+ def update_file_doc_with_webp(file_doc, image, extn):
+ webp_path = file_doc.get_full_path().replace(extn, "webp")
+ convert_and_save_image(image, webp_path)
+ delete_file(file_doc.get_full_path())
+ file_doc.file_url = f"{file_doc.file_url.replace(extn, 'webp')}"
+ file_doc.save()
+ return file_doc.file_url
+
+ def create_new_webp_file_doc(file_url, image, extn):
+ files = frappe.get_all(
+ "File", filters={"file_url": file_url}, fields=["name"], limit=1
+ )
+ if files:
+ _file = frappe.get_cached_doc("File", files[0].name)
+ webp_path = _file.get_full_path().replace(extn, "webp")
+ convert_and_save_image(image, webp_path)
+ new_file = frappe.copy_doc(_file)
+ new_file.file_name = f"{_file.file_name.replace(extn, 'webp')}"
+ new_file.file_url = f"{_file.file_url.replace(extn, 'webp')}"
+ new_file.save()
+ return new_file.file_url
+ return file_url
+
+ def handle_image_from_url(image_url):
+ image_url = unquote(image_url)
+ response = requests.get(image_url)
+ image = Image.open(io.BytesIO(response.content))
+ filename = image_url.split("/")[-1]
+ extn = get_extension(filename)
+ if can_convert_image(extn):
+ _file = frappe.get_cached_doc(
+ {
+ "doctype": "File",
+ "file_name": f"{filename.replace(extn, 'webp')}",
+ "file_url": f"/files/{filename.replace(extn, 'webp')}",
+ }
+ )
+ webp_path = _file.get_full_path()
+ convert_and_save_image(image, webp_path)
+ _file.save()
+ return _file.file_url
+ return image_url
+
+ if not image_url and not file_doc:
+ return ""
+
+ if file_doc:
+ if file_doc.file_url.startswith("/files"):
+ image, filename, extn = get_local_image(file_doc.file_url)
+ if can_convert_image(extn):
+ return update_file_doc_with_webp(file_doc, image, extn)
+ if file_doc.file_url.startswith("/private"):
+ image, filename, extn = get_local_image(file_doc.file_url)
+ if can_convert_image(extn):
+ return update_file_doc_with_webp(file_doc, image, extn)
+
+ return file_doc.file_url
+
+ if image_url.startswith("/files"):
+ image, filename, extn = get_local_image(image_url)
+ if can_convert_image(extn):
+ return create_new_webp_file_doc(image_url, image, extn)
+ return image_url
+ if image_url.startswith("/private"):
+ image, filename, extn = get_local_image(image_url)
+ if can_convert_image(extn):
+ return create_new_webp_file_doc(image_url, image, extn)
+ return image_url
+ if image_url.startswith("http"):
+ return handle_image_from_url(image_url)
+
+ return image_url
+
+
+def save_webp_image(doctype: str, docname: str, image_field: str):
+ file_url = frappe.db.get_value(doctype, docname, image_field)
+ if file_url:
+ webp_url = convert_to_webp(file_url)
+ frappe.db.set_value(doctype, docname, image_field, webp_url)
diff --git a/commit/api/erd_viewer.py b/commit/api/erd_viewer.py
index 3826673..0d9ade4 100644
--- a/commit/api/erd_viewer.py
+++ b/commit/api/erd_viewer.py
@@ -1,30 +1,31 @@
+import json
+
import frappe
+
from commit.commit.code_analysis.schema_builder import get_schema_from_doctypes_json
-import json
@frappe.whitelist(allow_guest=True)
def get_doctype_json(project_branch: str, doctype: str):
- '''
+ """
Get doctype json from a project branch
- '''
- project_branch = frappe.get_cached_doc(
- "Commit Project Branch", project_branch)
+ """
+ project_branch = frappe.get_cached_doc("Commit Project Branch", project_branch)
doctype_json = project_branch.get_doctype_json(doctype)
return doctype_json
@frappe.whitelist(allow_guest=True)
def get_erd_schema_for_module(project_branch: str, module: str):
- '''
+ """
Get ERD schema for a module
- '''
+ """
- project_branch = frappe.get_cached_doc(
- "Commit Project Branch", project_branch)
+ project_branch = frappe.get_cached_doc("Commit Project Branch", project_branch)
module_doctypes = project_branch.get_doctypes_in_module(module)
schema = get_erd_schema_for_doctypes(
- project_branch.name, json.dumps(module_doctypes))
+ project_branch.name, json.dumps(module_doctypes)
+ )
return schema
@@ -34,47 +35,48 @@ def get_erd_schema_for_doctypes(project_branch: list, doctypes):
branch_doctypes = {}
doctype_list = []
for doctype in doctypes:
- if doctype['project_branch'] not in branch_doctypes:
- branch_doctypes[doctype['project_branch']] = []
- branch_doctypes[doctype['project_branch']].append(doctype['doctype'])
- doctype_list.append(doctype['doctype'])
+ if doctype["project_branch"] not in branch_doctypes:
+ branch_doctypes[doctype["project_branch"]] = []
+ branch_doctypes[doctype["project_branch"]].append(doctype["doctype"])
+ doctype_list.append(doctype["doctype"])
doctype_jsons = []
for project_branch, doctypes in branch_doctypes.items():
project_branch_doc = frappe.get_cached_doc(
- "Commit Project Branch", project_branch)
+ "Commit Project Branch", project_branch
+ )
for doctype in doctypes:
doctype_json = project_branch_doc.get_doctype_json(doctype)
doctype_jsons.append(doctype_json)
- schema = get_schema_from_doctypes_json({
- 'doctypes': doctype_jsons,
- 'doctype_names': doctype_list
- })
+ schema = get_schema_from_doctypes_json(
+ {"doctypes": doctype_jsons, "doctype_names": doctype_list}
+ )
return schema
+
@frappe.whitelist()
-def get_meta_erd_schema_for_doctypes(doctypes:list):
- '''
+def get_meta_erd_schema_for_doctypes(doctypes: list):
+ """
Get ERD schema for a list of doctypes
- '''
+ """
doctype_jsons = []
for doctype in doctypes:
doctype_json = frappe.get_meta(doctype)
doctype_jsons.append(doctype_json)
- schema = get_schema_from_doctypes_json({
- 'doctypes': doctype_jsons,
- 'doctype_names': doctypes
- })
+ schema = get_schema_from_doctypes_json(
+ {"doctypes": doctype_jsons, "doctype_names": doctypes}
+ )
return schema
+
@frappe.whitelist()
def get_meta_for_doctype(doctype):
- '''
+ """
Get meta for a doctype
- '''
- return frappe.get_meta(doctype)
\ No newline at end of file
+ """
+ return frappe.get_meta(doctype)
diff --git a/commit/api/generate_documentation.py b/commit/api/generate_documentation.py
index 8687f87..ce712e5 100644
--- a/commit/api/generate_documentation.py
+++ b/commit/api/generate_documentation.py
@@ -1,13 +1,17 @@
import json
import re
-from commit.commit.doctype.open_ai_settings.open_ai_settings import open_ai_call
+
import frappe
+
from commit.api.api_explorer import get_file_content_from_path
+from commit.commit.doctype.open_ai_settings.open_ai_settings import open_ai_call
def generate_docs_for_apis(api_definitions):
- max_tokens_per_request = 1800 # This is a safe limit to avoid hitting the max token limit
+ max_tokens_per_request = (
+ 1800 # This is a safe limit to avoid hitting the max token limit
+ )
chunks = chunk_data(api_definitions, max_tokens_per_request)
all_docs = []
@@ -25,6 +29,7 @@ def estimate_tokens(text):
# Estimate tokens based on average character count
return len(text) // 4
+
def chunk_data(data, max_tokens):
chunks = []
current_chunk = []
@@ -47,6 +52,7 @@ def chunk_data(data, max_tokens):
return chunks
+
def clean_response(response_text):
# Remove non-JSON parts using regex
cleaned_text = re.sub(r"```json|```", "", response_text.strip())
@@ -69,7 +75,7 @@ def generate_docs_for_chunk(api_chunk):
"The response should be a valid JSON list of objects formatted as follows: "
"{function_name: , path: , last_updated:, documentation: }.\n"
"Ensure the response is in valid JSON format only, enclosed in triple backticks, and does not include `---`."
- )
+ ),
}
]
last_updated = frappe.utils.now()
@@ -104,7 +110,8 @@ def generate_docs_for_chunk(api_chunk):
return []
# return cleaned_response
-def generate_documentation_for_api_snippet(api_path:str,code_snippet:str):
+
+def generate_documentation_for_api_snippet(api_path: str, code_snippet: str):
messages = [
{
"role": "system",
@@ -119,10 +126,10 @@ def generate_documentation_for_api_snippet(api_path:str,code_snippet:str):
"The response should be a valid JSON formatted as follows: "
"{function_name: , path: , last_updated:, documentation: }.\n"
"Ensure the response is in valid JSON format only, and does not include `---`."
- )
+ ),
}
]
-
+
user_message = f"api path: {api_path}, last_updated:{frappe.utils.now()}, code:\n{code_snippet}"
if not code_snippet:
return []
@@ -154,14 +161,27 @@ def generate_documentation_for_api_snippet(api_path:str,code_snippet:str):
print("Second JSON Decode Error:", e)
return []
+
@frappe.whitelist()
-def get_documentation_for_api(project_branch: str, file_path: str,block_start: int, block_end: int,endpoint:str,viewer_type:str = 'app'):
- code_snippet = get_file_content_from_path(project_branch, file_path,block_start, block_end,viewer_type)
+def get_documentation_for_api(
+ project_branch: str,
+ file_path: str,
+ block_start: int,
+ block_end: int,
+ endpoint: str,
+ viewer_type: str = "app",
+):
+ code_snippet = get_file_content_from_path(
+ project_branch, file_path, block_start, block_end, viewer_type
+ )
api_path = endpoint
return generate_documentation_for_api_snippet(api_path, code_snippet)
+
@frappe.whitelist()
-def save_documentation(project_branch:str,endpoint:str,documentation:str,viewer_type:str = 'app'):
+def save_documentation(
+ project_branch: str, endpoint: str, documentation: str, viewer_type: str = "app"
+):
# Save the documentation to the project branch
# 1. Check for viewer_type app or project
# 2. If viewer_type is app, then check the document is already present in Commit Branch Documentation doctype
@@ -177,9 +197,12 @@ def save_documentation(project_branch:str,endpoint:str,documentation:str,viewer_
else:
save_documentation_for_project_branch(project_branch, endpoint, documentation)
-def save_documentation_for_project_branch(project_branch:str,endpoint:str,documentation:str):
- doc = frappe.get_doc("Commit Project Branch", project_branch)
+def save_documentation_for_project_branch(
+ project_branch: str, endpoint: str, documentation: str
+):
+
+ doc = frappe.get_cached_doc("Commit Project Branch", project_branch)
docs = json.loads(doc.documentation) if doc.documentation else {}
apis = docs.get("apis", [])
@@ -187,26 +210,34 @@ def save_documentation_for_project_branch(project_branch:str,endpoint:str,docume
# loop over apis and check if function_name and path matches then update the documentation else create a new dict and append to the documentation
found = False
for api in apis:
- if api.get("function_name") == endpoint.split(".")[-1] and api.get("path") == endpoint:
+ if (
+ api.get("function_name") == endpoint.split(".")[-1]
+ and api.get("path") == endpoint
+ ):
api["documentation"] = documentation
api["last_updated"] = frappe.utils.now()
found = True
break
if not found:
- apis.append({
- "function_name": endpoint.split(".")[-1],
- "path": endpoint,
- "last_updated": frappe.utils.now(),
- "documentation": documentation
- })
-
+ apis.append(
+ {
+ "function_name": endpoint.split(".")[-1],
+ "path": endpoint,
+ "last_updated": frappe.utils.now(),
+ "documentation": documentation,
+ }
+ )
+
doc.documentation = json.dumps({"apis": apis})
doc.save()
-def save_documentation_for_site_app(project_branch:str,endpoint:str,documentation:str):
- if frappe.db.exists("Commit Branch Documentation",project_branch):
- doc = frappe.get_doc("Commit Branch Documentation", project_branch)
+def save_documentation_for_site_app(
+ project_branch: str, endpoint: str, documentation: str
+):
+
+ if frappe.db.exists("Commit Branch Documentation", project_branch):
+ doc = frappe.get_cached_doc("Commit Branch Documentation", project_branch)
docs = json.loads(doc.documentation) if doc.documentation else {}
apis = docs.get("apis", [])
@@ -214,28 +245,39 @@ def save_documentation_for_site_app(project_branch:str,endpoint:str,documentatio
# loop over apis and check if function_name and path matches then update the documentation else create a new dict and append to the documentation
found = False
for api in apis:
- if api.get("function_name") == endpoint.split(".")[-1] and api.get("path") == endpoint:
+ if (
+ api.get("function_name") == endpoint.split(".")[-1]
+ and api.get("path") == endpoint
+ ):
api["documentation"] = documentation
api["last_updated"] = frappe.utils.now()
found = True
break
if not found:
- apis.append({
- "function_name": endpoint.split(".")[-1],
- "path": endpoint,
- "last_updated": frappe.utils.now(),
- "documentation": documentation
- })
+ apis.append(
+ {
+ "function_name": endpoint.split(".")[-1],
+ "path": endpoint,
+ "last_updated": frappe.utils.now(),
+ "documentation": documentation,
+ }
+ )
doc.documentation = json.dumps({"apis": apis})
doc.save()
else:
# Create a new document and append the documentation
doc = frappe.new_doc("Commit Branch Documentation")
doc.app = project_branch
- doc.documentation = json.dumps({"apis": [{
- "function_name": endpoint.split(".")[-1],
- "path": endpoint,
- "last_updated": frappe.utils.now(),
- "documentation": documentation
- }]})
- doc.save()
\ No newline at end of file
+ doc.documentation = json.dumps(
+ {
+ "apis": [
+ {
+ "function_name": endpoint.split(".")[-1],
+ "path": endpoint,
+ "last_updated": frappe.utils.now(),
+ "documentation": documentation,
+ }
+ ]
+ }
+ )
+ doc.save()
diff --git a/commit/api/get_commands.py b/commit/api/get_commands.py
index f4d84f6..f4635d3 100644
--- a/commit/api/get_commands.py
+++ b/commit/api/get_commands.py
@@ -1,19 +1,24 @@
import importlib
+import os
import sys
import traceback
-import os
+
import frappe
from frappe.utils.bench_helper import get_app_commands
+
@frappe.whitelist(allow_guest=True)
def get_project_app_commands(app: str, app_path: str = None) -> dict:
- '''
- Gets the commands for the app
- '''
- if not app_path or app_path == '':
+ """
+ Gets the commands for the app
+ """
+ if not app_path or app_path == "":
# Check the permissions of the user
if not is_system_manager():
- return frappe.throw('You do not have permission to access this resource', frappe.PermissionError)
+ return frappe.throw(
+ "You do not have permission to access this resource",
+ frappe.PermissionError,
+ )
return get_site_app_commands(app)
else:
ret = []
@@ -21,7 +26,7 @@ def get_project_app_commands(app: str, app_path: str = None) -> dict:
if app_path:
# Add the app's directory to the Python path
sys.path.append(app_path)
-
+
app_command_module = importlib.import_module(f"{app}.commands")
except ModuleNotFoundError as e:
if e.name == f"{app}.commands":
@@ -35,26 +40,28 @@ def get_project_app_commands(app: str, app_path: str = None) -> dict:
if app_path:
# Remove the app's directory from the Python path to avoid side effects
sys.path.remove(app_path)
-
+
command_list = []
- if hasattr(app_command_module, 'get_commands') and callable(getattr(app_command_module, 'get_commands')):
+ if hasattr(app_command_module, "get_commands") and callable(
+ getattr(app_command_module, "get_commands")
+ ):
commands_from_function = app_command_module.get_commands()
if commands_from_function:
for command_instance in commands_from_function:
- help_text = getattr(command_instance, 'help', 'No help text available')
- name = getattr(command_instance, 'name', [])
- obj = {
- 'name': name,
- 'help': help_text
- }
+ help_text = getattr(
+ command_instance, "help", "No help text available"
+ )
+ name = getattr(command_instance, "name", [])
+ obj = {"name": name, "help": help_text}
command_list.append(obj)
return command_list
+
@frappe.whitelist()
def get_site_app_commands(app: str) -> dict:
try:
app_command_module = importlib.import_module(f"{app}.commands")
- # Call get_commands if it is a callable
+ # Call get_commands if it is a callable
except ModuleNotFoundError as e:
if e.name == f"{app}.commands":
return []
@@ -62,20 +69,18 @@ def get_site_app_commands(app: str) -> dict:
return []
command_list = []
- if hasattr(app_command_module, 'commands'):
+ if hasattr(app_command_module, "commands"):
commands_from_function = app_command_module.commands
if commands_from_function:
for command_instance in commands_from_function:
- help_text = getattr(command_instance, 'help', 'No help text available')
- name = getattr(command_instance, 'name', [])
- obj = {
- 'name': name,
- 'help': help_text
- }
+ help_text = getattr(command_instance, "help", "No help text available")
+ name = getattr(command_instance, "name", [])
+ obj = {"name": name, "help": help_text}
command_list.append(obj)
return command_list
+
def is_system_manager():
user = frappe.session.user
roles = frappe.get_roles(user)
- return 'System Manager' in roles
+ return "System Manager" in roles
diff --git a/commit/api/github.py b/commit/api/github.py
index 000b38a..28e8b31 100644
--- a/commit/api/github.py
+++ b/commit/api/github.py
@@ -2,59 +2,84 @@
import requests
-def prepare_headers(access_token=None, type="bearer", accept="application/vnd.github+json"):
+def prepare_headers(
+ access_token=None, type="bearer", accept="application/vnd.github+json"
+):
return {
# "Authorization": type + " " + access_token,
"Accept": accept,
- "X-GitHub-Api-Version": "2022-11-28"
+ "X-GitHub-Api-Version": "2022-11-28",
}
+
+
def get_user(access_token=None):
- '''
+ """
Get user details from github
- '''
+ """
headers = prepare_headers(access_token)
response = requests.get("https://api.github.com/user", headers=headers)
return response.json()
+
def get_user_organizations(access_token=None):
- '''
+ """
Get user organizations from github
- '''
+ """
headers = prepare_headers(access_token)
response = requests.get("https://api.github.com/user/orgs", headers=headers)
return response.json()
def get_organization_repos(access_token, organization):
- '''
+ """
Get repositories in an organization from Github
- '''
+ """
headers = prepare_headers(access_token)
- response = requests.get(f"https://api.github.com/orgs/{organization}/repos", headers=headers)
+ response = requests.get(
+ f"https://api.github.com/orgs/{organization}/repos", headers=headers
+ )
return response.json()
-def get_file_in_repo(access_token:str, organization:str, repo:str, path: str):
- '''
+def get_file_in_repo(access_token: str, organization: str, repo: str, path: str):
+ """
Get file in a repository from Github
- '''
+ """
headers = prepare_headers(access_token, accept="application/vnd.github.raw")
- response = requests.get(f"https://api.github.com/repos/{organization}/{repo}/contents/{path}", headers=headers)
+ response = requests.get(
+ f"https://api.github.com/repos/{organization}/{repo}/contents/{path}",
+ headers=headers,
+ )
return response.text
-def get_all_files_in_repo(access_token:str, organization:str, repo:str, path:str=''):
- '''
+
+def get_all_files_in_repo(
+ access_token: str, organization: str, repo: str, path: str = ""
+):
+ """
Get all files in a repository from Github
- '''
+ """
# TODO: For every file, we need to store it in the database with the commit hash so that we do not need to call this API again to fetch the same result
headers = prepare_headers(access_token)
- response = requests.get(f"https://api.github.com/repos/{organization}/{repo}/contents/{path}", headers=headers)
+ response = requests.get(
+ f"https://api.github.com/repos/{organization}/{repo}/contents/{path}",
+ headers=headers,
+ )
return response.json()
-def search_for_file_in_repo(access_token:str, organization:str, repo:str, query:str | None =None, extension: str | None=None, page:int=1, per_page:int=100, accept=None):
- '''
+def search_for_file_in_repo(
+ access_token: str,
+ organization: str,
+ repo: str,
+ query: str | None = None,
+ extension: str | None = None,
+ page: int = 1,
+ per_page: int = 100,
+ accept=None,
+):
+ """
Search for a file in a repository from Github
query examples:
1. "CRM in:file" - searches for keyword CRM in all files
@@ -62,7 +87,7 @@ def search_for_file_in_repo(access_token:str, organization:str, repo:str, query:
3. Combined query: "path:erpnext/crm.doctype+CRM in:file" - searches for keyword CRM in all files in path erpnext/crm.doctype
Extension and repo will be added to search query automatically
- '''
+ """
# TODO: This API is expensive to use. We need to store the result based on commit hash and return from our own database
headers = prepare_headers(access_token, accept=accept)
@@ -70,5 +95,8 @@ def search_for_file_in_repo(access_token:str, organization:str, repo:str, query:
query = f"{query}+repo:{organization}/{repo}"
if extension:
query = f"{query}+extension:{extension}"
- response = requests.get(f"https://api.github.com/search/code?q={query}+repo:{organization}/{repo}&page={page}&per_page={per_page}", headers=headers)
- return response.json()
\ No newline at end of file
+ response = requests.get(
+ f"https://api.github.com/search/code?q={query}+repo:{organization}/{repo}&page={page}&per_page={per_page}",
+ headers=headers,
+ )
+ return response.json()
diff --git a/commit/api/link.py b/commit/api/link.py
new file mode 100644
index 0000000..e8aaca0
--- /dev/null
+++ b/commit/api/link.py
@@ -0,0 +1,9 @@
+import frappe
+
+
+@frappe.whitelist()
+def get_link_title(doctype, docname):
+ meta = frappe.get_meta(doctype)
+ if meta.title_field:
+ return frappe.get_value(doctype, docname, meta.title_field)
+ return docname
diff --git a/commit/api/meta_data.py b/commit/api/meta_data.py
index b657076..c17b68c 100644
--- a/commit/api/meta_data.py
+++ b/commit/api/meta_data.py
@@ -1,70 +1,73 @@
import frappe
+
from commit.commit.code_analysis.apis import find_all_occurrences_of_whitelist
+
@frappe.whitelist()
def get_installed_apps():
- '''
- Get all installed apps
- 1. Get the installed applications from the Installed Applications doctype
- 2. Get the app hooks for each app
- 3. Get the app description, publisher, logo, version and git branch
- 4. Return the updated apps
- '''
- install_app_doc = frappe.get_cached_doc('Installed Applications')
- install_apps = install_app_doc.get('installed_applications')
+ """
+ Get all installed apps
+ 1. Get the installed applications from the Installed Applications doctype
+ 2. Get the app hooks for each app
+ 3. Get the app description, publisher, logo, version and git branch
+ 4. Return the updated apps
+ """
+ install_app_doc = frappe.get_cached_doc("Installed Applications")
+ install_apps = install_app_doc.get("installed_applications")
updated_apps = []
for app in install_apps:
- app_name = app.get('app_name')
+ app_name = app.get("app_name")
app_hooks = frappe.get_hooks(app_name=app_name)
- app_description = app_hooks.get('app_description')
+ app_description = app_hooks.get("app_description")
if app_description is not None:
app_description = app_description[0]
- app_publisher = app_hooks.get('app_publisher')
+ app_publisher = app_hooks.get("app_publisher")
if app_publisher is not None:
app_publisher = app_publisher[0]
-
- app_logo_url = app_hooks.get('app_logo_url') or app_hooks.get('app_logo')
+
+ app_logo_url = app_hooks.get("app_logo_url") or app_hooks.get("app_logo")
if app_logo_url is not None:
app_logo_url = app_logo_url[0]
-
- app_version = app.get('app_version')
- git_branch = app.get('git_branch')
+ app_version = app.get("app_version")
+
+ git_branch = app.get("git_branch")
updated_app = {
- 'app_name': app_name,
- 'app_publisher': app_publisher,
- 'app_description': app_description,
- 'app_logo_url': app_logo_url,
- 'app_version': app_version,
- 'git_branch': git_branch
+ "app_name": app_name,
+ "app_publisher": app_publisher,
+ "app_description": app_description,
+ "app_logo_url": app_logo_url,
+ "app_version": app_version,
+ "git_branch": git_branch,
}
updated_apps.append(updated_app)
return updated_apps
+
@frappe.whitelist()
def get_apis_for_app(app_name: str):
- '''
- Gets the Project Branch document with the organization and app name
- '''
+ """
+ Gets the Project Branch document with the organization and app name
+ """
app_path = frappe.get_app_path(app_name)
# remove last part of the path which is the app name
- app_path = app_path.rsplit('/', 1)[0]
- apis = find_all_occurrences_of_whitelist(app_path,app_name)
+ app_path = app_path.rsplit("/", 1)[0]
+ apis = find_all_occurrences_of_whitelist(app_path, app_name)
app_hooks = frappe.get_hooks(app_name=app_name)
- install_app_doc = frappe.get_cached_doc('Installed Applications')
- install_apps = install_app_doc.get('installed_applications')
- app = [app for app in install_apps if app.get('app_name') == app_name][0]
+ install_app_doc = frappe.get_cached_doc("Installed Applications")
+ install_apps = install_app_doc.get("installed_applications")
+ app = [app for app in install_apps if app.get("app_name") == app_name][0]
- branch_name = app.get('git_branch')
+ branch_name = app.get("git_branch")
return {
"apis": apis,
"app_name": app_name,
"branch_name": branch_name,
- }
\ No newline at end of file
+ }
diff --git a/commit/api/openapi.py b/commit/api/openapi.py
new file mode 100644
index 0000000..60f23bd
--- /dev/null
+++ b/commit/api/openapi.py
@@ -0,0 +1,291 @@
+import json
+from collections import Counter
+
+import frappe
+
+from commit.commit.code_analysis.apis import find_all_occurrences_of_whitelist
+
+
+def _python_type_to_openapi_type(type_hint: str) -> dict:
+ """
+ Map a simple Python type hint string to an OpenAPI schema.
+ Falls back to string when unsure.
+ """
+ if not type_hint:
+ return {"type": "string"}
+
+ normalized = type_hint.strip().lower()
+
+ if normalized in {"int", "integer"}:
+ return {"type": "integer"}
+ if normalized in {"float", "decimal"}:
+ return {"type": "number", "format": "float"}
+ if normalized in {"bool", "boolean"}:
+ return {"type": "boolean"}
+ if normalized in {"list", "tuple", "set"}:
+ return {"type": "array", "items": {"type": "string"}}
+ if normalized in {"dict", "mapping"}:
+ return {"type": "object"}
+
+ return {"type": "string"}
+
+
+def _get_duplicate_api_names(apis: list) -> set:
+ """Return set of API names that appear more than once (so we can disambiguate summary)."""
+ names = [api.get("name") or "" for api in apis if api.get("api_path")]
+ counts = Counter(names)
+ return {n for n, c in counts.items() if c > 1}
+
+
+def _summary_for_api(api: dict, duplicate_names: set) -> str:
+ """Unique summary: name, or 'name - (module)' when name is duplicate."""
+ name = api.get("name") or api.get("api_path") or ""
+ if not name:
+ return api.get("api_path", "")
+ if name not in duplicate_names:
+ return name
+ # Disambiguate with module path (api_path without last segment). Use one separator
+ # to avoid " - (module)" becoming " - -module-" when tools sanitize for file names.
+ api_path = api.get("api_path", "")
+ parts = api_path.rsplit(".", 1)
+ file_name = parts[0] if len(parts) == 2 else api_path
+ return f"{name} · {file_name}"
+
+
+def _build_operation_for_api(api: dict, duplicate_names: set | None = None) -> dict:
+ """
+ Build the OpenAPI operation object for a single API (summary, parameters, responses, security).
+ Returns the operation dict that can be placed under a path and method (get, post, etc.).
+ """
+ duplicate_names = duplicate_names or set()
+ api_path = api.get("api_path", "")
+ arguments = api.get("arguments") or []
+ documentation = api.get("documentation") or ""
+
+ parameters = []
+ for arg in arguments:
+ name = arg.get("argument")
+ if not name or name in {"self", "cls"}:
+ continue
+ schema = _python_type_to_openapi_type(arg.get("type", ""))
+ required = arg.get("default", "") == ""
+ parameters.append(
+ {
+ "name": name,
+ "in": "query",
+ "required": required,
+ "schema": schema,
+ }
+ )
+
+ summary = _summary_for_api(api, duplicate_names)
+ operation = {
+ "summary": summary,
+ "description": documentation or "",
+ "operationId": api_path.replace(".", "_"),
+ "parameters": parameters,
+ "responses": {
+ "200": {
+ "description": "Successful response",
+ }
+ },
+ }
+ if api.get("allow_guest"):
+ operation["security"] = []
+
+ return operation
+
+
+def _build_path_item_for_api(
+ api: dict, duplicate_names: set | None = None
+) -> tuple[str, dict]:
+ """
+ Build the OpenAPI path and path-item (method -> operation) for a single API.
+ Returns (path_string, path_item) where path_item is e.g. {"get": {...}, "post": {...}}.
+ """
+ api_path = api.get("api_path")
+ if not api_path:
+ return "", {}
+
+ path = f"/api/method/{api_path}"
+ request_types = api.get("request_types") or ["GET"]
+ operation = _build_operation_for_api(api, duplicate_names)
+ path_item = {}
+ for method in request_types:
+ method_lower = (method or "GET").lower()
+ if method_lower not in path_item:
+ path_item[method_lower] = dict(operation)
+ return path, path_item
+
+
+def get_openapi_for_single_api(api: dict) -> dict:
+ """
+ Return the OpenAPI paths object for a single API.
+ Use this to get the OpenAPI representation of one whitelisted API (e.g. to expose or merge elsewhere).
+ """
+ path, path_item = _build_path_item_for_api(api)
+ if not path:
+ return {}
+ return {path: path_item}
+
+
+def _build_paths_from_apis(apis: list) -> dict:
+ """
+ Build OpenAPI paths from Commit's internal API discovery objects.
+ One pass to find duplicate API names; summaries become 'name - (module)' only when duplicated.
+ """
+ duplicate_names = _get_duplicate_api_names(apis)
+ paths: dict = {}
+ for api in apis:
+ path, path_item = _build_path_item_for_api(api, duplicate_names)
+ if not path:
+ continue
+ if path not in paths:
+ paths[path] = {}
+ for method_lower, operation in path_item.items():
+ if method_lower in paths[path]:
+ continue
+ paths[path][method_lower] = operation
+ return paths
+
+
+def _get_apis_from_project_branch(project_branch: str) -> list:
+ """
+ Collect API definitions for a single Commit Project Branch (from stored whitelisted_apis).
+ """
+ branch_doc = frappe.get_cached_doc("Commit Project Branch", project_branch)
+ apis = (
+ json.loads(branch_doc.whitelisted_apis).get("apis", [])
+ if branch_doc.whitelisted_apis
+ else []
+ )
+ documentation = (
+ json.loads(branch_doc.documentation).get("apis", [])
+ if branch_doc.documentation
+ else []
+ )
+ for api in apis:
+ for doc in documentation:
+ if isinstance(doc, str):
+ try:
+ doc = json.loads(doc)
+ except json.JSONDecodeError:
+ continue
+ if doc.get("function_name") == api.get("name") and doc.get(
+ "path"
+ ) == api.get("api_path"):
+ api["documentation"] = doc.get("documentation")
+ break
+ return apis
+
+
+def _get_request_base_url() -> str:
+ """Return the current request's base URL (scheme + host) so the OpenAPI spec has a default server."""
+ try:
+ return frappe.utils.get_url().rstrip("/")
+ except Exception:
+ return "https://your-frappe-site.com"
+
+
+def _build_openapi_document(
+ apis: list,
+ title: str | None = None,
+ description: str | None = None,
+) -> dict:
+ """
+ Build a minimal OpenAPI 3.0 document from Commit's API discovery data.
+ """
+ site_name = frappe.local.site if getattr(frappe.local, "site", None) else "Commit"
+ default_title = f"Commit API - {site_name}"
+ default_description = (
+ "Automatically generated OpenAPI definition for Frappe whitelisted methods."
+ )
+ base_url = _get_request_base_url()
+ paths = _build_paths_from_apis(apis)
+
+ # Ensure version is a string for strict OpenAPI validators (e.g. Postman).
+ try:
+ version = frappe.get_hooks().get("app_version", ["0.0.0"])
+ version = version[0] if version else "0.0.0"
+ except Exception:
+ version = "0.0.0"
+ if not isinstance(version, str):
+ version = "0.0.0"
+
+ doc = {
+ "openapi": "3.0.0",
+ "info": {
+ "title": title or default_title,
+ "version": version,
+ "description": description or default_description,
+ },
+ "servers": [{"url": base_url, "description": "Frappe site"}],
+ "paths": paths,
+ "components": {
+ "securitySchemes": {
+ "UserKey": {
+ "type": "apiKey",
+ "in": "header",
+ "name": "Authorization",
+ "description": "Frappe user API key / token or session cookie.",
+ }
+ }
+ },
+ }
+ # Do not set document-level security so Postman/other tools don't send auth by default.
+ # Guest endpoints work without it; protected endpoints can add auth in the tool.
+ return doc
+
+
+@frappe.whitelist(allow_guest=True)
+def get_openapi_definition_installed_apps(app_name: str | None = None) -> dict:
+ """
+ Return an OpenAPI 3.0 definition for Installed Apps only.
+
+ - When app_name is provided, only that app's whitelisted APIs are included.
+ - When app_name is omitted, all installed apps on the current site are included.
+
+ Use this for APIs discovered from apps in Installed Applications.
+ """
+ if not app_name:
+ frappe.throw("App name is required.")
+
+ app_path = frappe.get_app_path(app_name)
+ if not app_path:
+ frappe.throw(f"App {app_name} is not installed on this site.")
+ root_path = app_path.rsplit("/", 1)[0]
+ apis = find_all_occurrences_of_whitelist(root_path, app_name)
+ title = f"{app_name}'s OpenAPI Definition"
+
+ return _build_openapi_document(
+ apis,
+ title=title,
+ description=f"OpenAPI definition for whitelisted methods for {app_name}.",
+ )
+
+
+@frappe.whitelist(allow_guest=True)
+def get_openapi_definition_project_apps(project_branch: str | None = None) -> dict:
+ """
+ Return an OpenAPI 3.0 definition for Project Apps (Commit Project Branch) only.
+
+ - When project_branch is provided, only that branch's APIs are included.
+ - When project_branch is omitted, all Commit Project Branch documents on the site are included.
+
+ Use this for APIs from project branches (stored whitelisted_apis).
+ """
+ if not project_branch:
+ frappe.throw("Project branch is required.")
+
+ if not frappe.db.exists("Commit Project Branch", project_branch):
+ frappe.throw(f"Project branch {project_branch} does not exist.")
+
+ project_branch_doc = frappe.get_cached_doc("Commit Project Branch", project_branch)
+ apis = _get_apis_from_project_branch(project_branch)
+ title = f"{project_branch_doc.app_name}'s OpenAPI Definition"
+
+ return _build_openapi_document(
+ apis,
+ title=title,
+ description=f"OpenAPI definition for whitelisted methods for {project_branch_doc.app_name} ({project_branch_doc.branch_name}).",
+ )
diff --git a/commit/api/preview.py b/commit/api/preview.py
index 8fdf22d..e50f035 100644
--- a/commit/api/preview.py
+++ b/commit/api/preview.py
@@ -1,30 +1,31 @@
import asyncio
import io
-from pyppeteer import launch
+
import frappe
from frappe.utils.file_manager import save_file
+from pyppeteer import launch
+
from commit.api.convert_to_webp import convert_to_webp
+
async def capture_screenshot(url, width=1366, height=800, delay=3):
browser = await launch(
- headless=True,
- handleSIGINT=False,
- handleSIGTERM=False,
- handleSIGHUP=False
+ headless=True, handleSIGINT=False, handleSIGTERM=False, handleSIGHUP=False
)
page = await browser.newPage()
-
+
await page.setViewport({"width": width, "height": height})
await page.goto(url, {"waitUntil": "load"})
await asyncio.sleep(delay) # Ensure page loads fully
-
+
# ✅ Explicitly return bytes and ensure no file is saved
- screenshot_bytes = await page.screenshot({"fullPage": False, "encoding": "binary"})
-
+ screenshot_bytes = await page.screenshot({"fullPage": False, "encoding": "binary"})
+
await browser.close()
-
+
return screenshot_bytes # Return raw image data
+
def save_preview_screenshot(url, doctype, docname, field):
try:
loop = asyncio.get_running_loop()
@@ -38,21 +39,23 @@ def save_preview_screenshot(url, doctype, docname, field):
# ✅ Correct way: Wrap bytes in BytesIO
screenshot_io = io.BytesIO(screenshot_bytes)
- file_doc = frappe.get_doc({
- "doctype": "File",
- "file_name": docname + "_" + "preview.png",
- "attached_to_doctype": doctype,
- "attached_to_name": docname,
- "attached_to_field": field,
- "is_private": 1,
- "content": screenshot_io.getvalue()
- })
+ file_doc = frappe.get_cached_doc(
+ {
+ "doctype": "File",
+ "file_name": docname + "_" + "preview.png",
+ "attached_to_doctype": doctype,
+ "attached_to_name": docname,
+ "attached_to_field": field,
+ "is_private": 1,
+ "content": screenshot_io.getvalue(),
+ }
+ )
file_doc.save()
# Convert to WebP
file_url = convert_to_webp(file_doc.file_url, file_doc)
# Update the document with the preview file URL
- doc = frappe.get_doc(doctype, docname)
+ doc = frappe.get_cached_doc(doctype, docname)
doc.set(field, file_url)
doc.save()
diff --git a/commit/api/search.py b/commit/api/search.py
index 1d0b4ba..17f22e4 100644
--- a/commit/api/search.py
+++ b/commit/api/search.py
@@ -1,27 +1,28 @@
import frappe
-from frappe.desk.search import search_widget, build_for_autosuggest
+from frappe.desk.search import build_for_autosuggest, search_widget
+
# this is called by the Link Field
@frappe.whitelist()
def search_link(
- doctype,
- txt,
- query=None,
- filters=None,
- page_length=20,
+ doctype,
+ txt,
+ query=None,
+ filters=None,
+ page_length=20,
start=0,
- searchfield=None,
- reference_doctype=None,
- ignore_user_permissions=False,
+ searchfield=None,
+ reference_doctype=None,
+ ignore_user_permissions=False,
):
- results = search_widget(
- doctype,
- txt.strip(),
- query,
- searchfield=searchfield,
- page_length=page_length,
- filters=filters,
- reference_doctype=reference_doctype,
- ignore_user_permissions=ignore_user_permissions,
- )
- return build_for_autosuggest(results, doctype=doctype)
\ No newline at end of file
+ results = search_widget(
+ doctype,
+ txt.strip(),
+ query,
+ searchfield=searchfield,
+ page_length=page_length,
+ filters=filters,
+ reference_doctype=reference_doctype,
+ ignore_user_permissions=ignore_user_permissions,
+ )
+ return build_for_autosuggest(results, doctype=doctype)
diff --git a/commit/commit/code_analysis/apis.py b/commit/commit/code_analysis/apis.py
index c95ca96..f9ebafd 100644
--- a/commit/commit/code_analysis/apis.py
+++ b/commit/commit/code_analysis/apis.py
@@ -1,18 +1,21 @@
-import os
import ast
-import frappe
import json
+import os
+
+import frappe
other_decorators = [
- '@cache_source',
- '@frappe.validate_and_sanitize_search_inputs',
- '@rate_limit'
+ "@cache_source",
+ "@frappe.validate_and_sanitize_search_inputs",
+ "@rate_limit",
]
+
+
def find_all_occurrences_of_whitelist(path: str, app_name: str):
- '''
- Find all occurences of @frappe.whitelist in the app repository
- These should only be in .py files
- '''
+ """
+ Find all occurences of @frappe.whitelist in the app repository
+ These should only be in .py files
+ """
# Get list of all .py files in the app
py_files = get_py_files(path, app_name)
api_count = 0
@@ -20,30 +23,106 @@ def find_all_occurrences_of_whitelist(path: str, app_name: str):
api_details = []
# For each file, check if @frappe.whitelist is present
for file in py_files:
- file_content = open(file, 'r').read()
+ file_content = open(file, "r").read()
# @frappe.whitelist can be mentioned multiple times in a file
# So, we need to find all occurrences
# We can use the count() method to find the number of occurrences
# If the count is greater than 0, then the string is present
- no_of_occurrences = file_content.count('@frappe.whitelist')
+ no_of_occurrences = file_content.count("@frappe.whitelist")
if no_of_occurrences > 0:
api_count += no_of_occurrences
file_count += 1
## Comment out later
# if file.endswith('party.py'):
- indexes,line_nos,no_of_occurrences = find_indexes_of_whitelist(file_content, no_of_occurrences)
+ result = find_indexes_of_whitelist(file_content, no_of_occurrences)
+ indexes, line_nos = result[0], result[1]
+ no_of_occurrences = result[2]
+ ast_names = result[3] if len(result) > 3 else None
+ ast_def_indexes = result[4] if len(result) > 4 else None
api_count += no_of_occurrences
- apis = get_api_details(app_name, file, file_content, indexes,line_nos, path)
+ apis = get_api_details(
+ app_name,
+ file,
+ file_content,
+ indexes,
+ line_nos,
+ path,
+ ast_names,
+ ast_def_indexes,
+ )
api_details.extend(apis)
-
+
return api_details
+
+def _find_whitelist_indexes_via_ast(file_content: str):
+ """
+ Find all @frappe.whitelist decorator positions using AST.
+ Returns (indexes, line_nos, function_names, def_indexes) or None if parse fails.
+ This is order-independent and not confused by strings/comments.
+ Uses AST-derived function names and def positions so multi-line signatures
+ and "def " in comments do not cause missed APIs.
+ """
+ try:
+ tree = ast.parse(file_content)
+ except SyntaxError:
+ return None
+ lines = file_content.split("\n")
+ results = [] # (decorator_index, line_no, function_name, def_index)
+ for node in ast.walk(tree):
+ if not isinstance(node, ast.FunctionDef):
+ continue
+ decorators = get_decorators(node)
+ if "whitelist" not in decorators:
+ continue
+ for dec in node.decorator_list:
+ name = get_decorator_name(dec)
+ if name == "whitelist":
+ lineno = getattr(dec, "lineno", node.lineno)
+ if lineno < 1 or lineno > len(lines):
+ break
+ line_start = sum(len(l) + 1 for l in lines[: lineno - 1])
+ line_content = lines[lineno - 1]
+ pos_in_line = line_content.find("@frappe.whitelist")
+ if pos_in_line >= 0:
+ decorator_index = line_start + pos_in_line
+ # def_index: start of "def name" on the function's line
+ def_lineno = node.lineno
+ def_line_start = sum(len(l) + 1 for l in lines[: def_lineno - 1])
+ def_line_content = lines[def_lineno - 1]
+ def_pos_in_line = def_line_content.find("def ")
+ def_index = (
+ def_line_start + def_pos_in_line
+ if def_pos_in_line >= 0
+ else decorator_index
+ )
+ results.append((decorator_index, lineno, node.name, def_index))
+ break
+ if not results:
+ return None
+ # Keep source order (ast.walk order is not guaranteed)
+ results.sort(key=lambda r: r[0])
+ return (
+ [r[0] for r in results],
+ [r[1] for r in results],
+ [r[2] for r in results],
+ [r[3] for r in results],
+ )
+
+
def find_indexes_of_whitelist(file_content: str, count: int):
- '''
+ """
Find indexes of @frappe.whitelist in the file content,
ensuring it's not commented out or inside a string.
- '''
+ Prefers AST-based discovery (order-independent); falls back to
+ string scan when AST parse fails (e.g. syntax error).
+ """
+ ast_result = _find_whitelist_indexes_via_ast(file_content)
+ if ast_result is not None:
+ indexes, line_nos, ast_names, ast_def_indexes = ast_result
+ return indexes, line_nos, count - len(indexes), ast_names, ast_def_indexes
+
def is_in_string_or_comment(file_content, index):
# State variables
in_single_quote = False
@@ -55,9 +134,9 @@ def is_in_string_or_comment(file_content, index):
i = 0
while i < index:
char = file_content[i]
-
+
# Handle triple single-quoted strings
- if file_content[i:i+3] == "'''" and not in_double_quote:
+ if file_content[i : i + 3] == "'''" and not in_double_quote:
if in_triple_single_quote:
in_triple_single_quote = False
i += 2
@@ -65,7 +144,7 @@ def is_in_string_or_comment(file_content, index):
in_triple_single_quote = True
i += 2
# Handle triple double-quoted strings
- elif file_content[i:i+3] == '"""' and not in_single_quote:
+ elif file_content[i : i + 3] == '"""' and not in_single_quote:
if in_triple_double_quote:
in_triple_double_quote = False
i += 2
@@ -73,91 +152,167 @@ def is_in_string_or_comment(file_content, index):
in_triple_double_quote = True
i += 2
# Handle single-quoted strings
- elif char == "'" and not in_double_quote and not in_triple_single_quote and not in_triple_double_quote:
+ elif (
+ char == "'"
+ and not in_double_quote
+ and not in_triple_single_quote
+ and not in_triple_double_quote
+ ):
in_single_quote = not in_single_quote
# Handle double-quoted strings
- elif char == '"' and not in_single_quote and not in_triple_single_quote and not in_triple_double_quote:
+ elif (
+ char == '"'
+ and not in_single_quote
+ and not in_triple_single_quote
+ and not in_triple_double_quote
+ ):
in_double_quote = not in_double_quote
# Handle single-line comments
- elif char == '#' and not in_single_quote and not in_double_quote and not in_triple_single_quote and not in_triple_double_quote:
+ elif (
+ char == "#"
+ and not in_single_quote
+ and not in_double_quote
+ and not in_triple_single_quote
+ and not in_triple_double_quote
+ ):
in_comment = True
# Handle end of line for single-line comments
- elif char == '\n':
+ elif char == "\n":
in_comment = False
-
+
i += 1
- return in_single_quote or in_double_quote or in_comment or in_triple_single_quote or in_triple_double_quote
+ return (
+ in_single_quote
+ or in_double_quote
+ or in_comment
+ or in_triple_single_quote
+ or in_triple_double_quote
+ )
indexes = []
line_nos = []
actual_count = count
-
+
start = 0
while actual_count > 0:
- index = file_content.find('@frappe.whitelist', start)
+ index = file_content.find("@frappe.whitelist", start)
if index == -1:
break
if not is_in_string_or_comment(file_content, index):
indexes.append(index)
- line_nos.append(file_content.count('\n', 0, index) + 1)
+ line_nos.append(file_content.count("\n", 0, index) + 1)
actual_count -= 1
- start = index + len('@frappe.whitelist')
-
- return indexes, line_nos, actual_count
+ start = index + len("@frappe.whitelist")
-def get_api_details(app_name, file, file_content: str, indexes: list,line_nos:list, path: str):
- '''
+ return indexes, line_nos, actual_count, None, None
+
+
+def get_api_details(
+ app_name,
+ file,
+ file_content: str,
+ indexes: list,
+ line_nos: list,
+ path: str,
+ ast_names=None,
+ ast_def_indexes=None,
+):
+ """
Get details of the API
- '''
+ """
apis = []
- for index in indexes:
- whitelist_details = get_whitelist_details(file_content, index)
- api_details = get_api_name(file_content, index)
- other_decorators = get_other_decorators(file_content, index, api_details.get('def_index'))
- obj = {
- **api_details,
- **whitelist_details,
- 'other_decorators': other_decorators,
- 'index': index,
- 'block_start': line_nos[indexes.index(index)],
- 'block_end': find_function_end_lines(file_content,api_details.get('name','')),
- 'file': file,
- 'api_path': file.replace(path, '').replace('\\', '/').replace('.py', '').replace('/', '.')[1:] + '.' + api_details.get('name')
- }
- documentation, last_updated, is_published, published_on, publish_by, publish_id,published_route = get_documentation_from_branch_documentation(app_name, obj.get('name'), obj.get('api_path'))
- obj['documentation'] = documentation
- obj['last_updated'] = last_updated
- obj['is_published'] = is_published
- obj['published_on'] = published_on
- obj['publish_by'] = publish_by
- obj['publish_id'] = publish_id
- obj['published_route'] = published_route
- apis.append(obj)
-
+ for i, index in enumerate(indexes):
+ try:
+ whitelist_details = get_whitelist_details(file_content, index)
+ use_ast = (
+ ast_names is not None
+ and ast_def_indexes is not None
+ and i < len(ast_names)
+ and i < len(ast_def_indexes)
+ )
+ if use_ast:
+ api_name = ast_names[i]
+ def_index = ast_def_indexes[i]
+ api_details = get_api_name(file_content, index, def_index=def_index)
+ api_details["name"] = api_name
+ api_details["def_index"] = def_index
+ else:
+ api_details = get_api_name(file_content, index)
+ if not api_details.get("name"):
+ continue
+ def_idx = api_details.get("def_index", -1)
+ search_end = (
+ def_idx
+ if isinstance(def_idx, int) and def_idx >= 0
+ else len(file_content)
+ )
+ other_decorators = get_other_decorators(file_content, index, search_end)
+ obj = {
+ **api_details,
+ **whitelist_details,
+ "other_decorators": other_decorators,
+ "index": index,
+ "block_start": line_nos[i],
+ "block_end": find_function_end_lines(
+ file_content, api_details.get("name", "")
+ ),
+ "file": file,
+ "api_path": file.replace(path, "")
+ .replace("\\", "/")
+ .replace(".py", "")
+ .replace("/", ".")[1:]
+ + "."
+ + api_details.get("name"),
+ }
+ (
+ documentation,
+ last_updated,
+ is_published,
+ published_on,
+ publish_by,
+ publish_id,
+ published_route,
+ ) = get_documentation_from_branch_documentation(
+ app_name, obj.get("name"), obj.get("api_path")
+ )
+ obj["documentation"] = documentation
+ obj["last_updated"] = last_updated
+ obj["is_published"] = is_published
+ obj["published_on"] = published_on
+ obj["publish_by"] = publish_by
+ obj["publish_id"] = publish_id
+ obj["published_route"] = published_route
+ apis.append(obj)
+ except Exception:
+ frappe.log_error(
+ f"Commit API discovery: skipped API at index {index} in {file}",
+ "Commit API Discovery",
+ )
return apis
+
def get_other_decorators(file_content: str, index: int, def_index: int):
- '''
- See if other decorators are present in between the @frappe.whitelist decorator and the def
- '''
+ """
+ See if other decorators are present in between the @frappe.whitelist decorator and the def
+ """
decorators = []
for decorator in other_decorators:
decorator_index = file_content.find(decorator, index, def_index)
if decorator_index != -1:
decorators.append(decorator)
-
+
return decorators
-def get_whitelist_details(file_content: str, index: int):
- '''
+def get_whitelist_details(file_content: str, index: int):
+ """
Get details of the @frappe.whitelist decorator
The index is the index of the first occurrence of @frappe.whitelist
We need to find the first occurrence of ")" after the index
- '''
- whitelist_end_index = file_content.find(')', index)
- whitelisted_content = file_content[index:whitelist_end_index + 1]
+ """
+ whitelist_end_index = file_content.find(")", index)
+ whitelisted_content = file_content[index : whitelist_end_index + 1]
if "(" in whitelisted_content and ")" in whitelisted_content:
args = whitelisted_content.split("(")[1].split(")")[0].split(",")
else:
@@ -167,65 +322,128 @@ def get_whitelist_details(file_content: str, index: int):
allow_guest = False
for arg in args:
if "methods" in arg:
- request_types = arg.split("=")[1].replace("[", "").replace("]", "").replace('"', '').replace("'", "").split(",")
-
+ request_types = (
+ arg.split("=")[1]
+ .replace("[", "")
+ .replace("]", "")
+ .replace('"', "")
+ .replace("'", "")
+ .split(",")
+ )
+
if "xss_safe" in arg:
xss_safe = arg.split("=")[1].strip() == "True"
-
+
if "allow_guest" in arg:
allow_guest = arg.split("=")[1].strip() == "True"
-
+
return {
"request_types": request_types,
"xss_safe": xss_safe,
- "allow_guest": allow_guest
+ "allow_guest": allow_guest,
}
-def get_api_name(file_content: str, index: int):
- '''
- Get name of the API.
- To do this, we need to find the first occurrence of "def api_name" after the index
- '''
- api_name = ''
- # Find the first occurrence of "def" after the index
- def_index = file_content.find('def ', index)
- # Find occurrence of ":" after the def_index
- colon_index = file_content.find('):', def_index)
+def _find_def_at_line_start(file_content: str, index: int):
+ """Find next "def " that starts a line (after newline or start of file). Avoids matching "def " in comments."""
+ start = index
+ while True:
+ def_index = file_content.find("def ", start)
+ if def_index == -1:
+ return -1
+ if def_index == 0 or file_content[def_index - 1] == "\n":
+ return def_index
+ start = def_index + 1
+
- # Get the string between def_index and colon_index
- api_def = file_content[def_index:colon_index+1].replace('\n', '').replace('\t', '')
+def _find_signature_end(file_content: str, def_index: int):
+ """Find the "):" that closes the function signature. Prefers ) followed by newline or : to avoid type hints."""
+ pos = def_index
+ while True:
+ pos = file_content.find("):", pos)
+ if pos == -1:
+ return -1
+ next_char = file_content[pos + 2] if pos + 2 < len(file_content) else "\n"
+ if next_char in ("\n", ":"):
+ return pos
+ pos += 1
- # api_def is of the form "def api_name(self, arg1, arg2, ...)"
- # We need to get the api_name. To do this, we can remove the "def " first
- api_name_with_params = api_def.replace('def ', '')
+def get_api_name(file_content: str, index: int, def_index: int = None):
+ """
+ Get name of the API.
+ Finds the function definition after the decorator. When def_index is provided (from AST), uses it.
+ Otherwise finds "def " at line start only to avoid matching comments.
+ """
+ api_name = ""
+ if def_index is None:
+ def_index = _find_def_at_line_start(file_content, index)
+ if def_index == -1:
+ return {"name": "", "arguments": [], "def": "", "def_index": -1}
+ colon_index = _find_signature_end(file_content, def_index)
+ if colon_index == -1:
+ return {"name": "", "arguments": [], "def": "", "def_index": def_index}
+ api_def = (
+ file_content[def_index : colon_index + 1].replace("\n", "").replace("\t", "")
+ )
+ api_name_with_params = api_def.replace("def ", "", 1)
api_name = extract_name_from_def(api_name_with_params)
arguments = extract_arguments_from_def(api_name_with_params)
-
-
-
return {
- 'name': api_name,
- 'arguments': arguments,
- 'def': api_def,
- 'def_index': def_index,
+ "name": api_name,
+ "arguments": arguments,
+ "def": api_def,
+ "def_index": def_index,
}
+
def extract_name_from_def(api_def: str):
- '''
+ """
Extract name from def
- '''
+ """
return api_def.split("(")[0].strip()
+
+def _split_params_by_comma(params_str: str) -> list:
+ """
+ Split parameter string by top-level commas, ignoring commas inside [], (), {}.
+ """
+ parts = []
+ current = []
+ open_brackets = [] # stack of opening bracket chars
+ bracket_pairs = {"[": "]", "(": ")", "{": "}"}
+ i = 0
+ while i < len(params_str):
+ c = params_str[i]
+ if c in bracket_pairs:
+ open_brackets.append(c)
+ current.append(c)
+ i += 1
+ elif open_brackets and c == bracket_pairs[open_brackets[-1]]:
+ open_brackets.pop()
+ current.append(c)
+ i += 1
+ elif c == "," and not open_brackets:
+ parts.append("".join(current).strip())
+ current = []
+ i += 1
+ else:
+ current.append(c)
+ i += 1
+ if current:
+ parts.append("".join(current).strip())
+ return parts
+
+
def extract_arguments_from_def(api_def: str):
- '''
+ """
Extract arguments from def
- '''
+ """
if "(" not in api_def or ")" not in api_def:
arguments_with_types_defaults = []
else:
- arguments_with_types_defaults = api_def.split("(")[1].split(")")[0].split(",")
+ params_str = api_def.split("(")[1].split(")")[0]
+ arguments_with_types_defaults = _split_params_by_comma(params_str)
arguments = []
for arg in arguments_with_types_defaults:
@@ -234,33 +452,33 @@ def extract_arguments_from_def(api_def: str):
argument = ""
type = ""
if "=" in argument_with_types_default:
- default_split = argument_with_types_default.split("=")
- default = default_split[1].strip().replace('"', '').replace("'", "")
+ default_split = argument_with_types_default.split("=", 1)
+ default = default_split[1].strip().replace('"', "").replace("'", "")
argument = default_split[0].strip()
else:
argument = argument_with_types_default
if ":" in argument:
- type = argument.split(":")[1].strip()
- argument = argument.split(":")[0].strip()
- arguments.append({
- "argument": argument,
- "type": type,
- "default": default
- })
+ name_type = argument.split(":", 1)
+ argument = name_type[0].strip()
+ type = name_type[1].strip()
+ arguments.append({"argument": argument, "type": type, "default": default})
return arguments
+
+
def get_py_files(path: str, app_name: str):
- '''
+ """
Get list of all .py files in the app
- '''
+ """
py_files = []
for root, dirs, files in os.walk(os.path.join(path, app_name)):
for file in files:
- if file.endswith('.py'):
+ if file.endswith(".py"):
py_files.append(os.path.join(root, file))
return py_files
-def find_function_end_lines(source_code: str,function_name:str):
+
+def find_function_end_lines(source_code: str, function_name: str):
tree = ast.parse(source_code)
@@ -269,11 +487,12 @@ def find_function_end_lines(source_code: str,function_name:str):
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
decorators = get_decorators(node)
- if 'whitelist' in decorators:
+ if "whitelist" in decorators:
end_line = node.end_lineno
function_end_lines[node.name] = end_line
- return function_end_lines.get(function_name,0)
+ return function_end_lines.get(function_name, 0)
+
def get_decorator_name(node):
if isinstance(node, ast.Call):
@@ -286,6 +505,7 @@ def get_decorator_name(node):
else:
return None
+
def get_decorators(node):
decorators = []
for decorator in node.decorator_list:
@@ -294,32 +514,48 @@ def get_decorators(node):
decorators.append(decorator_name)
return decorators
-def get_documentation_from_branch_documentation(app_name:str, name: str, api_path: str):
- '''
+
+def get_documentation_from_branch_documentation(
+ app_name: str, name: str, api_path: str
+):
+ """
Get documentation from the Commit Branch Documentation
- '''
- if frappe.db.exists('Commit Branch Documentation',app_name):
- branch_documentation = frappe.get_doc('Commit Branch Documentation', app_name)
- docs = json.loads(branch_documentation.documentation) if branch_documentation.documentation else {}
+ """
+ if frappe.db.exists("Commit Branch Documentation", app_name):
+ branch_documentation = frappe.get_cached_doc(
+ "Commit Branch Documentation", app_name
+ )
+ docs = (
+ json.loads(branch_documentation.documentation)
+ if branch_documentation.documentation
+ else {}
+ )
apis = docs.get("apis", [])
- documentation = ''
- last_updated = ''
- is_published = ''
- published_on = ''
- publish_by = ''
- publish_id = ''
- published_route = ''
+ documentation = ""
+ last_updated = ""
+ is_published = ""
+ published_on = ""
+ publish_by = ""
+ publish_id = ""
+ published_route = ""
for api in apis:
if api.get("function_name") == name and api.get("path") == api_path:
documentation = api.get("documentation")
last_updated = api.get("last_updated")
- is_published = api.get("is_published",0)
+ is_published = api.get("is_published", 0)
published_on = api.get("published_on", None)
publish_by = api.get("publish_by", None)
publish_id = api.get("publish_id", None)
published_route = api.get("published_route", None)
break
- return documentation, last_updated, is_published, published_on, publish_by, publish_id, published_route
+ return (
+ documentation,
+ last_updated,
+ is_published,
+ published_on,
+ publish_by,
+ publish_id,
+ published_route,
+ )
else:
- return '', '', '', '', '', '', ''
-
\ No newline at end of file
+ return "", "", "", "", "", "", ""
diff --git a/commit/commit/code_analysis/doctypes.py b/commit/commit/code_analysis/doctypes.py
index bc70691..e6e378a 100644
--- a/commit/commit/code_analysis/doctypes.py
+++ b/commit/commit/code_analysis/doctypes.py
@@ -1,50 +1,55 @@
+import json
import os
+
from commit.commit.code_analysis.utils import get_module_path, parse_module_name
-import json
+
def get_doctypes_in_module(path: str, app_name: str, module: str):
- '''
+ """
Get list of doctypes in a module
- '''
+ """
doctype_names = []
module_path = get_module_path(path, app_name, module)
- doctype_folder_path = os.path.join(module_path, 'doctype')
+ doctype_folder_path = os.path.join(module_path, "doctype")
does_module_have_doctypes = os.path.isdir(doctype_folder_path)
if does_module_have_doctypes:
# Since the doctype folder exists - find all .json files within the folders (only one level deep) and return the file contents
for dir in os.listdir(doctype_folder_path):
# doctype .json files have the same name as the folder they are in
- doctype_file_path = os.path.join(doctype_folder_path, dir, dir + '.json')
+ doctype_file_path = os.path.join(doctype_folder_path, dir, dir + ".json")
if os.path.isfile(doctype_file_path):
- doctype_file = open(doctype_file_path, 'r')
+ doctype_file = open(doctype_file_path, "r")
doctype_json = json.loads(doctype_file.read())
# doctypes_list.append(doctype_json)
- doctype_names.append(doctype_json.get(
- 'name'
- ))
-
+ doctype_names.append(doctype_json.get("name"))
+
return {
- 'module': module,
+ "module": module,
# 'doctypes': doctypes_list,
- 'doctype_names': doctype_names,
- 'number_of_doctypes': len(doctype_names)
+ "doctype_names": doctype_names,
+ "number_of_doctypes": len(doctype_names),
}
-def get_doctype_json(path: str, app_name: str, module:str, doctype: str):
+
+def get_doctype_json(path: str, app_name: str, module: str, doctype: str):
module_path = get_module_path(path, app_name, module)
- module_doctypes_folder_path = os.path.join(module_path, 'doctype')
+ module_doctypes_folder_path = os.path.join(module_path, "doctype")
does_module_have_doctypes = os.path.isdir(module_doctypes_folder_path)
if does_module_have_doctypes:
parsed_doctype_name = parse_module_name(doctype)
- doctype_folder_path = os.path.join(module_doctypes_folder_path, parsed_doctype_name)
+ doctype_folder_path = os.path.join(
+ module_doctypes_folder_path, parsed_doctype_name
+ )
does_doctype_folder_exist = os.path.isdir(doctype_folder_path)
if does_doctype_folder_exist:
- doctype_file_path = os.path.join(doctype_folder_path, parsed_doctype_name + '.json')
+ doctype_file_path = os.path.join(
+ doctype_folder_path, parsed_doctype_name + ".json"
+ )
if os.path.isfile(doctype_file_path):
- doctype_file = open(doctype_file_path, 'r')
+ doctype_file = open(doctype_file_path, "r")
doctype_json = json.loads(doctype_file.read())
return doctype_json
-
- return None
\ No newline at end of file
+
+ return None
diff --git a/commit/commit/code_analysis/schema_builder.py b/commit/commit/code_analysis/schema_builder.py
index b2ef681..c0cceb5 100644
--- a/commit/commit/code_analysis/schema_builder.py
+++ b/commit/commit/code_analysis/schema_builder.py
@@ -1,69 +1,75 @@
-import os
import json
+import os
+
+DISALLOWED_FIELD_TYPES = [
+ "Section Break",
+ "Tab Break",
+ "Fold",
+ "Column Break",
+ "Heading",
+ "HTML",
+ "Image",
+ "Icon",
+ "Button",
+]
+LINK_FIELD_TYPES = ["Link", "Table", "Table MultiSelect"]
-DISALLOWED_FIELD_TYPES = ['Section Break', 'Tab Break', 'Fold', 'Column Break', 'Heading', 'HTML', 'Image', 'Icon', 'Button']
-LINK_FIELD_TYPES = ['Link', 'Table', 'Table MultiSelect']
def get_schema_from_doctypes_json(doctypes_json: dict):
- '''
+ """
Parse doctype file
- '''
+ """
tables = []
relationships = []
- doctype_names = doctypes_json.get('doctype_names')
- doctype_jsons = doctypes_json.get('doctypes')
+ doctype_names = doctypes_json.get("doctype_names")
+ doctype_jsons = doctypes_json.get("doctypes")
for doctype_json in doctype_jsons:
- doctype_name = doctype_json.get('name')
+ doctype_name = doctype_json.get("name")
if doctype_name:
- columns = [{
- 'name': 'ID',
- 'id': 'name',
- 'format': 'Data',
- }]
+ columns = [
+ {
+ "name": "ID",
+ "id": "name",
+ "format": "Data",
+ }
+ ]
# dynamic_links = []
- for field in doctype_json.get('fields'):
- fieldname = field.get('fieldname')
- fieldtype = field.get('fieldtype')
+ for field in doctype_json.get("fields"):
+ fieldname = field.get("fieldname")
+ fieldtype = field.get("fieldtype")
if fieldtype not in DISALLOWED_FIELD_TYPES:
column = {
- 'name': field.get('label', fieldname),
- 'id': fieldname,
- 'format': fieldtype,
- 'is_custom_field': field.get('is_custom_field') or False,
+ "name": field.get("label", fieldname),
+ "id": fieldname,
+ "format": fieldtype,
+ "is_custom_field": field.get("is_custom_field") or False,
}
columns.append(column)
-
+
if fieldtype in LINK_FIELD_TYPES:
- if field.get('options') in doctype_names:
+ if field.get("options") in doctype_names:
relationship = {
- 'id': doctype_name + "_" + fieldname,
- 'source_table_name': doctype_name,
- 'source_column_name': fieldname,
- 'target_table_name': field.get('options'),
- 'target_column_name': 'name',
+ "id": doctype_name + "_" + fieldname,
+ "source_table_name": doctype_name,
+ "source_column_name": fieldname,
+ "target_table_name": field.get("options"),
+ "target_column_name": "name",
}
relationships.append(relationship)
table = {
- 'name': doctype_name,
- 'id': doctype_name,
- 'module': doctype_json.get('module'),
- 'istable': doctype_json.get('istable'),
- 'columns': columns,
+ "name": doctype_name,
+ "id": doctype_name,
+ "module": doctype_json.get("module"),
+ "istable": doctype_json.get("istable"),
+ "columns": columns,
}
tables.append(table)
-
+
return {
- 'tables': tables,
- 'relationships': relationships,
+ "tables": tables,
+ "relationships": relationships,
}
-
-
-
-
-
-
-
diff --git a/commit/commit/code_analysis/utils.py b/commit/commit/code_analysis/utils.py
index a64c886..4225b7d 100644
--- a/commit/commit/code_analysis/utils.py
+++ b/commit/commit/code_analysis/utils.py
@@ -1,14 +1,17 @@
import os
+
+
def get_module_path(path: str, app_name: str, module_name: str):
- '''
+ """
Get path to modules directory
- '''
+ """
parsed_module_name = parse_module_name(module_name)
modules_path = os.path.join(path, app_name, parsed_module_name)
return modules_path
+
def parse_module_name(module_name: str):
- '''
+ """
Parse module name
- '''
- return module_name.replace('-', '_').replace(' ', '_').lower()
\ No newline at end of file
+ """
+ return module_name.replace("-", "_").replace(" ", "_").lower()
diff --git a/commit/commit/doctype/commit_branch_documentation/commit_branch_documentation.json b/commit/commit/doctype/commit_branch_documentation/commit_branch_documentation.json
index a09a825..fbc1698 100644
--- a/commit/commit/doctype/commit_branch_documentation/commit_branch_documentation.json
+++ b/commit/commit/doctype/commit_branch_documentation/commit_branch_documentation.json
@@ -49,4 +49,4 @@
"sort_field": "creation",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/commit/commit/doctype/commit_branch_documentation/commit_branch_documentation.py b/commit/commit/doctype/commit_branch_documentation/commit_branch_documentation.py
index c84c245..c800fae 100644
--- a/commit/commit/doctype/commit_branch_documentation/commit_branch_documentation.py
+++ b/commit/commit/doctype/commit_branch_documentation/commit_branch_documentation.py
@@ -6,4 +6,4 @@
class CommitBranchDocumentation(Document):
- pass
+ pass
diff --git a/commit/commit/doctype/commit_branch_documentation/test_commit_branch_documentation.py b/commit/commit/doctype/commit_branch_documentation/test_commit_branch_documentation.py
index cc8937a..d9bcf65 100644
--- a/commit/commit/doctype/commit_branch_documentation/test_commit_branch_documentation.py
+++ b/commit/commit/doctype/commit_branch_documentation/test_commit_branch_documentation.py
@@ -6,4 +6,4 @@
class TestCommitBranchDocumentation(FrappeTestCase):
- pass
+ pass
diff --git a/commit/commit/doctype/commit_docs/commit_docs.json b/commit/commit/doctype/commit_docs/commit_docs.json
index 4d0c18d..e341d43 100644
--- a/commit/commit/doctype/commit_docs/commit_docs.json
+++ b/commit/commit/doctype/commit_docs/commit_docs.json
@@ -207,4 +207,4 @@
"sort_field": "creation",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/commit/commit/doctype/commit_docs/commit_docs.py b/commit/commit/doctype/commit_docs/commit_docs.py
index 0168b4a..704c5eb 100644
--- a/commit/commit/doctype/commit_docs/commit_docs.py
+++ b/commit/commit/doctype/commit_docs/commit_docs.py
@@ -1,248 +1,280 @@
# Copyright (c) 2024, The Commit Company and contributors
# For license information, please see license.txt
+import json
+
import frappe
from frappe.model.document import Document
-from commit.api.preview import save_preview_screenshot
+
from commit.api.convert_to_webp import save_webp_image
-import json
+from commit.api.preview import save_preview_screenshot
+
+
class CommitDocs(Document):
- def before_insert(self):
- '''
- Validate the Document
- # 1. Check if the Route is Unique
- '''
- if frappe.db.exists('Commit Docs',{'route':self.route}):
- frappe.throw('Route Already Exists')
-
- def validate(self):
- # 2. Loop Over through the Navbar Items and check there should be only one is Primary Button
- primary_button_count = 0
- for navbar_item in self.navbar_items:
- if navbar_item.is_primary_button:
- primary_button_count += 1
- if primary_button_count > 1:
- frappe.throw('Only One Primary Button is Allowed')
- break
- def before_save(self):
- # This is to save the preview image of the first page of the commit docs
- # This is done to show the preview image in the commit docs dashboard
- # This is done using the async function to capture the screenshot
- # The function is called using the frappe.enqueue method
- if self.sidebar:
- first = self.sidebar[0].docs_page
- domain = frappe.utils.get_url()
- if first:
- docs_url = f'{domain}/commit-docs/{self.route}/{first}'
- frappe.enqueue(method=save_preview_screenshot, url=docs_url,doctype=self.doctype,docname=self.name,field='preview_image')
-
- old_doc = self.get_doc_before_save()
- if old_doc:
- if old_doc.light_mode_logo != self.light_mode_logo:
- frappe.enqueue(method=save_webp_image,doctype=self.doctype,docname=self.name,image_field='light_mode_logo')
- if old_doc.night_mode_logo != self.night_mode_logo:
- frappe.enqueue(method=save_webp_image,doctype=self.doctype,docname=self.name,image_field='dark_mode_logo')
+ def before_insert(self):
+ """
+ Validate the Document
+ # 1. Check if the Route is Unique
+ """
+ if frappe.db.exists("Commit Docs", {"route": self.route}):
+ frappe.throw("Route Already Exists")
+
+ def validate(self):
+ # 2. Loop Over through the Navbar Items and check there should be only one is Primary Button
+ primary_button_count = 0
+ for navbar_item in self.navbar_items:
+ if navbar_item.is_primary_button:
+ primary_button_count += 1
+ if primary_button_count > 1:
+ frappe.throw("Only One Primary Button is Allowed")
+ break
+
+ def before_save(self):
+ # This is to save the preview image of the first page of the commit docs
+ # This is done to show the preview image in the commit docs dashboard
+ # This is done using the async function to capture the screenshot
+ # The function is called using the frappe.enqueue method
+ if self.sidebar:
+ first = self.sidebar[0].docs_page
+ domain = frappe.utils.get_url()
+ if first:
+ docs_url = f"{domain}/commit-docs/{self.route}/{first}"
+ frappe.enqueue(
+ method=save_preview_screenshot,
+ url=docs_url,
+ doctype=self.doctype,
+ docname=self.name,
+ field="preview_image",
+ )
+
+ old_doc = self.get_doc_before_save()
+ if old_doc:
+ if old_doc.light_mode_logo != self.light_mode_logo:
+ frappe.enqueue(
+ method=save_webp_image,
+ doctype=self.doctype,
+ docname=self.name,
+ image_field="light_mode_logo",
+ )
+ if old_doc.night_mode_logo != self.night_mode_logo:
+ frappe.enqueue(
+ method=save_webp_image,
+ doctype=self.doctype,
+ docname=self.name,
+ image_field="dark_mode_logo",
+ )
@frappe.whitelist()
-def get_docs_sidebar_parent_labels(id:str):
- '''
- Get the Parent Labels List of the Sidebar from Commit Docs
- '''
+def get_docs_sidebar_parent_labels(id: str):
+ """
+ Get the Parent Labels List of the Sidebar from Commit Docs
+ """
+
+ # Get the Commit Docs Document
+ commit_docs = frappe.get_cached_doc("Commit Docs", id)
- # Get the Commit Docs Document
- commit_docs = frappe.get_doc('Commit Docs', id)
+ parent_labels = []
- parent_labels = []
+ for sidebar in commit_docs.sidebar:
+ parent_labels.append(sidebar.parent_label)
- for sidebar in commit_docs.sidebar:
- parent_labels.append(sidebar.parent_label)
+ parent_labels = list(set(parent_labels))
- parent_labels = list(set(parent_labels))
+ parent_labels_obj = []
+ for label in parent_labels:
+ parent_labels_obj.append({"label": label, "value": label})
- parent_labels_obj = []
- for label in parent_labels:
- parent_labels_obj.append({
- 'label': label,
- 'value': label
- })
+ return parent_labels_obj
- return parent_labels_obj
@frappe.whitelist()
def get_all_commit_docs_detail():
- '''
- Get the All Commit Docs Details which are Published
- # 1. Get the Commit Docs Document from the route
- # 2. Check if the Commit Docs Document Published
- # 3. Return the Commit Docs Document
- # 4. Get The Sidebar Items for the Commit Docs
- # 5. Return the Sidebar Items
- '''
-
- # Get All the Commit Docs which are Published
- all_commit_docs = frappe.get_all('Commit Docs',{'published':1},'name')
+ """
+ Get the All Commit Docs Details which are Published
+ # 1. Get the Commit Docs Document from the route
+ # 2. Check if the Commit Docs Document Published
+ # 3. Return the Commit Docs Document
+ # 4. Get The Sidebar Items for the Commit Docs
+ # 5. Return the Sidebar Items
+ """
- # Maintain the Commit Docs Object
- commit_docs_obj = {}
+ # Get All the Commit Docs which are Published
+ all_commit_docs = frappe.get_all("Commit Docs", {"published": 1}, "name")
- for commit_docs in all_commit_docs:
- commit_docs = frappe.get_doc('Commit Docs',commit_docs.name).as_dict()
+ # Maintain the Commit Docs Object
+ commit_docs_obj = {}
- parse_doc = parse_commit_docs(commit_docs)
+ for commit_docs in all_commit_docs:
+ commit_docs = frappe.get_cached_doc("Commit Docs", commit_docs.name).as_dict()
- commit_docs_obj[commit_docs['route']] = parse_doc
-
- return commit_docs_obj
+ parse_doc = parse_commit_docs(commit_docs)
+
+ commit_docs_obj[commit_docs["route"]] = parse_doc
+
+ return commit_docs_obj
@frappe.whitelist(allow_guest=True)
-def get_commit_docs_details(route:str,show_hidden_items:bool=False):
- '''
- Get the Commit Docs Details
- # 1. Get the Commit Docs Document from the route
- # 2. Check if the Commit Docs Document Published
- # 3. Return the Commit Docs Document
- # 4. Get The Sidebar Items for the Commit Docs
- # 5. Return the Sidebar Items
- '''
- user = frappe.session.user
- # Check if the Commit Docs Document Exists
- if frappe.db.exists('Commit Docs',{'route':route}):
-
- if user == "Guest":
- if frappe.db.get_value('Commit Docs',{'route':route},'published'):
- commit_docs = frappe.get_doc('Commit Docs',{'route':route}).as_dict()
-
- return parse_commit_docs(commit_docs)
- else:
- return frappe.throw('Docs Not Published')
- else:
- commit_docs = frappe.get_doc('Commit Docs',{'route':route}).as_dict()
-
- return parse_commit_docs(commit_docs,show_hidden_items)
-
- else:
- return frappe.throw('Docs Not Found')
-
-
-def parse_commit_docs(commit_docs,show_hidden_items:bool=False):
-
- # Get the Sidebar Items
- sidebar_items = get_sidebar_items(commit_docs.sidebar,show_hidden_items)
-
- # Get the Footer Items
- footer_items = get_footer_items(commit_docs.footer,show_hidden_items)
-
- # Get the Navbar Items
- navbar_items = get_navbar_items(commit_docs.navbar_items,show_hidden_items)
-
- # remove the sidebar from the commit_docs as it is not needed
- commit_docs.pop('sidebar')
- commit_docs.pop('footer')
- commit_docs.pop('navbar_items')
-
- return {
- 'commit_docs': commit_docs,
- 'sidebar_items': sidebar_items,
- 'footer_items': footer_items,
- 'navbar_items': navbar_items,
- }
-
-def get_footer_items(footer,show_hidden_items:bool=False):
- '''
- Get the Footer Items
- # 1. Loop Over the Footer Items Which have Parent Label URL and Label
- # 2. Check if the Footer Item is Hide on Footer
- # 3. Return the Footer Items
- '''
- footer_obj = {}
- for footer_item in footer:
- if footer_item.hide_on_footer and not show_hidden_items:
- # If the footer item is hidden and show_hidden_items is False, skip it
- continue
-
- if footer_item.parent_label not in footer_obj:
- footer_obj[footer_item.parent_label] = [
- {
- 'label': footer_item.label,
- 'url': footer_item.url,
- 'hide_on_footer': footer_item.hide_on_footer
- }
- ]
- else:
- footer_obj[footer_item.parent_label].append({
- 'label': footer_item.label,
- 'url': footer_item.url,
- 'hide_on_footer': footer_item.hide_on_footer
- })
-
- return footer_obj
-
-def get_navbar_items(navbar,show_hidden_items:bool=False):
- '''
- Get the Navbar Items
- # 1. Loop Over the Navbar Items Which have Label, Parent Label, URL
- # 2. Check if the Navbar Item is Hide on Navbar
- # 3. Navbar Items are Nothing But Buttons which are displayed on the Navbar
- # 4. Parent Label is not Mandatory it is nothing but Like as Menu Button which has Sub Buttons
- '''
-
- navbar_obj = {}
- parent_labels = []
- for navbar_item in navbar:
- if navbar_item.hide_on_navbar and not show_hidden_items:
- continue
-
-
- if navbar_item.parent_label:
- parent_labels.append(navbar_item.parent_label)
- if navbar_item.parent_label not in navbar_obj:
- navbar_obj[navbar_item.parent_label] = {
- 'type':'Menu',
- 'label': navbar_item.parent_label,
- 'items': [{
- 'label': navbar_item.label,
- 'url': navbar_item.url,
- 'type': 'Button',
- 'icon': navbar_item.icon,
- 'open_in_new_tab': navbar_item.open_in_new_tab
- }],
- 'is_primary_button': navbar_item.is_primary_button,
- 'hide_on_navbar': navbar_item.hide_on_navbar
- }
- else:
- navbar_obj[navbar_item.parent_label]['items'].append({
- 'label': navbar_item.label,
- 'url': navbar_item.url,
- 'type': 'Button',
- 'icon': navbar_item.icon,
- 'open_in_new_tab': navbar_item.open_in_new_tab,
- })
- else:
- if navbar_item.url:
- navbar_obj[navbar_item.label] = {
- 'label': navbar_item.label,
- 'type': 'Button',
- 'icon': navbar_item.icon,
- 'open_in_new_tab': navbar_item.open_in_new_tab,
- 'url': navbar_item.url,
- 'is_primary_button': navbar_item.is_primary_button,
- 'hide_on_navbar': navbar_item.hide_on_navbar
- }
-
- # Remove that Object whose type is Button and Key is in Parent Labels
- button_type_keys = [key for key in navbar_obj if navbar_obj[key]['type'] == 'Button' and key in parent_labels]
- for key in button_type_keys:
- navbar_obj.pop(key)
-
- return navbar_obj
-
-def get_sidebar_items(sidebar,show_hidden_items:bool=False):
- '''
+def get_commit_docs_details(route: str, show_hidden_items: bool = False):
+ """
+ Get the Commit Docs Details
+ # 1. Get the Commit Docs Document from the route
+ # 2. Check if the Commit Docs Document Published
+ # 3. Return the Commit Docs Document
+ # 4. Get The Sidebar Items for the Commit Docs
+ # 5. Return the Sidebar Items
+ """
+ user = frappe.session.user
+ # Check if the Commit Docs Document Exists
+ if frappe.db.exists("Commit Docs", {"route": route}):
+
+ if user == "Guest":
+ if frappe.db.get_value("Commit Docs", {"route": route}, "published"):
+ commit_docs = frappe.get_doc("Commit Docs", {"route": route}).as_dict()
+
+ return parse_commit_docs(commit_docs)
+ else:
+ return frappe.throw("Docs Not Published")
+ else:
+ commit_docs = frappe.get_doc("Commit Docs", {"route": route}).as_dict()
+
+ return parse_commit_docs(commit_docs, show_hidden_items)
+
+ else:
+ return frappe.throw("Docs Not Found")
+
+
+def parse_commit_docs(commit_docs, show_hidden_items: bool = False):
+
+ # Get the Sidebar Items
+ sidebar_items = get_sidebar_items(commit_docs.sidebar, show_hidden_items)
+
+ # Get the Footer Items
+ footer_items = get_footer_items(commit_docs.footer, show_hidden_items)
+
+ # Get the Navbar Items
+ navbar_items = get_navbar_items(commit_docs.navbar_items, show_hidden_items)
+
+ # remove the sidebar from the commit_docs as it is not needed
+ commit_docs.pop("sidebar")
+ commit_docs.pop("footer")
+ commit_docs.pop("navbar_items")
+
+ return {
+ "commit_docs": commit_docs,
+ "sidebar_items": sidebar_items,
+ "footer_items": footer_items,
+ "navbar_items": navbar_items,
+ }
+
+
+def get_footer_items(footer, show_hidden_items: bool = False):
+ """
+ Get the Footer Items
+ # 1. Loop Over the Footer Items Which have Parent Label URL and Label
+ # 2. Check if the Footer Item is Hide on Footer
+ # 3. Return the Footer Items
+ """
+ footer_obj = {}
+ for footer_item in footer:
+ if footer_item.hide_on_footer and not show_hidden_items:
+ # If the footer item is hidden and show_hidden_items is False, skip it
+ continue
+
+ if footer_item.parent_label not in footer_obj:
+ footer_obj[footer_item.parent_label] = [
+ {
+ "label": footer_item.label,
+ "url": footer_item.url,
+ "hide_on_footer": footer_item.hide_on_footer,
+ }
+ ]
+ else:
+ footer_obj[footer_item.parent_label].append(
+ {
+ "label": footer_item.label,
+ "url": footer_item.url,
+ "hide_on_footer": footer_item.hide_on_footer,
+ }
+ )
+
+ return footer_obj
+
+
+def get_navbar_items(navbar, show_hidden_items: bool = False):
+ """
+ Get the Navbar Items
+ # 1. Loop Over the Navbar Items Which have Label, Parent Label, URL
+ # 2. Check if the Navbar Item is Hide on Navbar
+ # 3. Navbar Items are Nothing But Buttons which are displayed on the Navbar
+ # 4. Parent Label is not Mandatory it is nothing but Like as Menu Button which has Sub Buttons
+ """
+
+ navbar_obj = {}
+ parent_labels = []
+ for navbar_item in navbar:
+ if navbar_item.hide_on_navbar and not show_hidden_items:
+ continue
+
+ if navbar_item.parent_label:
+ parent_labels.append(navbar_item.parent_label)
+ if navbar_item.parent_label not in navbar_obj:
+ navbar_obj[navbar_item.parent_label] = {
+ "type": "Menu",
+ "label": navbar_item.parent_label,
+ "items": [
+ {
+ "label": navbar_item.label,
+ "url": navbar_item.url,
+ "type": "Button",
+ "icon": navbar_item.icon,
+ "open_in_new_tab": navbar_item.open_in_new_tab,
+ }
+ ],
+ "is_primary_button": navbar_item.is_primary_button,
+ "hide_on_navbar": navbar_item.hide_on_navbar,
+ }
+ else:
+ navbar_obj[navbar_item.parent_label]["items"].append(
+ {
+ "label": navbar_item.label,
+ "url": navbar_item.url,
+ "type": "Button",
+ "icon": navbar_item.icon,
+ "open_in_new_tab": navbar_item.open_in_new_tab,
+ }
+ )
+ else:
+ if navbar_item.url:
+ navbar_obj[navbar_item.label] = {
+ "label": navbar_item.label,
+ "type": "Button",
+ "icon": navbar_item.icon,
+ "open_in_new_tab": navbar_item.open_in_new_tab,
+ "url": navbar_item.url,
+ "is_primary_button": navbar_item.is_primary_button,
+ "hide_on_navbar": navbar_item.hide_on_navbar,
+ }
+
+ # Remove that Object whose type is Button and Key is in Parent Labels
+ button_type_keys = [
+ key
+ for key in navbar_obj
+ if navbar_obj[key]["type"] == "Button" and key in parent_labels
+ ]
+ for key in button_type_keys:
+ navbar_obj.pop(key)
+
+ return navbar_obj
+
+
+def get_sidebar_items(sidebar, show_hidden_items: bool = False):
+ """
Get the Sidebar Items with support for nested Group Pages.
- '''
+ """
+
def get_group_items(commit_docs_page):
"""
Recursive function to fetch items for a Group Page, handling nested groups.
@@ -250,58 +282,70 @@ def get_group_items(commit_docs_page):
group_items = []
for group_item in commit_docs_page.linked_pages:
# Get the document for each linked page
- group_commit_docs_page = frappe.get_doc('Commit Docs Page', group_item.commit_docs_page)
+ group_commit_docs_page = frappe.get_cached_doc(
+ "Commit Docs Page", group_item.commit_docs_page
+ )
# Check permissions and publication status
- permitted = group_commit_docs_page.allow_guest or frappe.session.user != 'Guest'
- published = group_commit_docs_page.published or frappe.session.user != 'Guest'
+ permitted = (
+ group_commit_docs_page.allow_guest or frappe.session.user != "Guest"
+ )
+ published = (
+ group_commit_docs_page.published or frappe.session.user != "Guest"
+ )
if not permitted or not published:
continue
-
+
# Check if the linked page is also a Group Page
is_nested_group_page = group_commit_docs_page.is_group_page
# If it's a nested Group Page, recursively fetch its group items
if is_nested_group_page:
nested_group_items = get_group_items(group_commit_docs_page)
- group_items.append({
- 'name': group_commit_docs_page.name,
- 'type': 'Docs Page',
- 'title': group_commit_docs_page.title,
- 'route': group_commit_docs_page.route,
- 'badge': group_commit_docs_page.badge,
- 'badge_color': group_commit_docs_page.badge_color,
- 'icon': group_commit_docs_page.icon,
- 'parent_name': commit_docs_page.name,
- 'is_group_page': True,
- 'group_items': nested_group_items,
- 'idx': group_commit_docs_page.idx
- })
+ group_items.append(
+ {
+ "name": group_commit_docs_page.name,
+ "type": "Docs Page",
+ "title": group_commit_docs_page.title,
+ "route": group_commit_docs_page.route,
+ "badge": group_commit_docs_page.badge,
+ "badge_color": group_commit_docs_page.badge_color,
+ "icon": group_commit_docs_page.icon,
+ "parent_name": commit_docs_page.name,
+ "is_group_page": True,
+ "group_items": nested_group_items,
+ "idx": group_commit_docs_page.idx,
+ }
+ )
else:
# If it's a regular Docs Page, add it directly
- group_items.append({
- 'name': group_commit_docs_page.name,
- 'type': 'Docs Page',
- 'title': group_commit_docs_page.title,
- 'route': group_commit_docs_page.route,
- 'badge': group_commit_docs_page.badge,
- 'badge_color': group_commit_docs_page.badge_color,
- 'icon': group_commit_docs_page.icon,
- 'parent_name': commit_docs_page.name,
- 'idx': group_commit_docs_page.idx
- })
- return sorted(group_items, key=lambda x: x['idx'])
+ group_items.append(
+ {
+ "name": group_commit_docs_page.name,
+ "type": "Docs Page",
+ "title": group_commit_docs_page.title,
+ "route": group_commit_docs_page.route,
+ "badge": group_commit_docs_page.badge,
+ "badge_color": group_commit_docs_page.badge_color,
+ "icon": group_commit_docs_page.icon,
+ "parent_name": commit_docs_page.name,
+ "idx": group_commit_docs_page.idx,
+ }
+ )
+ return sorted(group_items, key=lambda x: x["idx"])
sidebar_obj = {}
for sidebar_item in sidebar: # Preserve the original order of the sidebar
if sidebar_item.hide_on_sidebar and not show_hidden_items:
continue
- commit_docs_page = frappe.get_doc('Commit Docs Page', sidebar_item.docs_page)
+ commit_docs_page = frappe.get_cached_doc(
+ "Commit Docs Page", sidebar_item.docs_page
+ )
- permitted = commit_docs_page.allow_guest or frappe.session.user != 'Guest'
- published = commit_docs_page.published or frappe.session.user != 'Guest'
+ permitted = commit_docs_page.allow_guest or frappe.session.user != "Guest"
+ published = commit_docs_page.published or frappe.session.user != "Guest"
is_group_page = commit_docs_page.is_group_page
if not permitted or not published:
@@ -312,18 +356,18 @@ def get_group_items(commit_docs_page):
# Prepare sidebar entry with group items if it exists
sidebar_entry = {
- 'name': commit_docs_page.name,
- 'type': 'Docs Page',
- 'title': commit_docs_page.title,
- 'route': commit_docs_page.route,
- 'badge': commit_docs_page.badge,
- 'badge_color': commit_docs_page.badge_color,
- 'icon': commit_docs_page.icon,
- 'group_name': sidebar_item.parent_label,
- 'is_group_page': is_group_page,
- 'group_items': group_items if is_group_page else None,
- 'idx': commit_docs_page.idx,
- 'hide_on_sidebar': sidebar_item.hide_on_sidebar
+ "name": commit_docs_page.name,
+ "type": "Docs Page",
+ "title": commit_docs_page.title,
+ "route": commit_docs_page.route,
+ "badge": commit_docs_page.badge,
+ "badge_color": commit_docs_page.badge_color,
+ "icon": commit_docs_page.icon,
+ "group_name": sidebar_item.parent_label,
+ "is_group_page": is_group_page,
+ "group_items": group_items if is_group_page else None,
+ "idx": commit_docs_page.idx,
+ "hide_on_sidebar": sidebar_item.hide_on_sidebar,
}
# Add sidebar entry to the parent label
@@ -334,200 +378,244 @@ def get_group_items(commit_docs_page):
return sidebar_obj
+
@frappe.whitelist(allow_guest=True)
-def get_first_page_route(route:str):
- '''
- Get the First Page Route from the Commit Docs
- '''
- if frappe.db.exists('Commit Docs',{'route':route}):
- commit_docs = frappe.get_doc('Commit Docs',{'route':route})
- found = False
- for sidebar in commit_docs.sidebar:
- commit_docs_page = frappe.get_doc('Commit Docs Page',sidebar.docs_page)
- if commit_docs_page.published:
- found = True
- return commit_docs_page.route
-
- if not found:
- return frappe.throw('Create and Publish the First Page')
-
- else:
- return frappe.throw('Commit Docs Not Found')
+def get_first_page_route(route: str):
+ """
+ Get the First Page Route from the Commit Docs
+ """
+ if frappe.db.exists("Commit Docs", {"route": route}):
+ commit_docs = frappe.get_doc("Commit Docs", {"route": route})
+ found = False
+ for sidebar in commit_docs.sidebar:
+ commit_docs_page = frappe.get_cached_doc(
+ "Commit Docs Page", sidebar.docs_page
+ )
+ if commit_docs_page.published:
+ found = True
+ return commit_docs_page.route
+
+ if not found:
+ return frappe.throw("Create and Publish the First Page")
+
+ else:
+ return frappe.throw("Commit Docs Not Found")
+
@frappe.whitelist(allow_guest=True)
def get_commit_docs_list():
- '''
- Get the List of Commit Docs
- '''
- user = frappe.session.user
- filters = {}
- if user == "Guest":
- filters['published'] = 1
-
- commit_docs_list = frappe.get_all('Commit Docs',
- filters=filters,
- fields=["header", "light_mode_logo", "route", "published", "description","name"],
- )
+ """
+ Get the List of Commit Docs
+ """
+ user = frappe.session.user
+ filters = {}
+ if user == "Guest":
+ filters["published"] = 1
+
+ commit_docs_list = frappe.get_all(
+ "Commit Docs",
+ filters=filters,
+ fields=[
+ "header",
+ "light_mode_logo",
+ "route",
+ "published",
+ "description",
+ "name",
+ ],
+ )
+
+ return commit_docs_list
- return commit_docs_list
@frappe.whitelist(methods=["POST"])
-def manage_sidebar(commit_doc:str,parent_labels,docs_page):
- '''
- This is to modify the sidebar items of the commit docs
- @param commit_doc: The Commit Docs ID
- @param parent_labels: The Parent Labels of the Sidebar List
- @param docs_page: List of Object having docs page and parent label
-
- # 1. Get the Commit Docs Document
- # 2. Loop Over the Parent Labels
- # 3. Look for the Parent Label in docs_page List of Object
- # 4. for loop on that filtered list append the docs_page and parent label to the sidebar
- # 5. Save the Sidebar Items
- '''
-
- # Get the Commit Docs Document
- doc = frappe.get_doc('Commit Docs',commit_doc)
-
- # Loop Over the Parent Labels
- if isinstance(parent_labels, str):
- parent_labels = json.loads(parent_labels)
-
- if isinstance(docs_page, str):
- docs_page = json.loads(docs_page)
-
- doc.sidebar = []
- for parent_label in parent_labels:
- # Filter the docs_page List of Object
- filtered_docs_page = [item for item in docs_page if item.get('columnId') == parent_label]
-
- # Check if there are any duplicate docs_page
- duplicate = set()
- for item in filtered_docs_page:
- if item.get('id') in duplicate:
- frappe.throw(f'You have Duplicate Docs Page {item.get("id")} in Same Parent Label {parent_label}')
- duplicate.add(item.get('id'))
-
- # sort by index field
- filtered_docs_page = sorted(filtered_docs_page, key=lambda x: x.get('index', 0))
-
- # Loop Over the Filtered List
- for item in filtered_docs_page:
- # Append the docs_page and parent label to the sidebar
- doc.append('sidebar',{
- 'parent_label': parent_label,
- 'docs_page': item.get('id'),
- })
-
- doc.save()
-
- return doc
+def manage_sidebar(commit_doc: str, parent_labels, docs_page):
+ """
+ This is to modify the sidebar items of the commit docs
+ @param commit_doc: The Commit Docs ID
+ @param parent_labels: The Parent Labels of the Sidebar List
+ @param docs_page: List of Object having docs page and parent label
+
+ # 1. Get the Commit Docs Document
+ # 2. Loop Over the Parent Labels
+ # 3. Look for the Parent Label in docs_page List of Object
+ # 4. for loop on that filtered list append the docs_page and parent label to the sidebar
+ # 5. Save the Sidebar Items
+ """
+
+ # Get the Commit Docs Document
+ doc = frappe.get_cached_doc("Commit Docs", commit_doc)
+
+ # Loop Over the Parent Labels
+ if isinstance(parent_labels, str):
+ parent_labels = json.loads(parent_labels)
+
+ if isinstance(docs_page, str):
+ docs_page = json.loads(docs_page)
+
+ doc.sidebar = []
+ for parent_label in parent_labels:
+ # Filter the docs_page List of Object
+ filtered_docs_page = [
+ item for item in docs_page if item.get("columnId") == parent_label
+ ]
+
+ # Check if there are any duplicate docs_page
+ duplicate = set()
+ for item in filtered_docs_page:
+ if item.get("id") in duplicate:
+ frappe.throw(
+ f'You have Duplicate Docs Page {item.get("id")} in Same Parent Label {parent_label}'
+ )
+ duplicate.add(item.get("id"))
+
+ # sort by index field
+ filtered_docs_page = sorted(filtered_docs_page, key=lambda x: x.get("index", 0))
+
+ # Loop Over the Filtered List
+ for item in filtered_docs_page:
+ # Append the docs_page and parent label to the sidebar
+ doc.append(
+ "sidebar",
+ {
+ "parent_label": parent_label,
+ "docs_page": item.get("id"),
+ },
+ )
+
+ doc.save()
+
+ return doc
+
@frappe.whitelist(methods=["POST"])
-def manage_navbar(commit_doc:str, navbar_items, sub_navbar_items=None):
- '''
- This is to modify the navbar items of the commit docs
- @param commit_doc: The Commit Docs ID
- @param navbar_items: The Navbar Items List of Object having label, url, parent label, icon, open_in_new_tab
-
- # 1. Get the Commit Docs Document
- # 2. Loop Over the Navbar Items
- # 3. Append the Navbar Items to the Navbar Items Table
- # 4. Save the Navbar Items
- '''
-
- doc = frappe.get_doc('Commit Docs',commit_doc)
-
- if isinstance(navbar_items, str):
- navbar_items = json.loads(navbar_items)
-
- if isinstance(sub_navbar_items, str):
- sub_navbar_items = json.loads(sub_navbar_items)
-
- doc.navbar_items = []
- # sort the navbar_items by index field
- navbar_items = sorted(navbar_items, key=lambda x: x.get('index', 0))
-
- for item in navbar_items:
- if item.get('type') == "Menu":
- doc.append('navbar_items',{
- 'label': item.get('label'),
- 'hide_on_navbar': item.get('hide_on_navbar'),
- })
- if sub_navbar_items:
- # find the task in the sub_navbar_items where columnId is equal to item.get('label')
- sub_items = [sub_item for sub_item in sub_navbar_items if sub_item.get('columnId') == item.get('label')]
- # sort the sub_items by index field
- sub_items = sorted(sub_items, key=lambda x: x.get('index', 0))
- # Loop Over the Sub Items
- for sub_item in sub_items:
- doc.append('navbar_items',{
- 'label': sub_item.get('label'),
- 'url': sub_item.get('url'),
- 'icon': sub_item.get('icon'),
- 'open_in_new_tab': sub_item.get('open_in_new_tab'),
- "parent_label": item.get('label'),
- })
-
- else:
- doc.append('navbar_items',{
- 'label': item.get('label'),
- 'url': item.get('url'),
- 'icon': item.get('icon'),
- 'open_in_new_tab': item.get('open_in_new_tab'),
- 'hide_on_navbar': item.get('hide_on_navbar'),
- 'is_primary_button': item.get('is_primary_button')
- })
-
- doc.save()
-
- return doc
+def manage_navbar(commit_doc: str, navbar_items, sub_navbar_items=None):
+ """
+ This is to modify the navbar items of the commit docs
+ @param commit_doc: The Commit Docs ID
+ @param navbar_items: The Navbar Items List of Object having label, url, parent label, icon, open_in_new_tab
+
+ # 1. Get the Commit Docs Document
+ # 2. Loop Over the Navbar Items
+ # 3. Append the Navbar Items to the Navbar Items Table
+ # 4. Save the Navbar Items
+ """
+
+ doc = frappe.get_cached_doc("Commit Docs", commit_doc)
+
+ if isinstance(navbar_items, str):
+ navbar_items = json.loads(navbar_items)
+
+ if isinstance(sub_navbar_items, str):
+ sub_navbar_items = json.loads(sub_navbar_items)
+
+ doc.navbar_items = []
+ # sort the navbar_items by index field
+ navbar_items = sorted(navbar_items, key=lambda x: x.get("index", 0))
+
+ for item in navbar_items:
+ if item.get("type") == "Menu":
+ doc.append(
+ "navbar_items",
+ {
+ "label": item.get("label"),
+ "hide_on_navbar": item.get("hide_on_navbar"),
+ },
+ )
+ if sub_navbar_items:
+ # find the task in the sub_navbar_items where columnId is equal to item.get('label')
+ sub_items = [
+ sub_item
+ for sub_item in sub_navbar_items
+ if sub_item.get("columnId") == item.get("id")
+ ]
+ # sort the sub_items by index field
+ sub_items = sorted(sub_items, key=lambda x: x.get("index", 0))
+ # Loop Over the Sub Items
+ for sub_item in sub_items:
+ doc.append(
+ "navbar_items",
+ {
+ "label": sub_item.get("label"),
+ "url": sub_item.get("url"),
+ "icon": sub_item.get("icon"),
+ "open_in_new_tab": sub_item.get("open_in_new_tab"),
+ "parent_label": item.get("label"),
+ },
+ )
+
+ else:
+ doc.append(
+ "navbar_items",
+ {
+ "label": item.get("label"),
+ "url": item.get("url"),
+ "icon": item.get("icon"),
+ "open_in_new_tab": item.get("open_in_new_tab"),
+ "hide_on_navbar": item.get("hide_on_navbar"),
+ "is_primary_button": item.get("is_primary_button"),
+ },
+ )
+
+ doc.save()
+
+ return doc
+
@frappe.whitelist(methods=["POST"])
-def manage_footer(commit_doc:str, footer_columns, footer_items):
- '''
- This is to modify the footer items of the commit docs
- @param commit_doc: The Commit Docs ID
- @param footer_columns: The Footer Columns List of Parent Label
- @param footer_items: The Footer Items List of Object having label, url, hide_on_footer, columnId,id
-
- # 1. Get the Commit Docs Document
- # 2. Loop Over the Footer Columns
- # 3. Search for the Parent Label in the Footer Items
- # 4. Loop over the filtered list and append the footer items to the footer
- # 5. Save the Footer Items
- '''
-
- doc = frappe.get_doc('Commit Docs',commit_doc)
- if isinstance(footer_columns, str):
- footer_columns = json.loads(footer_columns)
-
- if isinstance(footer_items, str):
- footer_items = json.loads(footer_items)
-
- doc.footer = []
- for parent_label in footer_columns:
- # Filter the footer_items List of Object
- filtered_footer_items = [item for item in footer_items if item.get('columnId') == parent_label]
-
- # Check if there are any duplicate footer_items
- duplicate = set()
- for item in filtered_footer_items:
- if item.get('id') in duplicate:
- frappe.throw(f'You have Duplicate Footer Item {item.get("id")} in Same Parent Label {parent_label}')
- duplicate.add(item.get('id'))
-
- # sort by index field
- filtered_footer_items = sorted(filtered_footer_items, key=lambda x: x.get('index', 0))
-
- for item in filtered_footer_items:
- doc.append('footer',{
- 'label': item.get('label'),
- 'url': item.get('url'),
- 'hide_on_footer': item.get('hide_on_footer'),
- 'parent_label': parent_label,
- })
-
- doc.save()
-
- return doc
\ No newline at end of file
+def manage_footer(commit_doc: str, footer_columns, footer_items):
+ """
+ This is to modify the footer items of the commit docs
+ @param commit_doc: The Commit Docs ID
+ @param footer_columns: The Footer Columns List of Parent Label
+ @param footer_items: The Footer Items List of Object having label, url, hide_on_footer, columnId,id
+
+ # 1. Get the Commit Docs Document
+ # 2. Loop Over the Footer Columns
+ # 3. Search for the Parent Label in the Footer Items
+ # 4. Loop over the filtered list and append the footer items to the footer
+ # 5. Save the Footer Items
+ """
+
+ doc = frappe.get_cached_doc("Commit Docs", commit_doc)
+ if isinstance(footer_columns, str):
+ footer_columns = json.loads(footer_columns)
+
+ if isinstance(footer_items, str):
+ footer_items = json.loads(footer_items)
+
+ doc.footer = []
+ for parent_label in footer_columns:
+ # Filter the footer_items List of Object
+ filtered_footer_items = [
+ item for item in footer_items if item.get("columnId") == parent_label
+ ]
+
+ # Check if there are any duplicate footer_items
+ duplicate = set()
+ for item in filtered_footer_items:
+ if item.get("id") in duplicate:
+ frappe.throw(
+ f'You have Duplicate Footer Item {item.get("id")} in Same Parent Label {parent_label}'
+ )
+ duplicate.add(item.get("id"))
+
+ # sort by index field
+ filtered_footer_items = sorted(
+ filtered_footer_items, key=lambda x: x.get("index", 0)
+ )
+
+ for item in filtered_footer_items:
+ doc.append(
+ "footer",
+ {
+ "label": item.get("label"),
+ "url": item.get("url"),
+ "hide_on_footer": item.get("hide_on_footer"),
+ "parent_label": parent_label,
+ },
+ )
+
+ doc.save()
+
+ return doc
diff --git a/commit/commit/doctype/commit_docs/test_commit_docs.py b/commit/commit/doctype/commit_docs/test_commit_docs.py
index 16a9eec..48d5af8 100644
--- a/commit/commit/doctype/commit_docs/test_commit_docs.py
+++ b/commit/commit/doctype/commit_docs/test_commit_docs.py
@@ -6,4 +6,4 @@
class TestCommitDocs(FrappeTestCase):
- pass
+ pass
diff --git a/commit/commit/doctype/commit_docs_footer_item/commit_docs_footer_item.json b/commit/commit/doctype/commit_docs_footer_item/commit_docs_footer_item.json
index 086ee0e..a3a8ea2 100644
--- a/commit/commit/doctype/commit_docs_footer_item/commit_docs_footer_item.json
+++ b/commit/commit/doctype/commit_docs_footer_item/commit_docs_footer_item.json
@@ -53,4 +53,4 @@
"sort_field": "creation",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/commit/commit/doctype/commit_docs_footer_item/commit_docs_footer_item.py b/commit/commit/doctype/commit_docs_footer_item/commit_docs_footer_item.py
index 731271e..0915636 100644
--- a/commit/commit/doctype/commit_docs_footer_item/commit_docs_footer_item.py
+++ b/commit/commit/doctype/commit_docs_footer_item/commit_docs_footer_item.py
@@ -6,4 +6,4 @@
class CommitDocsFooterItem(Document):
- pass
+ pass
diff --git a/commit/commit/doctype/commit_docs_group_item/commit_docs_group_item.json b/commit/commit/doctype/commit_docs_group_item/commit_docs_group_item.json
index 44023cc..1fb66f5 100644
--- a/commit/commit/doctype/commit_docs_group_item/commit_docs_group_item.json
+++ b/commit/commit/doctype/commit_docs_group_item/commit_docs_group_item.json
@@ -45,4 +45,4 @@
"sort_field": "creation",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/commit/commit/doctype/commit_docs_group_item/commit_docs_group_item.py b/commit/commit/doctype/commit_docs_group_item/commit_docs_group_item.py
index 04eb61e..aeebba0 100644
--- a/commit/commit/doctype/commit_docs_group_item/commit_docs_group_item.py
+++ b/commit/commit/doctype/commit_docs_group_item/commit_docs_group_item.py
@@ -6,4 +6,4 @@
class CommitDocsGroupItem(Document):
- pass
+ pass
diff --git a/commit/commit/doctype/commit_docs_page/commit_docs_page.json b/commit/commit/doctype/commit_docs_page/commit_docs_page.json
index c15b38a..0de8479 100644
--- a/commit/commit/doctype/commit_docs_page/commit_docs_page.json
+++ b/commit/commit/doctype/commit_docs_page/commit_docs_page.json
@@ -53,7 +53,7 @@
{
"depends_on": "eval:doc.is_group_page == 0",
"fieldname": "content",
- "fieldtype": "Markdown Editor",
+ "fieldtype": "Code",
"ignore_xss_filter": 1,
"label": "Content"
},
@@ -114,7 +114,7 @@
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2025-03-15 17:10:43.887534",
+ "modified": "2025-05-23 14:48:34.393866",
"modified_by": "Administrator",
"module": "commit",
"name": "Commit Docs Page",
@@ -139,4 +139,4 @@
"sort_order": "DESC",
"states": [],
"title_field": "title"
-}
\ No newline at end of file
+}
diff --git a/commit/commit/doctype/commit_docs_page/commit_docs_page.py b/commit/commit/doctype/commit_docs_page/commit_docs_page.py
index 63f3985..342d197 100644
--- a/commit/commit/doctype/commit_docs_page/commit_docs_page.py
+++ b/commit/commit/doctype/commit_docs_page/commit_docs_page.py
@@ -1,206 +1,304 @@
# Copyright (c) 2024, The Commit Company and contributors
# For license information, please see license.txt
+import json
+import re
+
import frappe
+from bs4 import BeautifulSoup
from frappe.model.document import Document
-import json
+
from commit.api.preview import save_preview_screenshot
+
class CommitDocsPage(Document):
-
- def before_insert(self):
- # Set the route for the page based on the title
- self.route = f'{self.commit_docs.lower().replace(" ", "-")}-{self.title.lower().replace(" ", "-")}'
-
- def before_save(self):
- # Check if this document is first item of commit docs sidebar child table
- if self.commit_docs:
- commit_docs = frappe.get_cached_doc('Commit Docs', self.commit_docs)
- if commit_docs.sidebar:
- first = commit_docs.sidebar[0]
- if first.docs_page == self.name:
- domain = frappe.utils.get_url()
- docs_url = f'{domain}/commit-docs/{commit_docs.route}/{self.name}'
- frappe.enqueue(method=save_preview_screenshot, url=docs_url,doctype="Commit Docs",docname=commit_docs.name,field='preview_image')
-
-@frappe.whitelist(methods=['POST'])
-def publish_documentation(project_branch, endpoint, viewer_type, docs_name, parent_label, title, published, allow_guest, content):
- '''
- Publish the Documentation
- # 1. Create a new Commit Docs Page Document
- # 2. Update the Commit Docs Document with the new Page by adding it to the Sidebar child table
- # 3. Based on viewer_type, update the flag is_published, published_on, published_by, publish_id
- '''
-
- # 1. Create a new Commit Docs Page Document
- commit_docs_page = frappe.get_doc({
- 'doctype': 'Commit Docs Page',
- 'title': title,
- 'published': published,
- 'allow_guest': allow_guest,
- 'content': content
- })
-
- commit_docs_page.insert()
-
- # 2. Update the Commit Docs Document with the new Page by adding it to the Sidebar child table
-
- commit_docs = frappe.get_doc('Commit Docs', docs_name)
-
- commit_docs.append('sidebar', {
- 'parent_label': parent_label,
- 'docs_page': commit_docs_page.name
- })
-
- commit_docs.save()
-
- # 3. Check the viewer_type
-
- if viewer_type == "project":
- # Get the Project Branch Document
- project_branch_doc = frappe.get_doc('Commit Project Branch', project_branch)
-
- # Get the documentation JSON
- documentation = json.loads(project_branch_doc.documentation).get("apis", []) if project_branch_doc.documentation else []
-
- if documentation:
- # Find the API from the documentation JSON
- api = next((api for api in documentation if api.get('path') == endpoint), None)
-
- if api:
- # Update the API with the published_on, published_by, is_published, publish_id
- api['published_on'] = commit_docs_page.creation
- api['published_by'] = frappe.session.user
- api['is_published'] = 1
- api['publish_id'] = commit_docs_page.name
- api['published_route'] = f'{commit_docs.route}/{commit_docs_page.route}'
-
- project_branch_doc.documentation = json.dumps({"apis": documentation})
- project_branch_doc.save()
-
- else:
- commit_branch_documentation= frappe.get_doc('Commit Branch Documentation', project_branch)
-
- # Get the documentation JSON
- documentation = json.loads(commit_branch_documentation.documentation).get("apis", []) if commit_branch_documentation.documentation else []
-
- if documentation:
- # Find the API from the documentation JSON
- api = next((api for api in documentation if api.get('path') == endpoint), None)
-
- if api:
- # Update the API with the published_on, published_by, is_published, publish_id
- api['published_on'] = commit_docs_page.creation
- api['published_by'] = frappe.session.user
- api['is_published'] = 1
- api['publish_id'] = commit_docs_page.name
- api['published_route'] = f'{commit_docs.route}/{commit_docs_page.route}'
-
- commit_branch_documentation.documentation = json.dumps({"apis": documentation})
- commit_branch_documentation.save()
-
- return {
- 'commit_docs_page': commit_docs_page.name,
- 'commit_docs': commit_docs.name
- }
+
+ def before_insert(self):
+ # Set the route for the page based on the title
+ self.route = f'{self.commit_docs.lower().replace(" ", "-")}-{self.title.lower().replace(" ", "-")}'
+
+ def before_save(self):
+ # Check if this document is first item of commit docs sidebar child table
+ if self.commit_docs:
+ commit_docs = frappe.get_cached_doc("Commit Docs", self.commit_docs)
+ if commit_docs.sidebar:
+ first = commit_docs.sidebar[0]
+ if first.docs_page == self.name:
+ domain = frappe.utils.get_url()
+ docs_url = f"{domain}/commit-docs/{commit_docs.route}/{self.name}"
+ frappe.enqueue(
+ method=save_preview_screenshot,
+ url=docs_url,
+ doctype="Commit Docs",
+ docname=commit_docs.name,
+ field="preview_image",
+ )
+
+ def get_docs_url(self):
+ """
+ Get the URL for the Commit Docs Page
+ """
+ if self.commit_docs:
+ commit_docs_route = frappe.get_cached_value(
+ "Commit Docs", self.commit_docs, "route"
+ )
+ domain = frappe.utils.get_url()
+ return f"{domain}/commit-docs/{commit_docs_route}/{self.route}"
+ return None
+
+
+@frappe.whitelist(methods=["POST"])
+def publish_documentation(
+ project_branch,
+ endpoint,
+ viewer_type,
+ docs_name,
+ parent_label,
+ title,
+ published,
+ allow_guest,
+ content,
+):
+ """
+ Publish the Documentation
+ # 1. Create a new Commit Docs Page Document
+ # 2. Update the Commit Docs Document with the new Page by adding it to the Sidebar child table
+ # 3. Based on viewer_type, update the flag is_published, published_on, published_by, publish_id
+ """
+
+ # 1. Create a new Commit Docs Page Document
+ commit_docs_page = frappe.get_cached_doc(
+ {
+ "doctype": "Commit Docs Page",
+ "title": title,
+ "published": published,
+ "allow_guest": allow_guest,
+ "content": content,
+ }
+ )
+
+ commit_docs_page.insert()
+
+ # 2. Update the Commit Docs Document with the new Page by adding it to the Sidebar child table
+
+ commit_docs = frappe.get_cached_doc("Commit Docs", docs_name)
+
+ commit_docs.append(
+ "sidebar", {"parent_label": parent_label, "docs_page": commit_docs_page.name}
+ )
+
+ commit_docs.save()
+
+ # 3. Check the viewer_type
+
+ if viewer_type == "project":
+ # Get the Project Branch Document
+ project_branch_doc = frappe.get_cached_doc(
+ "Commit Project Branch", project_branch
+ )
+
+ # Get the documentation JSON
+ documentation = (
+ json.loads(project_branch_doc.documentation).get("apis", [])
+ if project_branch_doc.documentation
+ else []
+ )
+
+ if documentation:
+ # Find the API from the documentation JSON
+ api = next(
+ (api for api in documentation if api.get("path") == endpoint), None
+ )
+
+ if api:
+ # Update the API with the published_on, published_by, is_published, publish_id
+ api["published_on"] = commit_docs_page.creation
+ api["published_by"] = frappe.session.user
+ api["is_published"] = 1
+ api["publish_id"] = commit_docs_page.name
+ api["published_route"] = f"{commit_docs.route}/{commit_docs_page.route}"
+
+ project_branch_doc.documentation = json.dumps({"apis": documentation})
+ project_branch_doc.save()
+
+ else:
+ commit_branch_documentation = frappe.get_cached_doc(
+ "Commit Branch Documentation", project_branch
+ )
+
+ # Get the documentation JSON
+ documentation = (
+ json.loads(commit_branch_documentation.documentation).get("apis", [])
+ if commit_branch_documentation.documentation
+ else []
+ )
+
+ if documentation:
+ # Find the API from the documentation JSON
+ api = next(
+ (api for api in documentation if api.get("path") == endpoint), None
+ )
+
+ if api:
+ # Update the API with the published_on, published_by, is_published, publish_id
+ api["published_on"] = commit_docs_page.creation
+ api["published_by"] = frappe.session.user
+ api["is_published"] = 1
+ api["publish_id"] = commit_docs_page.name
+ api["published_route"] = f"{commit_docs.route}/{commit_docs_page.route}"
+
+ commit_branch_documentation.documentation = json.dumps(
+ {"apis": documentation}
+ )
+ commit_branch_documentation.save()
+
+ return {"commit_docs_page": commit_docs_page.name, "commit_docs": commit_docs.name}
+
@frappe.whitelist(allow_guest=True)
def get_commit_docs_page(name):
- '''
- Get the Commit Docs Page
- '''
- user = frappe.session.user
-
- doc = frappe.get_cached_doc('Commit Docs Page', name)
+ """
+ Get the Commit Docs Page
+ """
+ user = frappe.session.user
+
+ doc = frappe.get_cached_doc("Commit Docs Page", name)
+
+ if user == "Guest" and not doc.allow_guest and not doc.published:
+ frappe.throw("You are not allowed to view this page")
+
+ def process_codeblocks(md):
+ # 1. Remove code fences for ```JSX blocks (render as HTML)
+ def jsx_repl(match):
+ code = match.group(1)
+ return code # Just the code, no code block
+
+ md = re.sub(r"```JSX\n(.*?)```", jsx_repl, md, flags=re.DOTALL)
+
+ # 2. Change ```React to ```jsx (lowercase)
+ def react_repl(match):
+ code = match.group(1)
+ return f"```jsx\n{code}```"
- if user == "Guest" and not doc.allow_guest and not doc.published:
- frappe.throw("You are not allowed to view this page")
+ md = re.sub(r"```React\n(.*?)```", react_repl, md, flags=re.DOTALL)
- # Get the content as HTML
- html = frappe.utils.md_to_html(doc.content)
+ # 3. Lowercase all other code block languages (except jsx, already handled)
+ def lower_repl(match):
+ lang = match.group(1)
+ code = match.group(2)
+ if lang.lower() in ["jsx", "react"]:
+ return match.group(0) # Already handled
+ return f"```{lang.lower()}\n{code}```"
- # Calculate the Table of Contents
- toc_obj = calculate_toc_object(html)
+ md = re.sub(r"```(\w+)\n(.*?)```", lower_repl, md, flags=re.DOTALL)
- return {
- 'doc': doc,
- 'toc_obj': toc_obj
- }
+ return md
+
+ doc.content = process_codeblocks(doc.content)
+
+ # Get the content as HTML
+ html = frappe.utils.md_to_html(doc.content)
+
+ # Calculate the Table of Contents
+ toc_obj = calculate_toc_object(html)
+
+ return {"doc": doc, "toc_obj": toc_obj}
def calculate_toc_object(html):
- from bs4 import BeautifulSoup
import re
+ from bs4 import BeautifulSoup
+
soup = BeautifulSoup(html, "html.parser")
headings = soup.find_all(["h2", "h3", "h4", "h5", "h6"])
toc = {}
+ stack = [] # To keep track of the current hierarchy
def add_to_toc(toc, level, heading_id, title):
- if level == 2:
- toc[heading_id] = {"name": title, "children": {}}
+ # Ensure the stack is consistent with the current level
+ while stack and stack[-1]["level"] >= level:
+ stack.pop()
+
+ # Create the new heading entry
+ new_entry = {"id": heading_id, "name": title, "children": {}}
+
+ if not stack:
+ # Top-level heading
+ toc[heading_id] = new_entry
+ stack.append({"level": level, "children": toc[heading_id]["children"]})
else:
- parent_level = level - 1
- parent = toc
- while parent_level > 2:
- if not parent:
- break
- parent = next(iter(parent.values()))["children"]
- parent_level -= 1
- if parent:
- parent[next(iter(parent.keys()))]["children"][heading_id] = {"name": title, "children": {}}
+ # Nested heading
+ parent = stack[-1]["children"]
+ parent[heading_id] = new_entry
+ stack.append({"level": level, "children": parent[heading_id]["children"]})
for heading in headings:
title = heading.get_text().strip()
- heading_id = re.sub(r"[^\u00C0-\u1FFF\u2C00-\uD7FF\w\- ]", "", title).replace(" ", "-").lower()
+ heading_id = (
+ re.sub(r"[^\u00C0-\u1FFF\u2C00-\uD7FF\w\- ]", "", title)
+ .replace(" ", "-")
+ .lower()
+ )
heading["id"] = heading_id
- level = int(heading.name[1])
+ level = int(
+ heading.name[1]
+ ) # Extract the level from the tag name (e.g., h2 -> 2)
add_to_toc(toc, level, heading_id, title)
return toc
+
@frappe.whitelist()
def get_commit_docs_page_list(commit_doc):
- '''
- Get the list of Commit Docs Page
- '''
- user_info = {}
- users = []
- page = frappe.get_all('Commit Docs Page', filters={'commit_docs': commit_doc}, fields=['*'], order_by='creation desc')
- for p in page:
- users.append(p.owner)
- users.append(p.modified_by)
- users = list(set(users))
- frappe.utils.add_user_info(users, user_info)
-
- return {
- 'pages': page,
- 'user_info': user_info
- }
+ """
+ Get the list of Commit Docs Page
+ """
+ user_info = {}
+ users = []
+ page = frappe.get_all(
+ "Commit Docs Page",
+ filters={"commit_docs": commit_doc},
+ fields=["*"],
+ order_by="creation desc",
+ )
+ for p in page:
+ users.append(p.owner)
+ users.append(p.modified_by)
+ users = list(set(users))
+ frappe.utils.add_user_info(users, user_info)
+
+ return {"pages": page, "user_info": user_info}
+
@frappe.whitelist()
def create_commit_docs_page(data):
- '''
- Create a new Commit Docs Page
- '''
- if isinstance(data, str):
- data = json.loads(data)
-
- # create a new Commit Docs Page
- commit_docs_page = frappe.get_doc({
- 'doctype': 'Commit Docs Page',
- 'title': data.get('title'),
- 'commit_docs': data.get('commit_docs'),
- })
-
- commit_docs_page.insert()
-
- if data.get('sidebar_label'):
- commit_doc = frappe.get_doc('Commit Docs', data.get('commit_docs'))
- commit_doc.append('sidebar', {
- 'parent_label': data.get('sidebar_label'),
- 'docs_page': commit_docs_page.name
- })
- commit_doc.save()
- return commit_docs_page
\ No newline at end of file
+ """
+ Create a new Commit Docs Page
+ """
+ if isinstance(data, str):
+ data = json.loads(data)
+
+ # create a new Commit Docs Page
+ commit_docs_page = frappe.get_cached_doc(
+ {
+ "doctype": "Commit Docs Page",
+ "title": data.get("title"),
+ "commit_docs": data.get("commit_docs"),
+ }
+ )
+
+ commit_docs_page.insert()
+
+ if data.get("sidebar_label"):
+ commit_doc = frappe.get_cached_doc("Commit Docs", data.get("commit_docs"))
+ commit_doc.append(
+ "sidebar",
+ {
+ "parent_label": data.get("sidebar_label"),
+ "docs_page": commit_docs_page.name,
+ },
+ )
+ commit_doc.save()
+ return commit_docs_page
diff --git a/commit/commit/doctype/commit_docs_page/test_commit_docs_page.py b/commit/commit/doctype/commit_docs_page/test_commit_docs_page.py
index 8f60b81..1e1691e 100644
--- a/commit/commit/doctype/commit_docs_page/test_commit_docs_page.py
+++ b/commit/commit/doctype/commit_docs_page/test_commit_docs_page.py
@@ -4,7 +4,6 @@
# import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
-
# On IntegrationTestCase, the doctype test records and all
# link-field test record depdendencies are recursively loaded
# Use these module variables to add/remove to/from that list
@@ -13,18 +12,18 @@
class TestCommitDocsPage(UnitTestCase):
- """
- Unit tests for CommitDocsPage.
- Use this class for testing individual functions and methods.
- """
+ """
+ Unit tests for CommitDocsPage.
+ Use this class for testing individual functions and methods.
+ """
- pass
+ pass
class TestCommitDocsPage(IntegrationTestCase):
- """
- Integration tests for CommitDocsPage.
- Use this class for testing interactions between multiple components.
- """
+ """
+ Integration tests for CommitDocsPage.
+ Use this class for testing interactions between multiple components.
+ """
- pass
+ pass
diff --git a/commit/commit/doctype/commit_docs_topbar_item/commit_docs_topbar_item.json b/commit/commit/doctype/commit_docs_topbar_item/commit_docs_topbar_item.json
index fa2edf2..98da27e 100644
--- a/commit/commit/doctype/commit_docs_topbar_item/commit_docs_topbar_item.json
+++ b/commit/commit/doctype/commit_docs_topbar_item/commit_docs_topbar_item.json
@@ -83,4 +83,4 @@
"sort_field": "creation",
"sort_order": "ASC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/commit/commit/doctype/commit_docs_topbar_item/commit_docs_topbar_item.py b/commit/commit/doctype/commit_docs_topbar_item/commit_docs_topbar_item.py
index 024b9f3..efd53cb 100644
--- a/commit/commit/doctype/commit_docs_topbar_item/commit_docs_topbar_item.py
+++ b/commit/commit/doctype/commit_docs_topbar_item/commit_docs_topbar_item.py
@@ -6,4 +6,4 @@
class CommitDocsTopbarItem(Document):
- pass
+ pass
diff --git a/commit/commit/doctype/commit_organization/commit_organization.json b/commit/commit/doctype/commit_organization/commit_organization.json
index 350a7b9..b645922 100644
--- a/commit/commit/doctype/commit_organization/commit_organization.json
+++ b/commit/commit/doctype/commit_organization/commit_organization.json
@@ -81,4 +81,4 @@
"states": [],
"title_field": "organization_name",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/commit/commit/doctype/commit_organization/commit_organization.py b/commit/commit/doctype/commit_organization/commit_organization.py
index 8567779..bafacd0 100644
--- a/commit/commit/doctype/commit_organization/commit_organization.py
+++ b/commit/commit/doctype/commit_organization/commit_organization.py
@@ -6,11 +6,11 @@
class CommitOrganization(Document):
- def on_trash(self):
- # find all project which are linked with this organisation
- # delete all projects
- projects = frappe.get_all('Commit Project',filters={
- 'org':self.name
- },pluck='name')
- for project in projects:
- frappe.db.delete('Commit Project',project)
+ def on_trash(self):
+ # find all project which are linked with this organisation
+ # delete all projects
+ projects = frappe.get_all(
+ "Commit Project", filters={"org": self.name}, pluck="name"
+ )
+ for project in projects:
+ frappe.db.delete("Commit Project", project)
diff --git a/commit/commit/doctype/commit_organization/test_commit_organization.py b/commit/commit/doctype/commit_organization/test_commit_organization.py
index ffb2f00..14516ed 100644
--- a/commit/commit/doctype/commit_organization/test_commit_organization.py
+++ b/commit/commit/doctype/commit_organization/test_commit_organization.py
@@ -6,4 +6,4 @@
class TestCommitOrganization(FrappeTestCase):
- pass
+ pass
diff --git a/commit/commit/doctype/commit_project/commit_project.json b/commit/commit/doctype/commit_project/commit_project.json
index 9ecfd15..afd371f 100644
--- a/commit/commit/doctype/commit_project/commit_project.json
+++ b/commit/commit/doctype/commit_project/commit_project.json
@@ -118,4 +118,4 @@
"states": [],
"title_field": "display_name",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/commit/commit/doctype/commit_project/commit_project.py b/commit/commit/doctype/commit_project/commit_project.py
index ea2afe6..81e76f3 100644
--- a/commit/commit/doctype/commit_project/commit_project.py
+++ b/commit/commit/doctype/commit_project/commit_project.py
@@ -1,48 +1,50 @@
# Copyright (c) 2023, The Commit Company and contributors
# For license information, please see license.txt
-import frappe
-import os
import io
+import os
from pathlib import Path
+
+import frappe
from frappe.model.document import Document
+
from commit.api.code_analysis import get_name_of_app
+
class CommitProject(Document):
- def before_insert(self):
- self.app_name = get_name_of_app(self.org, self.repo_name)
- self.create_project_folder()
-
- def create_project_folder(self):
- '''
- Need to create a project folder when a project is created
- The folder needs to be created in the site folder in public
- '''
- # Create folder for the org in the sites folder if it does not exist
- main_folder_path = frappe.get_site_path("public", "organizations")
- if not os.path.exists(main_folder_path):
- os.mkdir(main_folder_path)
- org_path = frappe.get_site_path("public", "organizations", self.org)
- if not os.path.exists(org_path):
- os.mkdir(org_path)
-
- # Create a folder for the project in the org folder if it does not exist
- project_path = org_path + "/" + self.repo_name
-
- if not os.path.exists(project_path):
- os.mkdir(project_path)
-
- self.path_to_folder = project_path
-
-
- return
-
- def on_trash(self):
- # find all branches which are linked with this project
- # delete all branches
- branches = frappe.get_all("Commit Project Branch", filters={
- 'project' : self.name
- }, pluck='name')
- for branch in branches:
- frappe.db.delete("Commit Project Branch", branch)
+ def before_insert(self):
+ self.app_name = get_name_of_app(self.org, self.repo_name)
+ self.create_project_folder()
+
+ def create_project_folder(self):
+ """
+ Need to create a project folder when a project is created
+ The folder needs to be created in the site folder in public
+ """
+ # Create folder for the org in the sites folder if it does not exist
+ main_folder_path = frappe.get_site_path("public", "organizations")
+ if not os.path.exists(main_folder_path):
+ os.mkdir(main_folder_path)
+ org_path = frappe.get_site_path("public", "organizations", self.org)
+ if not os.path.exists(org_path):
+ os.mkdir(org_path)
+
+ # Create a folder for the project in the org folder if it does not exist
+ project_path = org_path + "/" + self.repo_name
+
+ if not os.path.exists(project_path):
+ os.mkdir(project_path)
+
+ self.path_to_folder = project_path
+
+ return
+
+ def on_trash(self):
+ # find all branches which are linked with this project
+ # delete all branches
+ branches = frappe.get_all(
+ "Commit Project Branch", filters={"project": self.name}, pluck="name"
+ )
+ for branch in branches:
+ frappe.db.delete("Commit Project Branch", branch)
diff --git a/commit/commit/doctype/commit_project/test_commit_project.py b/commit/commit/doctype/commit_project/test_commit_project.py
index 8d1aba8..6b94f0d 100644
--- a/commit/commit/doctype/commit_project/test_commit_project.py
+++ b/commit/commit/doctype/commit_project/test_commit_project.py
@@ -6,4 +6,4 @@
class TestCommitProject(FrappeTestCase):
- pass
+ pass
diff --git a/commit/commit/doctype/commit_project_branch/commit_project_branch.json b/commit/commit/doctype/commit_project_branch/commit_project_branch.json
index 831818e..581e4eb 100644
--- a/commit/commit/doctype/commit_project_branch/commit_project_branch.json
+++ b/commit/commit/doctype/commit_project_branch/commit_project_branch.json
@@ -162,4 +162,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/commit/commit/doctype/commit_project_branch/commit_project_branch.py b/commit/commit/doctype/commit_project_branch/commit_project_branch.py
index 657cdc2..b0b5b44 100644
--- a/commit/commit/doctype/commit_project_branch/commit_project_branch.py
+++ b/commit/commit/doctype/commit_project_branch/commit_project_branch.py
@@ -1,69 +1,84 @@
# Copyright (c) 2023, The Commit Company and contributors
# For license information, please see license.txt
-import frappe
-import git
+import json
import os
import shutil
-import json
+
+import frappe
+import git
+from frappe.app import handle_exception
from frappe.model.document import Document
-from commit.commit.code_analysis.apis import find_all_occurrences_of_whitelist
-from commit.commit.code_analysis.doctypes import get_doctypes_in_module, get_doctype_json
from frappe.utils import now
-from frappe.app import handle_exception
+
from commit.api.api_explorer import get_file_content_from_path
from commit.api.generate_documentation import generate_docs_for_apis
+from commit.commit.code_analysis.apis import find_all_occurrences_of_whitelist
+from commit.commit.code_analysis.doctypes import (
+ get_doctype_json,
+ get_doctypes_in_module,
+)
+
class CommitProjectBranch(Document):
def before_insert(self):
self.path_to_folder = self.get_path_to_folder()
self.create_branch_folder()
-
+
def after_insert(self):
frappe.enqueue(
- method = background_fetch_process,
- is_async = True,
+ method=background_fetch_process,
+ is_async=True,
job_name="Fetch Project Branch",
- enqueue_after_commit = True,
- at_front = True,
- project_branch = self.name
+ enqueue_after_commit=True,
+ at_front=True,
+ project_branch=self.name,
)
def on_update(self):
old_doc = self.get_doc_before_save()
if type(self.whitelisted_apis) == str:
- apis = json.loads(self.whitelisted_apis if self.whitelisted_apis else '').get("apis", [])
+ apis = json.loads(
+ self.whitelisted_apis if self.whitelisted_apis else ""
+ ).get("apis", [])
else:
- apis = self.whitelisted_apis.get("apis", []) if self.whitelisted_apis else []
- if old_doc and old_doc.whitelisted_apis != self.whitelisted_apis and len(apis) > 0:
+ apis = (
+ self.whitelisted_apis.get("apis", []) if self.whitelisted_apis else []
+ )
+ if (
+ old_doc
+ and old_doc.whitelisted_apis != self.whitelisted_apis
+ and len(apis) > 0
+ ):
frappe.enqueue(
- method = generate_branch_documentation,
- is_async = True,
+ method=generate_branch_documentation,
+ is_async=True,
job_name="Generate Branch Documentation",
- enqueue_after_commit = True,
- at_front = True,
+ enqueue_after_commit=True,
+ at_front=True,
queue="long",
- project_branch = self.name
- )
+ project_branch=self.name,
+ )
def create_branch_folder(self):
if not os.path.exists(self.path_to_folder):
os.mkdir(self.path_to_folder)
def get_path_to_folder(self):
- project = frappe.get_doc("Commit Project", self.project)
+ project = frappe.get_cached_doc("Commit Project", self.project)
return project.path_to_folder + "/" + self.branch_name
def clone_repo(self):
- project = frappe.get_doc("Commit Project", self.project)
+ project = frappe.get_cached_doc("Commit Project", self.project)
self.app_name = project.app_name
- repo_url = "https://github.com/{}/{}".format(
- project.org, project.repo_name)
+ repo_url = "https://github.com/{}/{}".format(project.org, project.repo_name)
folder_path = self.path_to_folder
- repo = git.Repo.clone_from(repo_url, folder_path, branch=self.branch_name, single_branch=True)
+ repo = git.Repo.clone_from(
+ repo_url, folder_path, branch=self.branch_name, single_branch=True
+ )
self.last_fetched = frappe.utils.now_datetime()
self.commit_hash = repo.head.object.hexsha
@@ -71,8 +86,14 @@ def fetch_repo(self):
repo = git.Repo(self.path_to_folder)
repo.remotes.origin.fetch()
- # Pull the latest changes from the remote
- repo.remotes.origin.pull()
+ # Force fast-forward only to avoid merge commits
+ try:
+ # Try fast-forward pull
+ repo.git.pull("--ff-only")
+ except Exception as e:
+ # If fast-forward not possible, reset to remote branch
+ repo.git.reset("--hard", "origin/" + self.branch_name)
+
self.last_fetched = now()
self.commit_hash = repo.head.object.hexsha
@@ -83,10 +104,9 @@ def fetch_repo(self):
pass
def get_modules(self):
- modules_path = os.path.join(
- self.path_to_folder, self.app_name, 'modules.txt')
+ modules_path = os.path.join(self.path_to_folder, self.app_name, "modules.txt")
if os.path.isfile(modules_path):
- modules_file = open(modules_path, 'r')
+ modules_file = open(modules_path, "r")
modules = modules_file.read().splitlines()
self.modules = ",".join(modules)
@@ -94,7 +114,8 @@ def get_modules(self):
doctype_module_map = {}
for module in modules:
module_doctypes_map[module] = get_doctypes_in_module(
- self.path_to_folder, self.app_name, module)
+ self.path_to_folder, self.app_name, module
+ )
for doctype in module_doctypes_map[module].get("doctype_names", []):
doctype_module_map[doctype] = module
@@ -102,49 +123,56 @@ def get_modules(self):
self.doctype_module_map = doctype_module_map
def find_all_apis(self):
- apis = find_all_occurrences_of_whitelist(
- self.path_to_folder, self.app_name)
+ apis = find_all_occurrences_of_whitelist(self.path_to_folder, self.app_name)
# Convert list to string and save to database
- self.whitelisted_apis = {
- "apis": apis
- }
+ self.whitelisted_apis = {"apis": apis}
return apis
def get_whitelisted_apis_code(self):
apis = []
apis_code = []
-
+
if self.whitelisted_apis:
if type(self.whitelisted_apis) == str:
- apis = json.loads(self.whitelisted_apis if self.whitelisted_apis else '').get("apis", [])
+ apis = json.loads(
+ self.whitelisted_apis if self.whitelisted_apis else ""
+ ).get("apis", [])
else:
- apis = self.whitelisted_apis.get("apis", []) if self.whitelisted_apis else []
-
+ apis = (
+ self.whitelisted_apis.get("apis", [])
+ if self.whitelisted_apis
+ else []
+ )
+
for api in apis:
# file_content = get_file_content_from_path(self.name, api['file'], api['block_start'], api['block_end'], "project")
- file_content = get_code_from_file(api['file'], api['block_start'], api['block_end'])
+ file_content = get_code_from_file(
+ api["file"], api["block_start"], api["block_end"]
+ )
content = file_content.get("file_content", [])
content = "".join(content)
- apis_code.append({
- 'file': api['file'],
- 'path': api['api_path'],
- 'function_name': api['name'],
- 'code': content
- })
+ apis_code.append(
+ {
+ "file": api["file"],
+ "path": api["api_path"],
+ "function_name": api["name"],
+ "code": content,
+ }
+ )
documentation = generate_docs_for_apis(apis_code)
- self.documentation= {
- "apis": documentation
- }
+ self.documentation = {"apis": documentation}
def get_doctype_json(self, doctype_name):
if self.doctype_module_map:
doctype_module_map = json.loads(self.doctype_module_map)
module = doctype_module_map.get(doctype_name)
if module:
- return get_doctype_json(self.path_to_folder, self.app_name, module, doctype_name)
+ return get_doctype_json(
+ self.path_to_folder, self.app_name, module, doctype_name
+ )
return None
def get_doctypes_in_module(self, module):
@@ -157,123 +185,148 @@ def on_trash(self):
if self.path_to_folder and os.path.exists(self.path_to_folder):
shutil.rmtree(self.path_to_folder)
+
def get_code_from_file(file_path: str, block_start: int, block_end: int):
if os.path.isfile(file_path):
- file_content = open(file_path, 'r')
+ file_content = open(file_path, "r")
file_content = file_content.readlines()
# fetch the block
file_content = file_content[block_start:block_end]
- return {
- "file_content": file_content
- }
+ return {"file_content": file_content}
else:
frappe.throw("File not found")
+
def background_fetch_process(project_branch):
try:
- doc = frappe.get_doc("Commit Project Branch", project_branch)
- frappe.publish_realtime('commit_branch_clone_repo',
+ doc = frappe.get_cached_doc("Commit Project Branch", project_branch)
+ frappe.publish_realtime(
+ "commit_branch_clone_repo",
{
- 'branch_name': doc.branch_name,
- 'project': doc.project,
- 'text': "Cloning repository...",
- 'is_completed': False
- }, user=frappe.session.user)
-
+ "branch_name": doc.branch_name,
+ "project": doc.project,
+ "text": "Cloning repository...",
+ "is_completed": False,
+ },
+ user=frappe.session.user,
+ )
doc.clone_repo()
- frappe.publish_realtime('commit_branch_get_modules',
+ frappe.publish_realtime(
+ "commit_branch_get_modules",
{
- 'branch_name': doc.branch_name,
- 'project': doc.project,
- 'text': "Getting all modules for your app...",
- 'is_completed': False
- }, user=frappe.session.user)
+ "branch_name": doc.branch_name,
+ "project": doc.project,
+ "text": "Getting all modules for your app...",
+ "is_completed": False,
+ },
+ user=frappe.session.user,
+ )
doc.get_modules()
- frappe.publish_realtime('commit_branch_find_apis',
+ frappe.publish_realtime(
+ "commit_branch_find_apis",
{
- 'branch_name': doc.branch_name,
- 'project': doc.project,
- 'text': "Finding all APIs...",
- 'is_completed': False
- }, user=frappe.session.user)
+ "branch_name": doc.branch_name,
+ "project": doc.project,
+ "text": "Finding all APIs...",
+ "is_completed": False,
+ },
+ user=frappe.session.user,
+ )
doc.find_all_apis()
-
+
# doc.get_whitelisted_apis_code()
doc.save()
- frappe.publish_realtime("commit_project_branch_created", {
- 'name': doc.name,
- 'branch_name': doc.branch_name,
- 'project': doc.project,
- 'text': "Branch created successfully.",
- 'is_completed': True
- }, user=frappe.session.user)
+ frappe.publish_realtime(
+ "commit_project_branch_created",
+ {
+ "name": doc.name,
+ "branch_name": doc.branch_name,
+ "project": doc.project,
+ "text": "Branch created successfully.",
+ "is_completed": True,
+ },
+ user=frappe.session.user,
+ )
except Exception as e:
# throw the error and delete the document
- messages = [json.dumps({'message' :'There was an error while fetching branch repo.'})]
+ messages = [
+ json.dumps({"message": "There was an error while fetching branch repo."})
+ ]
frappe.clear_messages()
- frappe.publish_realtime('commit_branch_creation_error',
+ frappe.publish_realtime(
+ "commit_branch_creation_error",
{
- 'branch_name': doc.branch_name,
- 'project': doc.project,
- 'error':{
- "exception": frappe.get_traceback(),
- "_server_messages": json.dumps(messages),
- },
+ "branch_name": doc.branch_name,
+ "project": doc.project,
+ "error": {
+ "exception": frappe.get_traceback(),
+ "_server_messages": json.dumps(messages),
+ },
# 'response': handle_exception(e),
- 'is_completed': False
- }, user=frappe.session.user)
+ "is_completed": False,
+ },
+ user=frappe.session.user,
+ )
frappe.delete_doc("Commit Project Branch", project_branch)
# frappe.throw("Project Branch not found")
frappe.log(frappe.get_traceback())
-
+
# raise e
-
+
@frappe.whitelist(allow_guest=True)
-def fetch_repo(doc, name = None):
- if name :
- project_branch = frappe.get_doc("Commit Project Branch", name)
+def fetch_repo(doc, name=None):
+ if name:
+ project_branch = frappe.get_cached_doc("Commit Project Branch", name)
else:
doc = json.loads(doc)
- project_branch = frappe.get_doc("Commit Project Branch", doc.get("name"))
+ project_branch = frappe.get_cached_doc("Commit Project Branch", doc.get("name"))
project_branch.fetch_repo()
project_branch.save()
return "Hello"
def generate_branch_documentation(project_branch):
- frappe.publish_realtime('commit_branch_generate_documentation',
+ frappe.publish_realtime(
+ "commit_branch_generate_documentation",
{
- 'branch_name': project_branch,
- 'text': "Generating documentation...",
- 'is_completed': False
- }, user=frappe.session.user)
-
- doc = frappe.get_doc("Commit Project Branch", project_branch)
+ "branch_name": project_branch,
+ "text": "Generating documentation...",
+ "is_completed": False,
+ },
+ user=frappe.session.user,
+ )
+
+ doc = frappe.get_cached_doc("Commit Project Branch", project_branch)
doc.get_whitelisted_apis_code()
doc.save()
- frappe.publish_realtime("commit_branch_generate_documentation", {
- 'branch_name': doc.branch_name,
- 'project': doc.project,
- 'text': "Documentation generated successfully.",
- 'is_completed': True
- }, user=frappe.session.user)
+ frappe.publish_realtime(
+ "commit_branch_generate_documentation",
+ {
+ "branch_name": doc.branch_name,
+ "project": doc.project,
+ "text": "Documentation generated successfully.",
+ "is_completed": True,
+ },
+ user=frappe.session.user,
+ )
return "Documentation generated successfully"
+
@frappe.whitelist(allow_guest=True)
def get_module_doctype_map_for_branches(branches: str):
branches = json.loads(branches)
module_doctypes_map = {}
for branch in branches:
- project_branch = frappe.get_doc("Commit Project Branch", branch)
+ project_branch = frappe.get_cached_doc("Commit Project Branch", branch)
module_doctypes_map[branch] = json.loads(project_branch.module_doctypes_map)
- return module_doctypes_map
\ No newline at end of file
+ return module_doctypes_map
diff --git a/commit/commit/doctype/commit_project_branch/test_commit_project_branch.py b/commit/commit/doctype/commit_project_branch/test_commit_project_branch.py
index 42efd7f..00fa998 100644
--- a/commit/commit/doctype/commit_project_branch/test_commit_project_branch.py
+++ b/commit/commit/doctype/commit_project_branch/test_commit_project_branch.py
@@ -6,4 +6,4 @@
class TestCommitProjectBranch(FrappeTestCase):
- pass
+ pass
diff --git a/commit/commit/doctype/commit_settings/commit_settings.json b/commit/commit/doctype/commit_settings/commit_settings.json
index 72b4087..65806e2 100644
--- a/commit/commit/doctype/commit_settings/commit_settings.json
+++ b/commit/commit/doctype/commit_settings/commit_settings.json
@@ -57,4 +57,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/commit/commit/doctype/commit_settings/commit_settings.py b/commit/commit/doctype/commit_settings/commit_settings.py
index 753d7d0..6d183cc 100644
--- a/commit/commit/doctype/commit_settings/commit_settings.py
+++ b/commit/commit/doctype/commit_settings/commit_settings.py
@@ -6,4 +6,4 @@
class CommitSettings(Document):
- pass
+ pass
diff --git a/commit/commit/doctype/commit_settings/test_commit_settings.py b/commit/commit/doctype/commit_settings/test_commit_settings.py
index 5608e8f..83d8e8d 100644
--- a/commit/commit/doctype/commit_settings/test_commit_settings.py
+++ b/commit/commit/doctype/commit_settings/test_commit_settings.py
@@ -6,4 +6,4 @@
class TestCommitSettings(FrappeTestCase):
- pass
+ pass
diff --git a/commit/commit/doctype/github_settings/github_settings.json b/commit/commit/doctype/github_settings/github_settings.json
index b12df7c..364abf4 100644
--- a/commit/commit/doctype/github_settings/github_settings.json
+++ b/commit/commit/doctype/github_settings/github_settings.json
@@ -83,4 +83,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/commit/commit/doctype/github_settings/github_settings.py b/commit/commit/doctype/github_settings/github_settings.py
index 331253e..c643183 100644
--- a/commit/commit/doctype/github_settings/github_settings.py
+++ b/commit/commit/doctype/github_settings/github_settings.py
@@ -1,10 +1,11 @@
# Copyright (c) 2023, The Commit Company and contributors
# For license information, please see license.txt
+import json
+
import frappe
-from frappe.model.document import Document
import requests
-import json
+from frappe.model.document import Document
class GithubSettings(Document):
@@ -12,35 +13,35 @@ class GithubSettings(Document):
session = requests.Session()
-session.headers.update({'Accept': 'application/json'})
+session.headers.update({"Accept": "application/json"})
@frappe.whitelist(allow_guest=True)
def authenticate_user(code, state=None):
- '''API to authenticate the user with GitHub'''
+ """API to authenticate the user with GitHub"""
response = get_access_token(code)
if response:
- user_data = get_user_details(response.get('access_token'))
+ user_data = get_user_details(response.get("access_token"))
if user_data:
user = create_user(user_data)
def get_access_token(code):
- '''Get the access token from GitHub'''
- '''
+ """Get the access token from GitHub"""
+ """
1. Make a POST request to GitHub to get the access token
2. Return the access token
- '''
- github_settings = frappe.get_doc("Github Settings")
+ """
+ github_settings = frappe.get_cached_doc("Github Settings")
client_id = github_settings.client_id
- client_secret = github_settings.get_password('client_secret')
+ client_secret = github_settings.get_password("client_secret")
token_url = github_settings.token_uri
data = {
- 'client_id': client_id,
- 'client_secret': client_secret,
- 'code': code,
+ "client_id": client_id,
+ "client_secret": client_secret,
+ "code": code,
}
- headers = {'Accept': 'application/json'}
+ headers = {"Accept": "application/json"}
response = requests.post(token_url, data=data, headers=headers)
return response.json()
@@ -48,36 +49,42 @@ def get_access_token(code):
def get_user_details(access_token):
user_response = requests.get(
- 'https://api.github.com/user', headers={'Authorization': 'token ' + access_token})
+ "https://api.github.com/user",
+ headers={"Authorization": "token " + access_token},
+ )
user_data = {}
if user_response.status_code == 200 and user_response.json():
email_response = requests.get(
- 'https://api.github.com/user/emails', headers={'Authorization': 'token ' + access_token})
- user_data = {"user_details": user_response.json(
- ), "email_details": email_response.json()}
+ "https://api.github.com/user/emails",
+ headers={"Authorization": "token " + access_token},
+ )
+ user_data = {
+ "user_details": user_response.json(),
+ "email_details": email_response.json(),
+ }
return user_data
def create_user(user_data):
- '''Create a user in the system'''
- '''
+ """Create a user in the system"""
+ """
1. Get the user details from GitHub
2. Create a user in the system
3. Return the user details
- '''
+ """
user = frappe.new_doc("User")
- user.first_name = user_data.get('user_details').get('name').split()[0]
- user.last_name = user_data.get('user_details').get('name').split()[1]
- user.email = user_data.get('email_details')[0].get('email')
- user.username = user_data.get('user_details').get('login')
- user.user_image = user_data.get('user_details').get('avatar_url')
- user.bio = user_data.get('user_details').get('bio')
- user.location = user_data.get('user_details').get('location')
+ user.first_name = user_data.get("user_details").get("name").split()[0]
+ user.last_name = user_data.get("user_details").get("name").split()[1]
+ user.email = user_data.get("email_details")[0].get("email")
+ user.username = user_data.get("user_details").get("login")
+ user.user_image = user_data.get("user_details").get("avatar_url")
+ user.bio = user_data.get("user_details").get("bio")
+ user.location = user_data.get("user_details").get("location")
user.new_password = frappe.generate_hash()
user.enabled = 1
- user.user_type = 'Website User'
+ user.user_type = "Website User"
user.insert(ignore_permissions=True)
frappe.db.commit()
return user
diff --git a/commit/commit/doctype/github_settings/test_github_settings.py b/commit/commit/doctype/github_settings/test_github_settings.py
index 5edb028..7f692da 100644
--- a/commit/commit/doctype/github_settings/test_github_settings.py
+++ b/commit/commit/doctype/github_settings/test_github_settings.py
@@ -6,4 +6,4 @@
class TestGithubSettings(FrappeTestCase):
- pass
+ pass
diff --git a/commit/commit/doctype/github_token/github_token.json b/commit/commit/doctype/github_token/github_token.json
index 28ecdd3..953df68 100644
--- a/commit/commit/doctype/github_token/github_token.json
+++ b/commit/commit/doctype/github_token/github_token.json
@@ -81,4 +81,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/commit/commit/doctype/github_token/github_token.py b/commit/commit/doctype/github_token/github_token.py
index 09c589e..8bfb447 100644
--- a/commit/commit/doctype/github_token/github_token.py
+++ b/commit/commit/doctype/github_token/github_token.py
@@ -4,5 +4,6 @@
# import frappe
from frappe.model.document import Document
+
class GithubToken(Document):
- pass
+ pass
diff --git a/commit/commit/doctype/github_token/test_github_token.py b/commit/commit/doctype/github_token/test_github_token.py
index 757f3c3..5b09445 100644
--- a/commit/commit/doctype/github_token/test_github_token.py
+++ b/commit/commit/doctype/github_token/test_github_token.py
@@ -6,4 +6,4 @@
class TestGithubToken(FrappeTestCase):
- pass
+ pass
diff --git a/commit/commit/doctype/linked_commit_docs_page/linked_commit_docs_page.json b/commit/commit/doctype/linked_commit_docs_page/linked_commit_docs_page.json
index 5749938..f8531fc 100644
--- a/commit/commit/doctype/linked_commit_docs_page/linked_commit_docs_page.json
+++ b/commit/commit/doctype/linked_commit_docs_page/linked_commit_docs_page.json
@@ -30,4 +30,4 @@
"sort_field": "creation",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/commit/commit/doctype/linked_commit_docs_page/linked_commit_docs_page.py b/commit/commit/doctype/linked_commit_docs_page/linked_commit_docs_page.py
index 296d3bf..dc0c8aa 100644
--- a/commit/commit/doctype/linked_commit_docs_page/linked_commit_docs_page.py
+++ b/commit/commit/doctype/linked_commit_docs_page/linked_commit_docs_page.py
@@ -6,4 +6,4 @@
class LinkedCommitDocsPage(Document):
- pass
+ pass
diff --git a/commit/commit/doctype/open_ai_settings/open_ai_settings.json b/commit/commit/doctype/open_ai_settings/open_ai_settings.json
index 75aa9c0..d6bd36d 100644
--- a/commit/commit/doctype/open_ai_settings/open_ai_settings.json
+++ b/commit/commit/doctype/open_ai_settings/open_ai_settings.json
@@ -53,4 +53,4 @@
"sort_field": "creation",
"sort_order": "DESC",
"states": []
-}
\ No newline at end of file
+}
diff --git a/commit/commit/doctype/open_ai_settings/open_ai_settings.py b/commit/commit/doctype/open_ai_settings/open_ai_settings.py
index 825edff..a4ff5e9 100644
--- a/commit/commit/doctype/open_ai_settings/open_ai_settings.py
+++ b/commit/commit/doctype/open_ai_settings/open_ai_settings.py
@@ -7,14 +7,14 @@
class OpenAISettings(Document):
- pass
+ pass
def open_ai_call(message):
# 1. Get the organization ID and API key from Open API Settings
open_ai = frappe.get_single("Open AI Settings")
org_id = open_ai.organization
- api_key = open_ai.get_password('api_key')
+ api_key = open_ai.get_password("api_key")
if not org_id or not api_key:
frappe.throw("Please set the organization ID and API key in Open API Settings")
@@ -23,12 +23,12 @@ def open_ai_call(message):
client = OpenAI(organization=org_id, api_key=api_key)
# 2. Make the API call to Open AI
- response = client.chat.completions.create(
- model="gpt-3.5-turbo",
- messages=message,
- max_tokens=3900,
- temperature=0.3, # Lower temperature for more deterministic output
- stop=["Function Name:", "\n\n"] # Stop sequence to separate functions
+ response = client.chat.completions.create(
+ model="gpt-3.5-turbo",
+ messages=message,
+ max_tokens=3900,
+ temperature=0.3, # Lower temperature for more deterministic output
+ stop=["Function Name:", "\n\n"], # Stop sequence to separate functions
)
- return response.choices[0].message.content
\ No newline at end of file
+ return response.choices[0].message.content
diff --git a/commit/commit/doctype/open_ai_settings/test_open_ai_settings.py b/commit/commit/doctype/open_ai_settings/test_open_ai_settings.py
index fbb9e62..d8b3e20 100644
--- a/commit/commit/doctype/open_ai_settings/test_open_ai_settings.py
+++ b/commit/commit/doctype/open_ai_settings/test_open_ai_settings.py
@@ -6,4 +6,4 @@
class TestOpenAISettings(FrappeTestCase):
- pass
+ pass
diff --git a/commit/fixtures/server_script.json b/commit/fixtures/server_script.json
index 7eb3f2b..af49a3f 100644
--- a/commit/fixtures/server_script.json
+++ b/commit/fixtures/server_script.json
@@ -15,7 +15,7 @@
"rate_limit_count": 5,
"rate_limit_seconds": 86400,
"reference_doctype": null,
- "script": "commit_branches_daily = frappe.get_list(\"Commit Project Branch\", filters={\"frequency\": \"Daily\"}, pluck='name')\n\nfor branch in commit_branches_daily :\n branch_doc = frappe.get_doc(\"Commit Project Branch\", branch)\n branch_doc.fetch_repo()\n branch_doc.save()\n \nfrappe.db.commit()",
+ "script": "commit_branches_daily = frappe.get_list(\"Commit Project Branch\", filters={\"frequency\": \"Daily\"}, pluck='name')\n\nfor branch in commit_branches_daily :\n branch_doc = frappe.get_cached_doc(\"Commit Project Branch\", branch)\n branch_doc.fetch_repo()\n branch_doc.save()\n \nfrappe.db.commit()",
"script_type": "Scheduler Event"
},
{
@@ -34,7 +34,7 @@
"rate_limit_count": 5,
"rate_limit_seconds": 86400,
"reference_doctype": null,
- "script": "commit_branches_weekly = frappe.get_list(\"Commit Project Branch\", filters={\"frequency\" : \"Weekly\"}, pluck=\"name\")\n\n\nfor branch in commit_branches_weekly:\n branch_doc = frappe.get_doc(\"Commit Project Branch\", branch)\n branch_doc.fetch_repo()\n branch_doc.save()\n\nfrappe.db.commit()",
+ "script": "commit_branches_weekly = frappe.get_list(\"Commit Project Branch\", filters={\"frequency\" : \"Weekly\"}, pluck=\"name\")\n\n\nfor branch in commit_branches_weekly:\n branch_doc = frappe.get_cached_doc(\"Commit Project Branch\", branch)\n branch_doc.fetch_repo()\n branch_doc.save()\n\nfrappe.db.commit()",
"script_type": "Scheduler Event"
},
{
@@ -53,7 +53,7 @@
"rate_limit_count": 5,
"rate_limit_seconds": 86400,
"reference_doctype": null,
- "script": "commit_branches_monthly = frappe.get_list(\"Commit Project Branch\", filters={\"frequency\" : \"Monthly\"}, pluck = \"name\")\n\nfor branch in commit_branches_monthly:\n branch_doc = frappe.get_doc(\"Commit Project Branch\", branch)\n branch_doc.fetch_repo()\n branch_doc.save()\n\nfrappe.db.commit()",
+ "script": "commit_branches_monthly = frappe.get_list(\"Commit Project Branch\", filters={\"frequency\" : \"Monthly\"}, pluck = \"name\")\n\nfor branch in commit_branches_monthly:\n branch_doc = frappe.get_cached_doc(\"Commit Project Branch\", branch)\n branch_doc.fetch_repo()\n branch_doc.save()\n\nfrappe.db.commit()",
"script_type": "Scheduler Event"
}
-]
\ No newline at end of file
+]
diff --git a/commit/hooks.py b/commit/hooks.py
index e408d86..30e8f15 100644
--- a/commit/hooks.py
+++ b/commit/hooks.py
@@ -42,7 +42,7 @@
# website user home page (by Role)
# role_home_page = {
-# "Role": "home_page"
+# "Role": "home_page"
# }
# Generators
@@ -56,8 +56,8 @@
# add methods and filters to jinja environment
# jinja = {
-# "methods": "commit.utils.jinja_methods",
-# "filters": "commit.utils.jinja_filters"
+# "methods": "commit.utils.jinja_methods",
+# "filters": "commit.utils.jinja_filters"
# }
# Installation
@@ -83,11 +83,11 @@
# Permissions evaluated in scripted ways
# permission_query_conditions = {
-# "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions",
+# "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions",
# }
#
# has_permission = {
-# "Event": "frappe.desk.doctype.event.event.has_permission",
+# "Event": "frappe.desk.doctype.event.event.has_permission",
# }
# DocType Class
@@ -95,7 +95,7 @@
# Override standard doctype classes
# override_doctype_class = {
-# "ToDo": "custom_app.overrides.CustomToDo"
+# "ToDo": "custom_app.overrides.CustomToDo"
# }
# Document Events
@@ -103,32 +103,32 @@
# Hook on document methods and events
# doc_events = {
-# "*": {
-# "on_update": "method",
-# "on_cancel": "method",
-# "on_trash": "method"
-# }
+# "*": {
+# "on_update": "method",
+# "on_cancel": "method",
+# "on_trash": "method"
+# }
# }
# Scheduled Tasks
# ---------------
# scheduler_events = {
-# "all": [
-# "commit.tasks.all"
-# ],
-# "daily": [
-# "commit.tasks.daily"
-# ],
-# "hourly": [
-# "commit.tasks.hourly"
-# ],
-# "weekly": [
-# "commit.tasks.weekly"
-# ],
-# "monthly": [
-# "commit.tasks.monthly"
-# ],
+# "all": [
+# "commit.tasks.all"
+# ],
+# "daily": [
+# "commit.tasks.daily"
+# ],
+# "hourly": [
+# "commit.tasks.hourly"
+# ],
+# "weekly": [
+# "commit.tasks.weekly"
+# ],
+# "monthly": [
+# "commit.tasks.monthly"
+# ],
# }
# Testing
@@ -140,14 +140,14 @@
# ------------------------------
#
# override_whitelisted_methods = {
-# "frappe.desk.doctype.event.event.get_events": "commit.event.get_events"
+# "frappe.desk.doctype.event.event.get_events": "commit.event.get_events"
# }
#
# each overriding function accepts a `data` argument;
# generated from the base implementation of the doctype dashboard,
# along with any modifications made in other Frappe apps
# override_doctype_dashboards = {
-# "Task": "commit.task.get_dashboard_data"
+# "Task": "commit.task.get_dashboard_data"
# }
# exempt linked doctypes from being automatically cancelled
@@ -173,32 +173,35 @@
# --------------------
# user_data_fields = [
-# {
-# "doctype": "{doctype_1}",
-# "filter_by": "{filter_by}",
-# "redact_fields": ["{field_1}", "{field_2}"],
-# "partial": 1,
-# },
-# {
-# "doctype": "{doctype_2}",
-# "filter_by": "{filter_by}",
-# "partial": 1,
-# },
-# {
-# "doctype": "{doctype_3}",
-# "strict": False,
-# },
-# {
-# "doctype": "{doctype_4}"
-# }
+# {
+# "doctype": "{doctype_1}",
+# "filter_by": "{filter_by}",
+# "redact_fields": ["{field_1}", "{field_2}"],
+# "partial": 1,
+# },
+# {
+# "doctype": "{doctype_2}",
+# "filter_by": "{filter_by}",
+# "partial": 1,
+# },
+# {
+# "doctype": "{doctype_3}",
+# "strict": False,
+# },
+# {
+# "doctype": "{doctype_4}"
+# }
# ]
# Authentication and authorization
# --------------------------------
# auth_hooks = [
-# "commit.auth.validate"
+# "commit.auth.validate"
# ]
-fixtures = [{"doctype": "Server Script", "filters": [["module" , "in" , ("commit" )]]}]
+fixtures = [{"doctype": "Server Script", "filters": [["module", "in", ("commit")]]}]
-website_route_rules = [{'from_route': '/commit-docs/', 'to_route': 'commit-docs'}, {'from_route': '/commit/', 'to_route': 'commit'}]
\ No newline at end of file
+website_route_rules = [
+ {"from_route": "/commit-docs/", "to_route": "commit-docs"},
+ {"from_route": "/commit/", "to_route": "commit"},
+]
diff --git a/commit/modules.txt b/commit/modules.txt
index fcad765..01f9a2a 100644
--- a/commit/modules.txt
+++ b/commit/modules.txt
@@ -1 +1 @@
-commit
\ No newline at end of file
+commit
diff --git a/commit/utils/api_analysis.py b/commit/utils/api_analysis.py
index 414dcb6..33c9269 100644
--- a/commit/utils/api_analysis.py
+++ b/commit/utils/api_analysis.py
@@ -1,45 +1,51 @@
import re
+
def get_api_details_from_file_contents(file_contents: str, file_path: str):
- '''
+ """
Get list of all whitelisted API in a file string with:
1. Type
2. Path
3. Method name
4. Arguments
5. Python code snippet
- '''
+ """
whitelist_indexes = find_all_mentions_of_whitelist_in_file(file_contents)
# TODO: Get whitelist type (e.g. methods, rate_limit, etc.)
- api_details = get_api_content_from_file_contents(file_contents, whitelist_indexes, file_path)
+ api_details = get_api_content_from_file_contents(
+ file_contents, whitelist_indexes, file_path
+ )
return api_details
-
# return "{}:{}".format(base_path, indexes)
def convert_file_path_to_api_path(file_path: str):
- '''
+ """
Convert file path to API path
- '''
+ """
return file_path.replace("/", ".").replace(".py", "")
+
def find_all_mentions_of_whitelist_in_file(file_contents: str):
- '''
+ """
Find all mentions of @frappe.whitelist() in a file
- '''
+ """
- indexes = [m.start() for m in re.finditer('@frappe.whitelist', file_contents)]
+ indexes = [m.start() for m in re.finditer("@frappe.whitelist", file_contents)]
return indexes
-def get_api_content_from_file_contents(file_contents: str, indexes: list, file_path: str):
- '''
+
+def get_api_content_from_file_contents(
+ file_contents: str, indexes: list, file_path: str
+):
+ """
Get API name from file contents
- '''
+ """
# Loop over to find the first mention of "def" after the indexes
base_path = convert_file_path_to_api_path(file_path)
api_string = []
@@ -59,7 +65,7 @@ def get_api_content_from_file_contents(file_contents: str, indexes: list, file_p
api_content = ""
indentation_of_def = ""
if index_of_colon != -1:
- api_def = file_contents[index_of_def + 4:index_of_colon]
+ api_def = file_contents[index_of_def + 4 : index_of_colon]
# Need to find the start and end of the API function
@@ -68,7 +74,9 @@ def get_api_content_from_file_contents(file_contents: str, indexes: list, file_p
index_of_newline_before_def = file_contents.rfind("\n", 0, index_of_def)
api_content = index_of_newline_before_def
# Get the indentation string between the \n and the def
- indentation_of_def = file_contents[index_of_newline_before_def + 1:index_of_def]
+ indentation_of_def = file_contents[
+ index_of_newline_before_def + 1 : index_of_def
+ ]
if indentation_of_def == "":
# No indentation. Find first line after def that has no indentation
pass
@@ -78,31 +86,69 @@ def get_api_content_from_file_contents(file_contents: str, indexes: list, file_p
# Find the next line containing the same number of indentation
if api_def:
- api_string.append({
- "function_def": api_def,
- "content": api_content,
- "indentation": indentation_of_def,
- "name": extract_name_from_def(api_def),
- "arguments": extract_arguments_from_def(api_def),
- "path": base_path,
- "file_path": file_path,
- **whitelist_properties,
- })
-
+ api_string.append(
+ {
+ "function_def": api_def,
+ "content": api_content,
+ "indentation": indentation_of_def,
+ "name": extract_name_from_def(api_def),
+ "arguments": extract_arguments_from_def(api_def),
+ "path": base_path,
+ "file_path": file_path,
+ **whitelist_properties,
+ }
+ )
+
return api_string
def extract_name_from_def(api_def: str):
- '''
+ """
Extract name from def
- '''
+ """
return api_def.split("(")[0].strip()
+
+def _split_params_by_comma(params_str: str) -> list:
+ """
+ Split parameter string by top-level commas, ignoring commas inside [], (), {}.
+ """
+ parts = []
+ current = []
+ open_brackets = [] # stack of opening bracket chars
+ bracket_pairs = {"[": "]", "(": ")", "{": "}"}
+ i = 0
+ while i < len(params_str):
+ c = params_str[i]
+ if c in bracket_pairs:
+ open_brackets.append(c)
+ current.append(c)
+ i += 1
+ elif open_brackets and c == bracket_pairs[open_brackets[-1]]:
+ open_brackets.pop()
+ current.append(c)
+ i += 1
+ elif c == "," and not open_brackets:
+ parts.append("".join(current).strip())
+ current = []
+ i += 1
+ else:
+ current.append(c)
+ i += 1
+ if current:
+ parts.append("".join(current).strip())
+ return parts
+
+
def extract_arguments_from_def(api_def: str):
- '''
+ """
Extract arguments from def
- '''
- arguments_with_types_defaults = api_def.split("(")[1].split(")")[0].split(",")
+ """
+ if "(" not in api_def or ")" not in api_def:
+ arguments_with_types_defaults = []
+ else:
+ params_str = api_def.split("(")[1].split(")")[0]
+ arguments_with_types_defaults = _split_params_by_comma(params_str)
arguments = []
for arg in arguments_with_types_defaults:
@@ -111,42 +157,47 @@ def extract_arguments_from_def(api_def: str):
argument = ""
type = ""
if "=" in argument_with_types_default:
- default_split = argument_with_types_default.split("=")
- default = default_split[1].strip().replace('"', '').replace("'", "")
+ default_split = argument_with_types_default.split("=", 1)
+ default = default_split[1].strip().replace('"', "").replace("'", "")
argument = default_split[0].strip()
else:
argument = argument_with_types_default
if ":" in argument:
- type = argument.split(":")[1].strip()
- argument = argument.split(":")[0].strip()
- arguments.append({
- "argument": argument,
- "type": type,
- "default": default
- })
+ name_type = argument.split(":", 1)
+ argument = name_type[0].strip()
+ type = name_type[1].strip()
+ arguments.append({"argument": argument, "type": type, "default": default})
return arguments
+
def parse_whitelist(whitelisted_content: str):
- '''
+ """
Input being @frappe.whitelist() with args, find request type and other params
- '''
+ """
args = whitelisted_content.split("(")[1].split(")")[0].split(",")
request_types = []
xss_safe = False
allow_guest = False
for arg in args:
if "methods" in arg:
- request_types = arg.split("=")[1].replace("[", "").replace("]", "").replace('"', '').replace("'", "").split(",")
-
+ request_types = (
+ arg.split("=")[1]
+ .replace("[", "")
+ .replace("]", "")
+ .replace('"', "")
+ .replace("'", "")
+ .split(",")
+ )
+
if "xss_safe" in arg:
xss_safe = arg.split("=")[1].strip() == "True"
-
+
if "allow_guest" in arg:
allow_guest = arg.split("=")[1].strip() == "True"
-
+
return {
"request_types": request_types,
"xss_safe": xss_safe,
- "allow_guest": allow_guest
- }
\ No newline at end of file
+ "allow_guest": allow_guest,
+ }
diff --git a/commit/utils/conversions.py b/commit/utils/conversions.py
index 6d2ad0a..8a42005 100644
--- a/commit/utils/conversions.py
+++ b/commit/utils/conversions.py
@@ -1,6 +1,6 @@
def convert_module_name(module: str):
- '''
+ """
Convert module name to frappe module path name
Replace spaces with underscores and convert to lowercase
- '''
- return module.replace(" ", "_").lower()
\ No newline at end of file
+ """
+ return module.replace(" ", "_").lower()
diff --git a/commit/www/commit-docs.html b/commit/www/commit-docs.html
index 4865774..207c1e0 100644
--- a/commit/www/commit-docs.html
+++ b/commit/www/commit-docs.html
@@ -6,8 +6,8 @@
Docs
-
-
+
+
@@ -15,4 +15,4 @@
-