Files
upstream-frappe-crm/crm/api/session.py
upstream-mirror db69de1c11
Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1m13s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 6s
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped
snapshot of https://github.com/frappe/crm.git (develop, 2026-09-18; blobs > 131072B omitted)
2026-09-18 01:36:01 +05:00

172 lines
5.0 KiB
Python

import frappe
from frappe import _
CRM_ALLOWED_ROLES = ["System Manager", "Sales Manager", "Sales User"]
def get_session_role_flags():
roles = set(frappe.get_roles())
if not roles.intersection(set(CRM_ALLOWED_ROLES)):
frappe.throw(_("You are not permitted to access CRM resources."), frappe.PermissionError)
return {
"is_system_manager": "System Manager" in roles,
"is_sales_manager": "Sales Manager" in roles and "System Manager" not in roles,
"is_sales_user": "Sales User" in roles
and "Sales Manager" not in roles
and "System Manager" not in roles,
}
USER_FIELDS = [
"name",
"email",
"enabled",
"user_image",
"first_name",
"last_name",
"full_name",
"user_type",
"language",
]
@frappe.whitelist()
def get_users(include_all: bool = False):
"""Return (users, crm_users) for the CRM frontend.
By default (`include_all=False`) the User query is filtered at SQL level
to just users with CRM roles — typically a handful of rows. This unblocks
the UI on initial load even on sites with hundreds of thousands of users.
When `include_all=True` and the session has System Manager, the full
enabled user list is returned. The frontend uses this path for a
non-blocking background fetch that populates name/avatar info for
non-CRM users referenced in CRM activity. Non-System-Manager sessions
cannot escalate by passing this param.
"""
session_roles = get_session_role_flags()
# Param arrives as a string from the HTTP layer; coerce.
if isinstance(include_all, str):
include_all = include_all.lower() in ("1", "true", "yes")
if not session_roles["is_system_manager"]:
include_all = False
# Always need the CRM user name set — used both as the filter for the
# fast path and as the membership check on the full path.
crm_user_names = set(
frappe.get_all(
"Has Role",
filters={"parenttype": "User", "role": ["in", CRM_ALLOWED_ROLES]},
pluck="parent",
distinct=True,
)
)
crm_user_names.add("Administrator")
user_filters = {"enabled": 1}
if not include_all:
user_filters["name"] = ["in", list(crm_user_names)]
users = frappe.qb.get_query(
"User",
fields=USER_FIELDS,
order_by="full_name asc",
filters=user_filters,
).run(as_dict=1)
if not users:
return [], []
system_language = frappe.db.get_single_value("System Settings", "language")
session_user = frappe.session.user
# Has Role lookup — restrict by parent on the fast path (tiny IN list);
# unfiltered scan on the full path because the IN list would otherwise
# carry every enabled user name (see commit dropping IN-list filters).
if include_all:
role_filters = {"parenttype": "User"}
else:
role_filters = {"parenttype": "User", "parent": ["in", list(crm_user_names)]}
role_rows = frappe.get_all("Has Role", filters=role_filters, fields=["parent", "role"])
roles_by_user = {}
for row in role_rows:
roles_by_user.setdefault(row.parent, []).append(row.role)
# Telephony agent table is tiny on any real site; full pluck is cheaper
# than serializing an IN list and gives identical results.
telephony_agents = set(frappe.get_all("CRM Telephony Agent", pluck="user"))
role_priority = ("System Manager", "Sales Manager", "Sales User", "Guest")
crm_users = []
for user in users:
if session_user == user.name:
user.session_user = True
# Administrator has every role implicitly via frappe.get_roles() but
# its Has Role child table is not guaranteed to contain System Manager
# on every install — special-case it to avoid locking the admin out.
if user.name == "Administrator":
user.roles = ["System Manager", "All"]
user.role = "System Manager"
else:
# Mirror frappe.get_roles() which appends implicit "All" and "Guest"
user.roles = [*roles_by_user.get(user.name, []), "All", "Guest"]
user.role = ""
for role in role_priority:
if role in user.roles:
user.role = role
break
user.is_telephony_agent = user.name in telephony_agents
user.language = user.language or system_language
if user.role in CRM_ALLOWED_ROLES:
crm_users.append(user)
if not include_all:
# Fast path — both positions are the CRM users.
return crm_users, crm_users
return users, crm_users
@frappe.whitelist()
def get_user_info(users: str | list):
"""Resolve display info for a batch of User names.
Used by the frontend to fill in name/avatar info for non-CRM users
referenced in CRM activity (comment authors, doc owners, etc.) when
the background full-list fetch has not yet landed. Gated behind a
CRM role; capped at 200 names per call to limit enumeration cost.
"""
get_session_role_flags()
if isinstance(users, str):
users = frappe.parse_json(users)
if not users:
return []
return frappe.get_all(
"User",
filters={"name": ["in", list(users)[:200]]},
fields=["name", "email", "full_name", "user_image", "user_type"],
)
@frappe.whitelist()
def get_organizations():
get_session_role_flags()
organizations = frappe.qb.get_query(
"CRM Organization",
fields=["*"],
order_by="name asc",
distinct=True,
).run(as_dict=1)
return organizations