From a5ee0e7becbd3dc7b31d3ef7e19a9608da2e1592 Mon Sep 17 00:00:00 2001 From: Niraj Gautam Date: Wed, 26 Aug 2026 12:56:15 +0530 Subject: [PATCH] refactor: Route all GET endpoints to read replica via read_only --- helpdesk/api/agent_home/agent_home.py | 8 ++++++++ helpdesk/api/article.py | 2 ++ helpdesk/api/assignment_rule.py | 1 + helpdesk/api/auth.py | 2 ++ helpdesk/api/config.py | 1 + helpdesk/api/contact.py | 2 ++ helpdesk/api/dashboard.py | 1 + helpdesk/api/doc.py | 4 ++++ helpdesk/api/general.py | 1 + helpdesk/api/knowledge_base.py | 5 +++++ helpdesk/api/onboarding.py | 3 +++ helpdesk/api/saved_replies.py | 1 + helpdesk/api/search.py | 2 ++ helpdesk/api/session.py | 1 + helpdesk/api/settings/email_notifications.py | 1 + helpdesk/api/settings/field_dependency.py | 1 + helpdesk/api/ticket_analytics.py | 1 + helpdesk/api/ticket_stats.py | 5 +++++ helpdesk/helpdesk/doctype/hd_customer/hd_customer.py | 2 ++ .../hd_service_holiday_list/hd_service_holiday_list.py | 1 + helpdesk/helpdesk/doctype/hd_settings/helpers.py | 1 + helpdesk/helpdesk/doctype/hd_team/hd_team.py | 1 + helpdesk/helpdesk/doctype/hd_ticket/api.py | 8 ++++++++ helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py | 1 + .../doctype/hd_ticket_comment/hd_ticket_comment.py | 2 ++ helpdesk/helpdesk/doctype/hd_ticket_template/api.py | 1 + helpdesk/integrations/erpnext/api.py | 1 + 27 files changed, 60 insertions(+) diff --git a/helpdesk/api/agent_home/agent_home.py b/helpdesk/api/agent_home/agent_home.py index 537bec4981..04ffed4fb0 100644 --- a/helpdesk/api/agent_home/agent_home.py +++ b/helpdesk/api/agent_home/agent_home.py @@ -18,6 +18,7 @@ @frappe.whitelist() +@frappe.read_only() @agent_only def get_dashboard(reset_layout: bool = False): dashboard = frappe.db.exists("HD Field Layout", {"user": frappe.session.user}) @@ -78,6 +79,7 @@ def _resolve_window(period: str): @frappe.whitelist() +@frappe.read_only() @agent_only def get_agent_tickets(period: str = "last month"): current_from, current_to, previous_from, previous_to = _resolve_window(period) @@ -139,18 +141,21 @@ def get_ticket_data(from_date, to_date): @frappe.whitelist() +@frappe.read_only() @agent_only def get_avg_first_response_time(period: str = "last month"): return get_avg_time_metric(period, "first_response_time", scope="agent") @frappe.whitelist() +@frappe.read_only() @agent_only def get_avg_resolution_time(period: str = "last month"): return get_avg_time_metric(period, "resolution_time", scope="agent") @frappe.whitelist() +@frappe.read_only() @agent_only def get_recent_feedback( period: str = "all_time", @@ -275,6 +280,7 @@ def get_recent_feedback( @frappe.whitelist() +@frappe.read_only() @agent_only def get_avg_time_metrics( period: str = "6m", from_date: str = None, to_date: str = None @@ -571,6 +577,7 @@ def _get_pending_response_tickets(limit=10): @frappe.whitelist() +@frappe.read_only() @agent_only def get_pending_tickets(ticket_type: str = "upcoming_sla"): if ticket_type == "upcoming_sla": @@ -625,6 +632,7 @@ def get_pending_tickets(ticket_type: str = "upcoming_sla"): @frappe.whitelist() +@frappe.read_only() @agent_only def get_recent_activity() -> list[dict]: """The current agent's latest action per ticket (replied, commented, updated, diff --git a/helpdesk/api/article.py b/helpdesk/api/article.py index 3368c5c8a6..2408616b45 100644 --- a/helpdesk/api/article.py +++ b/helpdesk/api/article.py @@ -42,6 +42,7 @@ def sanitize_query(query: str) -> str: @frappe.whitelist() +@frappe.read_only() def get_article_stats(article_name: str): views = frappe.db.get_value("HD Article", article_name, "views") @@ -69,6 +70,7 @@ def get_article_stats(article_name: str): @frappe.whitelist() +@frappe.read_only() def search(query: str) -> list: query = sanitize_query(query) ret, enough = search_with_enough_results([], query) diff --git a/helpdesk/api/assignment_rule.py b/helpdesk/api/assignment_rule.py index c35a9c0053..89e6d04331 100644 --- a/helpdesk/api/assignment_rule.py +++ b/helpdesk/api/assignment_rule.py @@ -2,6 +2,7 @@ @frappe.whitelist() +@frappe.read_only() def get_assignment_rules_list(): if not frappe.has_permission("Assignment Rule", "read"): frappe.throw( diff --git a/helpdesk/api/auth.py b/helpdesk/api/auth.py index 38caa9badf..2500666239 100644 --- a/helpdesk/api/auth.py +++ b/helpdesk/api/auth.py @@ -5,6 +5,7 @@ @frappe.whitelist() +@frappe.read_only() def get_user(): current_user = frappe.session.user filters = {"name": current_user} @@ -78,6 +79,7 @@ def get_user(): @frappe.whitelist() +@frappe.read_only() @agent_only def get_current_user_email_info(): user = frappe.session.user diff --git a/helpdesk/api/config.py b/helpdesk/api/config.py index adab35e992..70721635d9 100644 --- a/helpdesk/api/config.py +++ b/helpdesk/api/config.py @@ -2,6 +2,7 @@ @frappe.whitelist(allow_guest=True) +@frappe.read_only() def get_config(): fields = [ "brand_name", diff --git a/helpdesk/api/contact.py b/helpdesk/api/contact.py index 9ccce1dc63..7d70b9069c 100644 --- a/helpdesk/api/contact.py +++ b/helpdesk/api/contact.py @@ -9,6 +9,7 @@ @frappe.whitelist(methods=["GET"]) +@frappe.read_only() def search_contacts( txt: str, additional_filters: str | list | None = None ) -> list[dict[Literal["full_name", "name", "email_id"], str]]: @@ -159,6 +160,7 @@ def edit_contact(name: str, doc: dict): @frappe.whitelist(methods=["GET"]) +@frappe.read_only() def get_contact_info(name: str) -> dict: frappe.has_permission("Contact", "read", doc=name, throw=True) contact = frappe.get_doc("Contact", name) diff --git a/helpdesk/api/dashboard.py b/helpdesk/api/dashboard.py index f12f8bdcde..50b1b4cfa2 100644 --- a/helpdesk/api/dashboard.py +++ b/helpdesk/api/dashboard.py @@ -21,6 +21,7 @@ @frappe.whitelist() +@frappe.read_only() @agent_only def get_dashboard_data( dashboard_type: str, filters: dict[str, any] = None diff --git a/helpdesk/api/doc.py b/helpdesk/api/doc.py index 7eedd825ae..89638a4516 100644 --- a/helpdesk/api/doc.py +++ b/helpdesk/api/doc.py @@ -19,6 +19,7 @@ @frappe.whitelist() +@frappe.read_only() def get_list_data( doctype: str, # flake8: noqa @@ -244,6 +245,7 @@ def get_options(fieldtype, options): @frappe.whitelist() +@frappe.read_only() @redis_cache() def get_filterable_fields( doctype: str, @@ -393,6 +395,7 @@ def get_filterable_fields( @frappe.whitelist() +@frappe.read_only() def sort_options(doctype: str, show_customer_portal_fields: bool = False): fields = frappe.get_meta(doctype).fields fields = [field for field in fields if field.fieldtype not in no_value_fields] @@ -422,6 +425,7 @@ def sort_options(doctype: str, show_customer_portal_fields: bool = False): @frappe.whitelist() +@frappe.read_only() def get_quick_filters(doctype: str, show_customer_portal_fields: bool = False): meta = frappe.get_meta(doctype) fields = [field for field in meta.fields if field.in_standard_filter] diff --git a/helpdesk/api/general.py b/helpdesk/api/general.py index 24f3432434..a2e314e390 100644 --- a/helpdesk/api/general.py +++ b/helpdesk/api/general.py @@ -3,6 +3,7 @@ @frappe.whitelist(allow_guest=True, methods=["GET"]) +@frappe.read_only() def get_translations(): language = None if frappe.session.user != "Guest": diff --git a/helpdesk/api/knowledge_base.py b/helpdesk/api/knowledge_base.py index 7722d9f88c..1b43340009 100644 --- a/helpdesk/api/knowledge_base.py +++ b/helpdesk/api/knowledge_base.py @@ -8,6 +8,7 @@ @frappe.whitelist(allow_guest=True) +@frappe.read_only() def get_article(name: str): article = frappe.get_doc("HD Article", name).as_dict() @@ -87,6 +88,7 @@ def move_to_category(category: str, articles: list[str]): @frappe.whitelist() +@frappe.read_only() def get_categories(): categories = frappe.get_list( "HD Article Category", @@ -103,6 +105,7 @@ def get_categories(): @frappe.whitelist() +@frappe.read_only() def get_category_articles(category: str): articles = frappe.get_list( "HD Article", @@ -140,6 +143,7 @@ def merge_category(source: str, target: str): @frappe.whitelist() +@frappe.read_only() def get_general_category(): return frappe.db.get_value( "HD Article Category", {"category_name": "General"}, "name" @@ -147,6 +151,7 @@ def get_general_category(): @frappe.whitelist() +@frappe.read_only() def get_category_title(category: str): return frappe.db.get_value("HD Article Category", category, "category_name") diff --git a/helpdesk/api/onboarding.py b/helpdesk/api/onboarding.py index b76976291a..0c28f954cb 100644 --- a/helpdesk/api/onboarding.py +++ b/helpdesk/api/onboarding.py @@ -22,6 +22,7 @@ def mark_persona_captured(brand_name: str | None = None) -> None: @frappe.whitelist() +@frappe.read_only() @agent_manager_only def get_welcome_ticket() -> str | None: """Name of the seeded welcome ticket, if it still exists.""" @@ -29,6 +30,7 @@ def get_welcome_ticket() -> str | None: @frappe.whitelist() +@frappe.read_only() def get_first_ticket(ticket: str | None = None): """Get first ticket created except the default ticket""" # If a cached ticket ID was passed, verify it still exists @@ -49,6 +51,7 @@ def get_first_ticket(ticket: str | None = None): @frappe.whitelist() +@frappe.read_only() def get_general_category_id(): """Get the id of the general category""" category = frappe.get_all( diff --git a/helpdesk/api/saved_replies.py b/helpdesk/api/saved_replies.py index 524d349bf2..c99381b080 100644 --- a/helpdesk/api/saved_replies.py +++ b/helpdesk/api/saved_replies.py @@ -13,6 +13,7 @@ @frappe.whitelist() +@frappe.read_only() @agent_only def get_rendered_saved_reply(ticket_id: str, saved_reply_id: str | None = None): if not saved_reply_id: diff --git a/helpdesk/api/search.py b/helpdesk/api/search.py index 9760b0be04..b3ffbf1b43 100644 --- a/helpdesk/api/search.py +++ b/helpdesk/api/search.py @@ -8,6 +8,7 @@ @frappe.whitelist() +@frappe.read_only() def search( query: str, filters: str | None = None, limit: int = 20, title_only: bool = False ): @@ -42,6 +43,7 @@ def search( @frappe.whitelist() +@frappe.read_only() def get_filter_options(): """Get available filter options for search interface""" from helpdesk.search_sqlite import HelpdeskSearch diff --git a/helpdesk/api/session.py b/helpdesk/api/session.py index 05307a9fe7..4a2fa99640 100644 --- a/helpdesk/api/session.py +++ b/helpdesk/api/session.py @@ -4,6 +4,7 @@ @frappe.whitelist() +@frappe.read_only() @agent_only def get_users(): session_user = frappe.session.user diff --git a/helpdesk/api/settings/email_notifications.py b/helpdesk/api/settings/email_notifications.py index cdceb42fc6..27e3b5b756 100644 --- a/helpdesk/api/settings/email_notifications.py +++ b/helpdesk/api/settings/email_notifications.py @@ -94,6 +94,7 @@ def get_reply_via_agent_data(): @frappe.whitelist(methods=["GET"]) +@frappe.read_only() def get_data(notification: str): only_for_managers() diff --git a/helpdesk/api/settings/field_dependency.py b/helpdesk/api/settings/field_dependency.py index 63843c8dda..aa1332defc 100644 --- a/helpdesk/api/settings/field_dependency.py +++ b/helpdesk/api/settings/field_dependency.py @@ -5,6 +5,7 @@ @frappe.whitelist() +@frappe.read_only() def get_field_dependency(name: str): """ Returns the field dependency for the given name. diff --git a/helpdesk/api/ticket_analytics.py b/helpdesk/api/ticket_analytics.py index b702d2649b..93b7e5f42b 100644 --- a/helpdesk/api/ticket_analytics.py +++ b/helpdesk/api/ticket_analytics.py @@ -39,6 +39,7 @@ @frappe.whitelist() +@frappe.read_only() @agent_only def get_ticket_analytics(ticket: str) -> dict: frappe.has_permission("HD Ticket", "read", ticket, throw=True) diff --git a/helpdesk/api/ticket_stats.py b/helpdesk/api/ticket_stats.py index 3416309499..b10d8ab1b6 100644 --- a/helpdesk/api/ticket_stats.py +++ b/helpdesk/api/ticket_stats.py @@ -51,6 +51,7 @@ def _fill_date_series(from_date_str, to_date_str, rows: list) -> list: @frappe.whitelist() +@frappe.read_only() @agent_only def get_feedback_received( scope: Scope, @@ -91,6 +92,7 @@ def get_feedback_received( @frappe.whitelist() +@frappe.read_only() @agent_only def get_sla_violations( dt: str, @@ -206,6 +208,7 @@ def _query(from_date, to_date, grouped=False): @frappe.whitelist() +@frappe.read_only() @agent_only def get_avg_first_response_time( dt: str, @@ -216,6 +219,7 @@ def get_avg_first_response_time( @frappe.whitelist() +@frappe.read_only() @agent_only def get_avg_resolution_time( dt: str, @@ -226,6 +230,7 @@ def get_avg_resolution_time( @frappe.whitelist() +@frappe.read_only() @agent_only def get_ticket_stats( dt: str, diff --git a/helpdesk/helpdesk/doctype/hd_customer/hd_customer.py b/helpdesk/helpdesk/doctype/hd_customer/hd_customer.py index dffd240682..9d95cb51db 100644 --- a/helpdesk/helpdesk/doctype/hd_customer/hd_customer.py +++ b/helpdesk/helpdesk/doctype/hd_customer/hd_customer.py @@ -168,6 +168,7 @@ def get_user(self, contact_name, throw_error=True): return None @frappe.whitelist() + @frappe.read_only() @agent_only def get_contacts(self): contact_names = [contact.contact_name for contact in self.contacts] @@ -332,6 +333,7 @@ def invite_contacts(self, to_invite: list[dict], role: str) -> dict: return result @frappe.whitelist() + @frappe.read_only() def get_pending_invites(self): if "Agent Manager" not in frappe.get_roles(): frappe.throw( diff --git a/helpdesk/helpdesk/doctype/hd_service_holiday_list/hd_service_holiday_list.py b/helpdesk/helpdesk/doctype/hd_service_holiday_list/hd_service_holiday_list.py index a85b397d5e..2ce6f70998 100644 --- a/helpdesk/helpdesk/doctype/hd_service_holiday_list/hd_service_holiday_list.py +++ b/helpdesk/helpdesk/doctype/hd_service_holiday_list/hd_service_holiday_list.py @@ -109,6 +109,7 @@ def clear_table(self): @frappe.whitelist() +@frappe.read_only() def get_events(start: str, end: str, filters: str | None = None): """Returns events for Gantt / Calendar view rendering. diff --git a/helpdesk/helpdesk/doctype/hd_settings/helpers.py b/helpdesk/helpdesk/doctype/hd_settings/helpers.py index d6980e1d0a..4b3f5453d5 100644 --- a/helpdesk/helpdesk/doctype/hd_settings/helpers.py +++ b/helpdesk/helpdesk/doctype/hd_settings/helpers.py @@ -94,6 +94,7 @@ def get_default_email_content(type: str) -> str: @frappe.whitelist() +@frappe.read_only() def get_banner_msg(): """Get current and default banner message for settings UI""" diff --git a/helpdesk/helpdesk/doctype/hd_team/hd_team.py b/helpdesk/helpdesk/doctype/hd_team/hd_team.py index 17a31d45dc..f26b90be8e 100644 --- a/helpdesk/helpdesk/doctype/hd_team/hd_team.py +++ b/helpdesk/helpdesk/doctype/hd_team/hd_team.py @@ -116,6 +116,7 @@ def unassign_condition(self, team_name: str) -> tuple[str, str]: @frappe.whitelist() +@frappe.read_only() @agent_only def get_team_members(team: str): return frappe.get_all("HD Team Member", filters={"parent": team}, pluck="user") diff --git a/helpdesk/helpdesk/doctype/hd_ticket/api.py b/helpdesk/helpdesk/doctype/hd_ticket/api.py index 271028dda4..f605788a1a 100644 --- a/helpdesk/helpdesk/doctype/hd_ticket/api.py +++ b/helpdesk/helpdesk/doctype/hd_ticket/api.py @@ -36,6 +36,7 @@ def new(doc: dict, attachments: list[dict] = []): @frappe.whitelist() +@frappe.read_only() def get_one(name: str, is_customer_portal: bool = False): frappe.has_permission("HD Ticket", "read", name, throw=True) QBContact = frappe.qb.DocType("Contact") @@ -552,6 +553,7 @@ def duplicate_ticket(ticket_doc, subject): @frappe.whitelist() +@frappe.read_only() @agent_only def get_ticket_customizations(): # get form script @@ -567,6 +569,7 @@ def get_ticket_customizations(): @frappe.whitelist() +@frappe.read_only() # TODO: make it bette, on mount fetch only once and cache it def get_navigation_tickets(ticket: str, current_view: str | None = None): """ @@ -654,6 +657,7 @@ def get_navigation_order_by(view): @frappe.whitelist() +@frappe.read_only() def get_ticket_contact(ticket: str): frappe.has_permission("HD Ticket", "read", ticket, throw=True) if not frappe.db.exists("HD Ticket", ticket): @@ -680,6 +684,7 @@ def get_ticket_contact(ticket: str): @frappe.whitelist() +@frappe.read_only() def get_recent_similar_tickets(ticket: str): frappe.has_permission("HD Ticket", "read", str(ticket), throw=True) if not frappe.db.exists("HD Ticket", ticket): @@ -735,6 +740,7 @@ def get_recent_tickets(ticket: str): @frappe.whitelist() +@frappe.read_only() def get_ticket_activities(ticket: str): frappe.has_permission("HD Ticket", "read", ticket, throw=True) activities = { @@ -748,6 +754,7 @@ def get_ticket_activities(ticket: str): @frappe.whitelist() +@frappe.read_only() def get_ticket_assignees(ticket: str) -> list[dict]: frappe.has_permission("HD Ticket", "read", ticket, throw=True) assignee_names = json.loads( @@ -784,6 +791,7 @@ def show_banner_next_day(ticket): @frappe.whitelist() +@frappe.read_only() def show_outside_hours_banner(ticket_name: str): show_banner_settings = frappe.db.get_single_value( "HD Settings", "enable_outside_hours_banner" diff --git a/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py b/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py index 51010362da..53de162f0c 100644 --- a/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py +++ b/helpdesk/helpdesk/doctype/hd_ticket/hd_ticket.py @@ -598,6 +598,7 @@ def instantly_send_email(self): return bool(int(check)) @frappe.whitelist() + @frappe.read_only() def get_last_communication(self): filters = { "reference_doctype": "HD Ticket", diff --git a/helpdesk/helpdesk/doctype/hd_ticket_comment/hd_ticket_comment.py b/helpdesk/helpdesk/doctype/hd_ticket_comment/hd_ticket_comment.py index c538315a85..356c7e9aa1 100644 --- a/helpdesk/helpdesk/doctype/hd_ticket_comment/hd_ticket_comment.py +++ b/helpdesk/helpdesk/doctype/hd_ticket_comment/hd_ticket_comment.py @@ -104,6 +104,7 @@ def toggle_reaction(comment: str, emoji: str): @frappe.whitelist() +@frappe.read_only() def get_reactions(comment: str): if not frappe.db.get_single_value("HD Settings", "enable_comment_reactions"): return [] @@ -187,5 +188,6 @@ def notify_reaction(doc, emoji, user): @frappe.whitelist() +@frappe.read_only() def get_preset_emojis(): return PRESET_EMOJIS diff --git a/helpdesk/helpdesk/doctype/hd_ticket_template/api.py b/helpdesk/helpdesk/doctype/hd_ticket_template/api.py index 55b858ceab..233520e88f 100644 --- a/helpdesk/helpdesk/doctype/hd_ticket_template/api.py +++ b/helpdesk/helpdesk/doctype/hd_ticket_template/api.py @@ -13,6 +13,7 @@ @frappe.whitelist() +@frappe.read_only() def get_one(name: str): check_permissions(DOCTYPE_TEMPLATE, None) found, about, description_template = frappe.get_value( diff --git a/helpdesk/integrations/erpnext/api.py b/helpdesk/integrations/erpnext/api.py index 65637bbe15..4eacc575c0 100644 --- a/helpdesk/integrations/erpnext/api.py +++ b/helpdesk/integrations/erpnext/api.py @@ -7,6 +7,7 @@ @frappe.whitelist() +@frappe.read_only() def get_sync_info() -> dict: """Drive the ERPNext integration settings UI. `in_sync` gates the 'Sync now' action. Whether ERPNext is installed is read on the frontend from the boot