diff --git a/forms_pro/api/team.py b/forms_pro/api/team.py
new file mode 100644
index 0000000..4358c5a
--- /dev/null
+++ b/forms_pro/api/team.py
@@ -0,0 +1,19 @@
+import frappe
+
+from forms_pro.utils.teams import GetTeamFormsResponseSchema
+from forms_pro.utils.teams import get_team_forms as get_team_forms_utils
+
+
+@frappe.whitelist()
+def get_team_forms(team_id: str) -> list[GetTeamFormsResponseSchema]:
+ """
+ Get the list of forms for the current team
+
+ Args:
+ team_id: ID of the team
+
+ Returns:
+ list[GetTeamFormsResponseSchema] - List of forms for the team
+ """
+ forms = get_team_forms_utils(team_id=team_id)
+ return forms
diff --git a/forms_pro/api/user.py b/forms_pro/api/user.py
new file mode 100644
index 0000000..3ffc590
--- /dev/null
+++ b/forms_pro/api/user.py
@@ -0,0 +1,61 @@
+import frappe
+from frappe.core.doctype.has_role.has_role import HasRole
+from pydantic import BaseModel, Field, field_validator
+
+from forms_pro.utils.teams import get_user_teams as get_user_teams_utils
+
+
+class GetUserTeamsResponseSchema(BaseModel):
+ name: str = Field(description="ID of the team")
+ team_name: str = Field(description="The name of the team")
+ is_current: bool = Field(description="Whether this is the current team")
+
+
+class GetUserResponseSchema(BaseModel):
+ email: str
+ first_name: str
+ last_name: str | None = None
+ full_name: str
+ username: str
+ desk_theme: str
+ roles: list[str]
+ has_desk_access: bool
+
+ @field_validator("roles", mode="before")
+ @classmethod
+ def extract_roles(cls, v: list[HasRole]) -> list[str]:
+ if not v:
+ return []
+
+ return [role.role for role in v]
+
+
+@frappe.whitelist()
+def get_user() -> GetUserResponseSchema:
+ """
+ Get Current User Data
+ """
+
+ user_id = frappe.session.user
+ user_doc = frappe.get_doc("User", user_id)
+ data = user_doc.as_dict()
+ data["roles"] = user_doc.get("roles")
+ data["has_desk_access"] = user_doc.has_desk_access()
+
+ return GetUserResponseSchema.model_validate(data).model_dump()
+
+
+@frappe.whitelist()
+def get_user_teams() -> list[GetUserTeamsResponseSchema]:
+ """
+ Get the list of teams for the current user
+ """
+
+ user = frappe.session.user
+
+ if user == "Guest":
+ return []
+
+ teams = get_user_teams_utils(user)
+
+ return [GetUserTeamsResponseSchema.model_validate(team).model_dump() for team in teams]
diff --git a/forms_pro/forms_pro/doctype/form/form.json b/forms_pro/forms_pro/doctype/form/form.json
index eb770c7..f145089 100644
--- a/forms_pro/forms_pro/doctype/form/form.json
+++ b/forms_pro/forms_pro/doctype/form/form.json
@@ -11,6 +11,7 @@
"column_break_pqae",
"title",
"linked_doctype",
+ "linked_team_id",
"settings_section",
"login_required",
"column_break_xocw",
@@ -107,12 +108,19 @@
"fieldname": "allow_incomplete",
"fieldtype": "Check",
"label": "Allow Incomplete Forms"
+ },
+ {
+ "fieldname": "linked_team_id",
+ "fieldtype": "Link",
+ "label": "Linked Team",
+ "options": "FP Team",
+ "reqd": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2025-10-07 12:16:18.825427",
+ "modified": "2025-12-20 20:40:24.219032",
"modified_by": "Administrator",
"module": "Forms Pro",
"name": "Form",
diff --git a/forms_pro/forms_pro/doctype/form/form.py b/forms_pro/forms_pro/doctype/form/form.py
index db4b1ac..9378aba 100644
--- a/forms_pro/forms_pro/doctype/form/form.py
+++ b/forms_pro/forms_pro/doctype/form/form.py
@@ -21,6 +21,7 @@ class Form(Document):
fields: DF.Table[FormField]
is_published: DF.Check
linked_doctype: DF.Link
+ linked_team_id: DF.Link
login_required: DF.Check
metadata: DF.Code | None
route: DF.Data | None
diff --git a/forms_pro/forms_pro/doctype/form/test_form.py b/forms_pro/forms_pro/doctype/form/test_form.py
index 99fe67b..9d1d1ae 100644
--- a/forms_pro/forms_pro/doctype/form/test_form.py
+++ b/forms_pro/forms_pro/doctype/form/test_form.py
@@ -4,6 +4,8 @@
import frappe
from frappe.tests import IntegrationTestCase
+from forms_pro.utils.teams import get_user_teams
+
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
@@ -18,9 +20,18 @@ def setUp(self):
self.test_doctype_name = "Test Form DocType"
self.create_test_doctype()
+ self.test_user = "test_forms_pro_user@example.com"
+ self.test_team = get_user_teams(self.test_user)[0]["name"]
+
# Create a test Form
self.test_form = frappe.get_doc(
- {"doctype": "Form", "title": "Test Form", "linked_doctype": self.test_doctype_name, "fields": []}
+ {
+ "doctype": "Form",
+ "title": "Test Form",
+ "linked_doctype": self.test_doctype_name,
+ "fields": [],
+ "linked_team_id": self.test_team,
+ }
)
self.test_form.insert()
diff --git a/forms_pro/forms_pro/doctype/fp_team/fp_team.py b/forms_pro/forms_pro/doctype/fp_team/fp_team.py
index b346ad6..f31c501 100644
--- a/forms_pro/forms_pro/doctype/fp_team/fp_team.py
+++ b/forms_pro/forms_pro/doctype/fp_team/fp_team.py
@@ -20,4 +20,24 @@ class FPTeam(Document):
users: DF.TableMultiSelect[FPTeamMember]
# end: auto-generated types
- pass
+ @property
+ def team_members(self) -> list[str]:
+ """
+ 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 []
+
+ def is_team_member(self, user: str) -> bool:
+ """
+ Check if a user is a member of the team
+
+ Args:
+ user: The user email address
+
+ Returns:
+ bool - True if the user is a member of the team, False otherwise
+ """
+ return user in self.team_members
diff --git a/forms_pro/hooks.py b/forms_pro/hooks.py
index 8b88771..2274150 100644
--- a/forms_pro/hooks.py
+++ b/forms_pro/hooks.py
@@ -166,7 +166,7 @@
# Testing
# -------
-# before_tests = "forms_pro.install.before_tests"
+before_tests = "forms_pro.install.before_tests"
# Overriding Methods
# ------------------------------
diff --git a/forms_pro/install.py b/forms_pro/install.py
new file mode 100644
index 0000000..ef686b1
--- /dev/null
+++ b/forms_pro/install.py
@@ -0,0 +1,30 @@
+import frappe
+from frappe.core.doctype.user.user import User
+
+from forms_pro.roles import FORMS_PRO_ROLE
+
+
+def before_tests():
+ give_admin_forms_pro_role()
+ create_test_user()
+
+
+def give_admin_forms_pro_role():
+ admin = frappe.get_doc("User", "Administrator")
+ admin.append("roles", {"role": FORMS_PRO_ROLE})
+ admin.save()
+
+
+def create_test_user():
+ test_user = "test_forms_pro_user@example.com"
+
+ if frappe.db.exists("User", test_user):
+ return
+
+ user: User = frappe.new_doc("User")
+ user.email = test_user
+ user.first_name = "Test"
+ user.last_name = "Forms Pro User"
+ user.insert(ignore_permissions=True)
+ user.append("roles", {"role": FORMS_PRO_ROLE})
+ user.save(ignore_permissions=True)
diff --git a/forms_pro/overrides/roles.py b/forms_pro/overrides/roles.py
index b2b09af..53f8eff 100644
--- a/forms_pro/overrides/roles.py
+++ b/forms_pro/overrides/roles.py
@@ -2,7 +2,7 @@
from frappe.core.doctype.user.user import User
from forms_pro.roles import FORMS_PRO_ROLE
-from forms_pro.utils.teams import get_user_teams
+from forms_pro.utils.teams import get_user_teams, set_current_team
def handle_forms_pro_role_change(doc, method) -> None:
@@ -27,7 +27,7 @@ def handle_forms_pro_role_change(doc, method) -> None:
create_default_team_for_user(user)
-def create_default_team_for_user(user: User):
+def create_default_team_for_user(user: User) -> None:
from forms_pro.forms_pro.doctype.fp_team.fp_team import FPTeam
team: FPTeam = frappe.new_doc("FP Team")
@@ -40,3 +40,4 @@ def create_default_team_for_user(user: User):
},
)
team.save(ignore_permissions=True)
+ set_current_team(team.name, user.name)
diff --git a/forms_pro/utils/form_generator.py b/forms_pro/utils/form_generator.py
index 2365173..28faeac 100644
--- a/forms_pro/utils/form_generator.py
+++ b/forms_pro/utils/form_generator.py
@@ -4,12 +4,12 @@
@frappe.whitelist()
-def create_form_with_doctype(doctype: str):
+def create_form_with_doctype(team_id: str, doctype: str):
roles = frappe.get_roles(frappe.session.user)
if FORMS_PRO_ROLE not in roles:
frappe.throw("You are not authorized to create a form")
- form_generator = FormGenerator(linked_doctype=doctype)
+ form_generator = FormGenerator(team_id=team_id, linked_doctype=doctype)
form_generator.generate()
return {
@@ -19,12 +19,12 @@ def create_form_with_doctype(doctype: str):
@frappe.whitelist()
-def create_form():
+def create_form(team_id: str):
roles = frappe.get_roles(frappe.session.user)
if FORMS_PRO_ROLE not in roles:
frappe.throw("You are not authorized to create a form")
- form_generator = FormGenerator()
+ form_generator = FormGenerator(team_id=team_id)
form_generator.generate()
return {
@@ -34,8 +34,13 @@ def create_form():
class FormGenerator:
- def __init__(self, linked_doctype: str | None = None) -> None:
+ def __init__(
+ self,
+ team_id: str,
+ linked_doctype: str | None = None,
+ ) -> None:
self.doctype = None
+ self.team_id = team_id
if linked_doctype:
self.doctype = frappe.get_doc("DocType", linked_doctype)
@@ -80,6 +85,7 @@ def _initialize_form_document(self) -> None:
form_document = frappe.new_doc("Form")
form_document.linked_doctype = self.doctype.name
form_document.title = "Untitled Form"
+ form_document.linked_team_id = self.team_id
form_document.insert(ignore_permissions=True)
self.form_document = form_document
diff --git a/forms_pro/utils/teams.py b/forms_pro/utils/teams.py
index 69484a7..3306760 100644
--- a/forms_pro/utils/teams.py
+++ b/forms_pro/utils/teams.py
@@ -1,4 +1,55 @@
+from datetime import datetime
+from typing import Any
+
import frappe
+from frappe.defaults import get_user_default, set_user_default
+from frappe.query_builder import Case
+from pydantic import BaseModel, Field, field_validator
+
+
+class GetTeamFormsResponseSchema(BaseModel):
+ """
+ Schema for the response of the get_team_forms function
+ """
+
+ name: str = Field(description="ID of the form")
+ title: str = Field(description="Title of the form")
+ description: str | None = Field(default="", description="Description of the form")
+ route: str | None = Field(default="", description="Route of the form")
+ is_published: bool = Field(description="Whether the form is published")
+ creation: datetime = Field(description="Creation date of the form")
+ modified: datetime = Field(description="Modification date of the form")
+ linked_team_id: str = Field(description="Team ID of the form")
+
+ @field_validator("creation", "modified", mode="before")
+ @classmethod
+ def parse_datetime(cls, v: Any) -> datetime:
+ """Convert datetime string to datetime object."""
+ if isinstance(v, str):
+ return frappe.utils.get_datetime(v)
+ if isinstance(v, datetime):
+ return v
+ raise ValueError(f"Invalid datetime value: {v}")
+
+ @field_validator("is_published", mode="before")
+ @classmethod
+ def parse_boolean(cls, v: Any) -> bool:
+ """Convert 0/1 to boolean."""
+ if isinstance(v, bool):
+ return v
+ if isinstance(v, int):
+ return bool(v)
+ if v is None:
+ return False
+ raise ValueError(f"Invalid boolean value: {v}")
+
+ @field_validator("description", "route", mode="before")
+ @classmethod
+ def handle_none_strings(cls, v: Any) -> str | None:
+ """Handle None values for optional string fields."""
+ if v is None:
+ return ""
+ return v
def get_user_teams(user: str = frappe.session.user):
@@ -9,13 +60,57 @@ def get_user_teams(user: str = frappe.session.user):
FP_TEAM = frappe.qb.DocType("FP Team")
FP_TEAM_MEMBER = frappe.qb.DocType("FP Team Member")
+ user_default_team = get_user_default("current_team", user)
+
query = (
frappe.qb.from_(FP_TEAM)
.join(FP_TEAM_MEMBER)
.on(FP_TEAM.name == FP_TEAM_MEMBER.parent)
.where(FP_TEAM_MEMBER.user == user)
- .select(FP_TEAM.team_name, FP_TEAM.name)
+ .select(
+ FP_TEAM.team_name,
+ FP_TEAM.name,
+ Case().when(FP_TEAM.name == user_default_team, True).else_(False).as_("is_current"),
+ )
+ .orderby(FP_TEAM.creation)
)
teams = query.run(as_dict=True)
+ if user_default_team is None and len(teams) > 0:
+ teams[0]["is_current"] = True
+ set_user_default("current_team", teams[0]["name"])
+
return teams or []
+
+
+def set_current_team(team_name: str, user: str = frappe.session.user):
+ set_user_default("current_team", team_name, user)
+
+
+def get_team_forms(team_id: str) -> list[GetTeamFormsResponseSchema]:
+ """
+ Get the list of forms for a team
+
+ Args:
+ team_id: ID of the team
+
+ Returns:
+ list[GetTeamFormsResponseSchema] - List of forms for the team
+ """
+ forms = frappe.get_all(
+ "Form",
+ filters={"linked_team_id": team_id},
+ fields=[
+ "name",
+ "title",
+ "description",
+ "route",
+ "is_published",
+ "creation",
+ "modified",
+ "linked_team_id",
+ ],
+ )
+
+ data = [GetTeamFormsResponseSchema.model_validate(form).model_dump() for form in forms]
+ return data
diff --git a/forms_pro/utils/test_form_generator.py b/forms_pro/utils/test_form_generator.py
index a00b975..3ed680a 100644
--- a/forms_pro/utils/test_form_generator.py
+++ b/forms_pro/utils/test_form_generator.py
@@ -5,9 +5,19 @@
from frappe.tests import IntegrationTestCase
from forms_pro.utils.form_generator import FormGenerator
+from forms_pro.utils.teams import get_user_teams
class IntegrationTestFormGenerator(IntegrationTestCase):
+ def setUp(self):
+ super().setUp()
+ self.test_user = "test_forms_pro_user@example.com"
+ self.test_team = get_user_teams(self.test_user)[0]["name"]
+
+ def tearDown(self):
+ frappe.set_user("Administrator")
+ super().tearDown()
+
def test_form_generator_initialization_with_doctype(self):
"""Test FormGenerator initialization with existing DocType"""
# Create a test DocType
@@ -18,7 +28,7 @@ def test_form_generator_initialization_with_doctype(self):
test_doctype.insert(ignore_permissions=True)
# Initialize FormGenerator with existing DocType
- form_generator = FormGenerator(linked_doctype=test_doctype.name)
+ form_generator = FormGenerator(linked_doctype=test_doctype.name, team_id=self.test_team)
# Assertions
self.assertIsNotNone(form_generator.doctype)
@@ -27,14 +37,14 @@ def test_form_generator_initialization_with_doctype(self):
def test_form_generator_initialization_without_doctype(self):
"""Test FormGenerator initialization without DocType"""
- form_generator = FormGenerator()
+ form_generator = FormGenerator(team_id=self.test_team)
# Assertions
self.assertIsNone(form_generator.doctype)
def test_generate_doctype_name_format(self):
"""Test that generate_doctype_name returns correct format"""
- form_generator = FormGenerator()
+ form_generator = FormGenerator(team_id=self.test_team)
doctype_name = form_generator._generate_doctype_name()
# Assertions
@@ -45,7 +55,7 @@ def test_generate_doctype_name_format(self):
def test_generate_doctype_name_uniqueness(self):
"""Test that generate_doctype_name generates unique names"""
- form_generator = FormGenerator()
+ form_generator = FormGenerator(team_id=self.test_team)
names = set()
# Generate multiple names and check uniqueness
@@ -56,7 +66,7 @@ def test_generate_doctype_name_uniqueness(self):
def test_generate_creates_doctype_when_none_provided(self):
"""Test that generate() creates a new DocType when none is provided"""
- form_generator = FormGenerator()
+ form_generator = FormGenerator(team_id=self.test_team)
# Call generate method
form_generator.generate()
@@ -82,7 +92,7 @@ def test_generate_uses_existing_doctype_when_provided(self):
test_doctype.custom = True
test_doctype.insert(ignore_permissions=True)
- form_generator = FormGenerator(linked_doctype=test_doctype.name)
+ form_generator = FormGenerator(linked_doctype=test_doctype.name, team_id=self.test_team)
# Call generate method
form_generator.generate()
@@ -93,7 +103,7 @@ def test_generate_uses_existing_doctype_when_provided(self):
def test_generate_creates_form_document(self):
"""Test that generate() creates a Form document"""
- form_generator = FormGenerator()
+ form_generator = FormGenerator(team_id=self.test_team)
# Call generate method
form_generator.generate()
@@ -107,7 +117,7 @@ def test_generate_creates_form_document(self):
def test_generate_complete_flow(self):
"""Test the complete flow of FormGenerator.generate()"""
- form_generator = FormGenerator()
+ form_generator = FormGenerator(team_id=self.test_team)
# Call generate method
form_generator.generate()
@@ -135,7 +145,7 @@ def test_generate_with_existing_doctype_complete_flow(self):
test_doctype.custom = True
test_doctype.insert(ignore_permissions=True)
- form_generator = FormGenerator(linked_doctype=test_doctype.name)
+ form_generator = FormGenerator(linked_doctype=test_doctype.name, team_id=self.test_team)
# Call generate method
form_generator.generate()
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index a3ff02b..210cb0f 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -1,6 +1,10 @@
diff --git a/frontend/src/layouts/BaseLayout.vue b/frontend/src/layouts/BaseLayout.vue
index 9fd7506..5030c3c 100644
--- a/frontend/src/layouts/BaseLayout.vue
+++ b/frontend/src/layouts/BaseLayout.vue
@@ -23,19 +23,23 @@
import { session } from "@/data/session";
import { ref, computed } from "vue";
import { Sidebar } from "frappe-ui";
-import { LayoutDashboard } from "lucide-vue-next";
+import { LayoutDashboard, ArrowLeftRight, LogOut } from "lucide-vue-next";
import toggleTheme from "@/utils/theme";
+import { useUser } from "@/stores/user";
+const userStore = useUser();
const menuItems = computed(() => {
return [
// {
- // label: "Toggle Theme",
- // icon: "moon",
- // onClick: toggleTheme,
+ // label: "Switch Teams",
+ // icon: ArrowLeftRight,
+ // onClick: () => {
+ // console.log("Switch Teams");
+ // },
// },
{
label: "Log out",
- icon: "log-out",
+ icon: LogOut,
onClick: session.logout.submit,
},
];
diff --git a/frontend/src/pages/Dashboard.vue b/frontend/src/pages/Dashboard.vue
index b2915f0..6180f01 100644
--- a/frontend/src/pages/Dashboard.vue
+++ b/frontend/src/pages/Dashboard.vue
@@ -36,34 +36,46 @@
Manage and create formsDashboard
Loading...
-+
Loading...
+No forms created yet