Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9bd7011
feat: add `User Forms` module and update modules list
harshtandiya Dec 24, 2025
81c605e
feat: enhance form generation with error handling and permissions man…
harshtandiya Dec 24, 2025
bbc1bd2
fix: error handling
harshtandiya Dec 24, 2025
4c088a6
refactor: remove debug print statement from create_form function
harshtandiya Dec 24, 2025
3433fb5
chore: update Python version in CI workflow to 3.12
harshtandiya Dec 24, 2025
5823e4e
chore: update Python version in CI workflow to 3.14
harshtandiya Dec 24, 2025
91b062e
chore: update Node.js version in CI workflow to 24
harshtandiya Dec 24, 2025
435b88b
chore: add dayjs dependency to frontend
harshtandiya Dec 26, 2025
96c882b
feat: add date utility functions using dayjs
harshtandiya Dec 26, 2025
091ba60
feat: add useQueryParam composable for URL query parameter synchroniz…
harshtandiya Dec 28, 2025
ae24b97
chore: upgrade frappe-ui to `v0.1.244`
harshtandiya Dec 28, 2025
eb8eee6
fix: frappe ui css
harshtandiya Dec 28, 2025
20613e5
chore: add pydantic[email] dependencies in pyproject
harshtandiya Dec 30, 2025
d5b632a
feat: add new global components to Vue declaration
harshtandiya Dec 30, 2025
b3989ea
refactor: replace tailwind.config.js with tailwind.config.ts
harshtandiya Dec 30, 2025
8f601f3
refactor: main.js to main.ts
harshtandiya Dec 30, 2025
374fed7
feat: add Avatar component with user information display
harshtandiya Jan 1, 2026
c87dc09
refactor: update BaseLayout.vue to use props for sidebar configuration
harshtandiya Jan 1, 2026
d57226f
refactor: clean up date utility functions by removing redundant type …
harshtandiya Jan 1, 2026
303e6fb
feat: manage form v1
harshtandiya Jan 1, 2026
2f1a875
fix: semgrep
harshtandiya Jan 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
python-version: "3.14"

- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 18
node-version: 24
check-latest: true

- name: Cache pip
Expand Down Expand Up @@ -96,7 +96,7 @@ jobs:
bench --site test_site install-app forms_pro
bench build
env:
CI: 'Yes'
CI: "Yes"

- name: Run Tests
working-directory: /home/runner/frappe-bench
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ repos:
rev: v2.7.1
hooks:
- id: prettier
types_or: [javascript, vue, scss]
types_or: [javascript, vue, scss, ts]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's examine the .pre-commit-config.yaml file around the mentioned lines
cat -n .pre-commit-config.yaml | sed -n '25,55p'

Repository: BuildWithHussain/forms_pro

Length of output: 1160


🏁 Script executed:

#!/bin/bash
# Check if TypeScript configuration files exist
echo "=== Checking for TypeScript configuration files ==="
ls -la tsconfig.json 2>&1
ls -la .eslintrc* 2>&1
ls -la eslint.config.* 2>&1

echo -e "\n=== Checking for TypeScript in package.json ==="
if [ -f "package.json" ]; then
  cat package.json | jq '.devDependencies // {} | to_entries[] | select(.key | contains("typescript") or contains("eslint")) | "\(.key): \(.value)"'
fi

Repository: BuildWithHussain/forms_pro

Length of output: 353


🏁 Script executed:

#!/bin/bash
# Check the overall structure of .pre-commit-config.yaml
echo "=== Full .pre-commit-config.yaml content ==="
cat .pre-commit-config.yaml

Repository: BuildWithHussain/forms_pro

Length of output: 1781


Add tsconfig.json before enabling TypeScript support in pre-commit hooks.

The hook changes themselves are syntactically correct, but verification reveals that tsconfig.json is missing from the project. Adding TypeScript file processing to prettier and eslint without a TypeScript configuration will lead to inconsistent behavior and potential build failures. Create a tsconfig.json in the repository root before these changes take effect. Alternatively, confirm whether TypeScript is actually intended to be used in this project—if not, these hook changes should not be merged.

# Ignore any files that might contain jinja / bundles
exclude: |
(?x)^(
Expand All @@ -45,7 +45,7 @@ repos:
rev: v8.44.0
hooks:
- id: eslint
types_or: [javascript]
types_or: [javascript, ts]
args: ["--quiet"]
# Ignore any files that might contain jinja / bundles
exclude: |
Expand Down
89 changes: 77 additions & 12 deletions forms_pro/api/form.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
import frappe
from frappe import _
from frappe.share import remove
from pydantic import BaseModel, Field

from forms_pro.api.user import get_user
from forms_pro.forms_pro.doctype.form.form import Form


class FormSharedWithResponse(BaseModel):
full_name: str
user_image: str | None
email: str = Field(alias="user")
read: bool
write: bool
share: bool
submit: bool


@frappe.whitelist(allow_guest=True)
Expand All @@ -9,7 +25,7 @@ def get_form_by_route(route: str) -> dict:

@frappe.whitelist(allow_guest=True)
def get_form(form_id: str) -> dict:
form = frappe.get_doc(
form: Form = frappe.get_doc(
"Form",
form_id,
)
Expand All @@ -24,6 +40,66 @@ def get_form(form_id: str) -> dict:
}


@frappe.whitelist()
def get_form_shared_with(form_id: str) -> list[frappe.Any]:
"""
Get list of users with which a form is shared.

We validate the current user has read access to the form.
"""
if not frappe.has_permission(
"Form",
"read",
form_id,
):
frappe.throw(_("You do not have read access to this form"))

form: Form = frappe.get_doc("Form", form_id)
shared_with = form.shared_with()

shared_with_responses = []

for user in shared_with:
_user = get_user(user["user"])
user.update(_user)
shared_with_responses.append(FormSharedWithResponse.model_validate(user).model_dump())
Comment on lines +61 to +64
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Handle None return value from get_user.

The get_user function can return None (as defined in forms_pro/api/user.py lines 38-44), but this code calls user.update(_user) without checking. This will cause a TypeError when trying to update with None.

🔎 Proposed fix
     for user in shared_with:
         _user = get_user(user["user"])
-        user.update(_user)
+        if _user:
+            user.update(_user)
         shared_with_responses.append(FormSharedWithResponse.model_validate(user).model_dump())
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for user in shared_with:
_user = get_user(user["user"])
user.update(_user)
shared_with_responses.append(FormSharedWithResponse.model_validate(user).model_dump())
for user in shared_with:
_user = get_user(user["user"])
if _user:
user.update(_user)
shared_with_responses.append(FormSharedWithResponse.model_validate(user).model_dump())
🤖 Prompt for AI Agents
In forms_pro/api/form.py around lines 61 to 64, the code calls
user.update(_user) but get_user(...) can return None causing a TypeError; change
the logic to check if _user is None before calling update — if _user is None
either skip that shared_with entry (optionally log a warning with the user id)
or populate required fallback/default fields so
FormSharedWithResponse.model_validate receives a dict, then only append when you
have a non-None dict to pass to model_validate.


return shared_with_responses


@frappe.whitelist()
def remove_form_access(form_id: str, user_email: str) -> None:
"""
Remove access to a form for a user.

We validate the current user has write access to the form.

args:
form_id: str - The ID of the form to remove access to.
user_email: str - The email of the user to remove access to.

"""

if not frappe.has_permission("Form", "write", form_id):
frappe.throw(_("You do not have write access to this form"))

return remove(doctype="Form", name=form_id, user=user_email, flags={"ignore_permissions": True})


@frappe.whitelist()
def get_doctype_list() -> list[str]:
if not frappe.has_permission("DocType", "read"):
frappe.throw(_("You do not have read access to this doctype"))

return frappe.db.get_list(
"DocType",
filters={"istable": 0},
pluck="name",
order_by="name",
limit_page_length=99999,
)


@frappe.whitelist()
def get_doctype_fields(doctype: str) -> dict:
doctype = frappe.get_doc("DocType", doctype)
Expand All @@ -43,14 +119,3 @@ def get_doctype_fields(doctype: str) -> dict:
fields = [field for field in fields if field.fieldtype not in FIELDTYPES_TO_REMOVE]

return fields


@frappe.whitelist()
def get_doctype_list() -> list[str]:
return frappe.db.get_list(
"DocType",
filters={"istable": 0},
pluck="name",
order_by="name",
limit_page_length=99999,
)
24 changes: 24 additions & 0 deletions forms_pro/api/team.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import frappe

from forms_pro.forms_pro.doctype.fp_team.fp_team import FPTeam, GetTeamMembersResponse
from forms_pro.utils.teams import GetTeamFormsResponseSchema
from forms_pro.utils.teams import get_team_forms as get_team_forms_utils

Expand All @@ -17,3 +18,26 @@ def get_team_forms(team_id: str) -> list[GetTeamFormsResponseSchema]:
"""
forms = get_team_forms_utils(team_id=team_id)
return forms


@frappe.whitelist()
def get_team_members(team_id: str) -> list[GetTeamMembersResponse]:
"""

Get the list of team members in a FP Team.
This endpoint checks if the session user has the permission to read this FP Team DocType

"""
frappe.has_permission(
doctype="FP Team",
ptype="read",
doc=team_id,
user=frappe.session.user,
throw=True,
)
print("team_id", team_id)

team: FPTeam = frappe.get_doc("FP Team", team_id)
members = team.team_members

return members
16 changes: 15 additions & 1 deletion forms_pro/api/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,22 @@ def extract_roles(cls, v: list[HasRole]) -> list[str]:
return [role.role for role in v]


class GetUserBasicResponse(BaseModel):
full_name: str
user_image: str | None = None
Comment on lines +33 to +35
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical type mismatch: user_image nullability inconsistency.

The Python model defines user_image as str | None, but the frontend type in frontend/src/utils/user.ts defines it as a required string without null handling:

export type GetUserBasicResponse = {
  full_name: string;
  user_image: string;  // ❌ Not nullable
};

This mismatch will cause TypeScript type errors when user_image is None.

🔎 Fix the frontend type definition

Update frontend/src/utils/user.ts to match the backend model:

 export type GetUserBasicResponse = {
   full_name: string;
-  user_image: string;
+  user_image: string | null;
 };
🤖 Prompt for AI Agents
In forms_pro/api/user.py around lines 33 to 35 the Pydantic model declares
user_image as nullable (str | None) but the frontend type in
frontend/src/utils/user.ts marks it as a required string; update the frontend
type to match the backend by changing GetUserBasicResponse.user_image to allow
null (e.g., string | null or string | undefined) or make it optional, then
rebuild/typecheck the frontend to ensure no TypeScript errors remain.



@frappe.whitelist()
def get_user(user: str) -> GetUserBasicResponse | None:
"""Get basic user data for a given user"""
data = frappe.db.get_value("User", user, ["full_name", "user_image"], as_dict=True)
if not data:
return None
return GetUserBasicResponse.model_validate(data).model_dump()


@frappe.whitelist()
def get_user() -> GetUserResponseSchema:
def get_current_user() -> GetUserResponseSchema:
"""
Get Current User Data
"""
Expand Down
11 changes: 11 additions & 0 deletions forms_pro/forms_pro/doctype/form/form.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Copyright (c) 2025, harsh@buildwithhussain.com and contributors
# For license information, please see license.txt

from typing import Any

import frappe
from frappe.model.document import Document
from frappe.share import get_users


class Form(Document):
Expand Down Expand Up @@ -34,6 +37,14 @@ class Form(Document):
def linked_doctype_doc(self) -> Document:
return frappe.get_doc("DocType", self.linked_doctype)

@frappe.whitelist()
def shared_with(self) -> list[dict[str, Any]]:
"""
Get list of users with which this form is shared
"""
users_shared_with = get_users(self.doctype, self.name)
return users_shared_with
Comment on lines +40 to +46
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for frappe.share.get_users usage patterns and permission checks
rg -n -A5 -B5 'frappe\.share\.get_users|from frappe\.share import get_users' --type=py

Repository: BuildWithHussain/forms_pro

Length of output: 794


🏁 Script executed:

# Search for other whitelisted methods that check permissions
rg -n -A10 '@frappe\.whitelist\(\)' --type=py | head -100

Repository: BuildWithHussain/forms_pro

Length of output: 5439


🏁 Script executed:

# Examine the specific file
cat -n forms_pro/forms_pro/doctype/form/form.py | head -60

Repository: BuildWithHussain/forms_pro

Length of output: 2195


🏁 Script executed:

# Search for permission checks in whitelisted methods more broadly
rg -B2 -A8 '@frappe\.whitelist\(\)' --type=py | rg -B8 -A2 'check_perm|has_perm|get_permission' | head -80

Repository: BuildWithHussain/forms_pro

Length of output: 1617


🏁 Script executed:

# Search for frappe.share documentation or get_users usage in similar contexts
rg -n 'get_users' --type=py -B3 -A3 | head -50

Repository: BuildWithHussain/forms_pro

Length of output: 1023


🏁 Script executed:

# Check if there are other document methods with whitelist that don't check permissions
rg -B2 -A8 '@frappe\.whitelist\(\)' --type=py forms_pro/forms_pro/doctype/ -A8

Repository: BuildWithHussain/forms_pro

Length of output: 881


🏁 Script executed:

# Look at the full get_form_shared_with implementation for comparison
sed -n '43,68p' forms_pro/api/form.py

Repository: BuildWithHussain/forms_pro

Length of output: 782


🏁 Script executed:

# Search for any comments or docstrings about frappe.whitelist() permission handling
rg -B5 -A5 'frappe.whitelist' --type=py | rg -B5 -A5 'permission|auth|check' | head -60

Repository: BuildWithHussain/forms_pro

Length of output: 2856


🏁 Script executed:

# Check if there are any permission decorators or checks in Frappe that might auto-validate
rg -n 'allow_guest|check_permission|require_login' --type=py | head -20

Repository: BuildWithHussain/forms_pro

Length of output: 257


🏁 Script executed:

# Look for the actual Form doctype definition to see if there are document-level permission rules
fd -name '*.json' forms_pro/forms_pro/doctype/form

Repository: BuildWithHussain/forms_pro

Length of output: 300


🏁 Script executed:

# Find the Form doctype JSON definition
fd form.json forms_pro/forms_pro/doctype/form/

Repository: BuildWithHussain/forms_pro

Length of output: 112


🏁 Script executed:

# Check the JSON file if it exists
cat forms_pro/forms_pro/doctype/form/form.json 2>/dev/null | head -100

Repository: BuildWithHussain/forms_pro

Length of output: 1931


🏁 Script executed:

# Check if there are permissions defined in the doctype JSON for the shared_with method
cat forms_pro/forms_pro/doctype/form/form.json | grep -A50 'permission' 2>/dev/null | head -80

Repository: BuildWithHussain/forms_pro

Length of output: 357


🏁 Script executed:

# Examine whether Frappe automatically enforces permissions on document methods
rg -n 'def shared_with' -A20 forms_pro/forms_pro/doctype/form/form.py

Repository: BuildWithHussain/forms_pro

Length of output: 849


Add explicit permission validation before returning sensitive sharing information.

The shared_with() whitelisted method returns sensitive form sharing data without authorization checks. While a similar API endpoint (get_form_shared_with() in forms_pro/api/form.py) correctly validates read access with frappe.has_permission("Form", "read", form_id), this document method is exposed directly without checks. Add the same permission validation to prevent unauthorized access to form sharing lists:

@frappe.whitelist()
def shared_with(self) -> list[dict[str, Any]]:
    """
    Get list of users with which this form is shared
    """
    if not frappe.has_permission("Form", "read", self.name):
        frappe.throw(_("You do not have read access to this form"))
    
    users_shared_with = get_users(self.doctype, self.name)
    return users_shared_with
🤖 Prompt for AI Agents
In forms_pro/forms_pro/doctype/form/form.py around lines 40 to 46, the
whitelisted shared_with method returns sharing details without authorization;
add an explicit read-permission check using frappe.has_permission("Form",
"read", self.name) and raise a permission error (frappe.throw with an
appropriate message) when the check fails before calling get_users, so only
authorized users can retrieve the shared-with list.


def generate_initial_route(self) -> str:
return "s/forms_pro_" + frappe.utils.random_string(8)

Expand Down
26 changes: 23 additions & 3 deletions forms_pro/forms_pro/doctype/fp_team/fp_team.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@

# import frappe
from frappe.model.document import Document
from frappe.utils import cached_property
from pydantic import BaseModel, EmailStr

from forms_pro.api.user import get_user


class GetTeamMembersResponse(BaseModel):
full_name: str
user_image: str | None
email: EmailStr


class FPTeam(Document):
Expand All @@ -20,15 +30,25 @@ class FPTeam(Document):
users: DF.TableMultiSelect[FPTeamMember]
# end: auto-generated types

@property
def team_members(self) -> list[str]:
@cached_property
def team_members(self) -> list[GetTeamMembersResponse]:
"""
Get the list of team members

Returns:
list[str] - List of team member email addresses
"""
return [member.user for member in self.users] if self.users else []
if not len(self.users):
return []

members = []

for member in self.users:
_user = get_user(member.user)
_user["email"] = member.user
members.append(GetTeamMembersResponse.model_validate(_user).model_dump())

return members
Comment on lines +33 to +51
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Type annotation mismatch: returns list[dict], not list[GetTeamMembersResponse].

Line 34 declares the return type as list[GetTeamMembersResponse], but line 49 calls .model_dump() which returns a dictionary. The actual return type should be list[dict] or the method should return Pydantic model instances without calling .model_dump().

🔎 Fix return type annotation
 @cached_property
-def team_members(self) -> list[GetTeamMembersResponse]:
+def team_members(self) -> list[dict]:
     """
     Get the list of team members
🤖 Prompt for AI Agents
forms_pro/forms_pro/doctype/fp_team/fp_team.py around lines 33-51: the method
currently returns a list of dicts (it calls
GetTeamMembersResponse.model_validate(...).model_dump()) but is annotated as
list[GetTeamMembersResponse]; update the signature and docstring to reflect the
actual return type (change return type to list[dict] and adjust the Returns
docstring accordingly), or alternatively remove .model_dump() and return the
Pydantic model instances if you intend to return GetTeamMembersResponse
objects—pick one approach and make the annotation, docstring, and returned
values consistent.


def is_team_member(self, user: str) -> bool:
"""
Expand Down
3 changes: 2 additions & 1 deletion forms_pro/modules.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
Forms Pro
Forms Pro
User Forms
Empty file.
Loading