Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 
 
 

615 řádky
16 KiB

  1. # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
  2. # MIT License. See license.txt
  3. # Author - Shivam Mishra <shivam@frappe.io>
  4. from __future__ import unicode_literals
  5. import frappe
  6. from json import loads, dumps
  7. from frappe import _, DoesNotExistError, ValidationError, _dict
  8. from frappe.boot import get_allowed_pages, get_allowed_reports
  9. from six import string_types
  10. from functools import wraps
  11. from frappe.cache_manager import (
  12. build_domain_restriced_doctype_cache,
  13. build_domain_restriced_page_cache,
  14. build_table_count_cache
  15. )
  16. def handle_not_exist(fn):
  17. @wraps(fn)
  18. def wrapper(*args, **kwargs):
  19. try:
  20. return fn(*args, **kwargs)
  21. except DoesNotExistError:
  22. if frappe.message_log:
  23. frappe.message_log.pop()
  24. return []
  25. return wrapper
  26. class Workspace:
  27. def __init__(self, page_name, minimal=False):
  28. self.page_name = page_name
  29. self.extended_links = []
  30. self.extended_charts = []
  31. self.extended_shortcuts = []
  32. self.user = frappe.get_user()
  33. self.allowed_modules = self.get_cached('user_allowed_modules', self.get_allowed_modules)
  34. self.doc = self.get_page_for_user()
  35. if self.doc.module and self.doc.module not in self.allowed_modules:
  36. raise frappe.PermissionError
  37. self.can_read = self.get_cached('user_perm_can_read', self.get_can_read_items)
  38. self.allowed_pages = get_allowed_pages(cache=True)
  39. self.allowed_reports = get_allowed_reports(cache=True)
  40. if not minimal:
  41. self.onboarding_doc = self.get_onboarding_doc()
  42. self.onboarding = None
  43. self.table_counts = get_table_with_counts()
  44. self.restricted_doctypes = frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache()
  45. self.restricted_pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache()
  46. def is_page_allowed(self):
  47. cards = self.doc.get_link_groups() + get_custom_reports_and_doctypes(self.doc.module) + self.extended_links
  48. shortcuts = self.doc.shortcuts + self.extended_shortcuts
  49. for section in cards:
  50. links = loads(section.get('links')) if isinstance(section.get('links'), string_types) else section.get('links')
  51. for item in links:
  52. if self.is_item_allowed(item.get('link_to'), item.get('link_type')):
  53. return True
  54. def _in_active_domains(item):
  55. if not item.restrict_to_domain:
  56. return True
  57. else:
  58. return item.restrict_to_domain in frappe.get_active_domains()
  59. for item in shortcuts:
  60. if self.is_item_allowed(item.link_to, item.type) and _in_active_domains(item):
  61. return True
  62. return False
  63. def get_cached(self, cache_key, fallback_fn):
  64. _cache = frappe.cache()
  65. value = _cache.get_value(cache_key, user=frappe.session.user)
  66. if value:
  67. return value
  68. value = fallback_fn()
  69. # Expire every six hour
  70. _cache.set_value(cache_key, value, frappe.session.user, 21600)
  71. return value
  72. def get_can_read_items(self):
  73. if not self.user.can_read:
  74. self.user.build_permissions()
  75. return self.user.can_read
  76. def get_allowed_modules(self):
  77. if not self.user.allow_modules:
  78. self.user.build_permissions()
  79. return self.user.allow_modules
  80. def get_page_for_user(self):
  81. filters = {
  82. 'extends': self.page_name,
  83. 'for_user': frappe.session.user
  84. }
  85. user_pages = frappe.get_all("Workspace", filters=filters, limit=1)
  86. if user_pages:
  87. return frappe.get_cached_doc("Workspace", user_pages[0])
  88. filters = {
  89. 'extends_another_page': 1,
  90. 'extends': self.page_name,
  91. 'is_default': 1
  92. }
  93. default_page = frappe.get_all("Workspace", filters=filters, limit=1)
  94. if default_page:
  95. return frappe.get_cached_doc("Workspace", default_page[0])
  96. self.get_pages_to_extend()
  97. return frappe.get_cached_doc("Workspace", self.page_name)
  98. def get_onboarding_doc(self):
  99. # Check if onboarding is enabled
  100. if not frappe.get_system_settings("enable_onboarding"):
  101. return None
  102. if not self.doc.onboarding:
  103. return None
  104. if frappe.db.get_value("Module Onboarding", self.doc.onboarding, "is_complete"):
  105. return None
  106. doc = frappe.get_doc("Module Onboarding", self.doc.onboarding)
  107. # Check if user is allowed
  108. allowed_roles = set(doc.get_allowed_roles())
  109. user_roles = set(frappe.get_roles())
  110. if not allowed_roles & user_roles:
  111. return None
  112. # Check if already complete
  113. if doc.check_completion():
  114. return None
  115. return doc
  116. def get_pages_to_extend(self):
  117. pages = frappe.get_all("Workspace", filters={
  118. "extends": self.page_name,
  119. 'restrict_to_domain': ['in', frappe.get_active_domains()],
  120. 'for_user': '',
  121. 'module': ['in', self.allowed_modules]
  122. })
  123. pages = [frappe.get_cached_doc("Workspace", page['name']) for page in pages]
  124. for page in pages:
  125. self.extended_links = self.extended_links + page.get_link_groups()
  126. self.extended_charts = self.extended_charts + page.charts
  127. self.extended_shortcuts = self.extended_shortcuts + page.shortcuts
  128. def is_item_allowed(self, name, item_type):
  129. if frappe.session.user == "Administrator":
  130. return True
  131. item_type = item_type.lower()
  132. if item_type == "doctype":
  133. return (name in self.can_read or [] and name in self.restricted_doctypes or [])
  134. if item_type == "page":
  135. return (name in self.allowed_pages and name in self.restricted_pages)
  136. if item_type == "report":
  137. return name in self.allowed_reports
  138. if item_type == "help":
  139. return True
  140. if item_type == "dashboard":
  141. return True
  142. return False
  143. def build_workspace(self):
  144. self.cards = {
  145. 'label': _(self.doc.cards_label),
  146. 'items': self.get_links()
  147. }
  148. self.charts = {
  149. 'label': _(self.doc.charts_label),
  150. 'items': self.get_charts()
  151. }
  152. self.shortcuts = {
  153. 'label': _(self.doc.shortcuts_label),
  154. 'items': self.get_shortcuts()
  155. }
  156. if self.onboarding_doc:
  157. self.onboarding = {
  158. 'label': _(self.onboarding_doc.title),
  159. 'subtitle': _(self.onboarding_doc.subtitle),
  160. 'success': _(self.onboarding_doc.success_message),
  161. 'docs_url': self.onboarding_doc.documentation_url,
  162. 'items': self.get_onboarding_steps()
  163. }
  164. def _doctype_contains_a_record(self, name):
  165. exists = self.table_counts.get(name, False)
  166. if not exists and frappe.db.exists(name):
  167. if not frappe.db.get_value('DocType', name, 'issingle'):
  168. exists = bool(frappe.db.get_all(name, limit=1))
  169. else:
  170. exists = True
  171. self.table_counts[name] = exists
  172. return exists
  173. def _prepare_item(self, item):
  174. if item.dependencies:
  175. dependencies = [dep.strip() for dep in item.dependencies.split(",")]
  176. incomplete_dependencies = [d for d in dependencies if not self._doctype_contains_a_record(d)]
  177. if len(incomplete_dependencies):
  178. item.incomplete_dependencies = incomplete_dependencies
  179. else:
  180. item.incomplete_dependencies = ""
  181. if item.onboard:
  182. # Mark Spotlights for initial
  183. if item.get("type") == "doctype":
  184. name = item.get("name")
  185. count = self._doctype_contains_a_record(name)
  186. item["count"] = count
  187. # Translate label
  188. item["label"] = _(item.label) if item.label else _(item.name)
  189. return item
  190. @handle_not_exist
  191. def get_links(self):
  192. cards = self.doc.get_link_groups()
  193. if not self.doc.hide_custom:
  194. cards = cards + get_custom_reports_and_doctypes(self.doc.module)
  195. if len(self.extended_links):
  196. cards = merge_cards_based_on_label(cards + self.extended_links)
  197. default_country = frappe.db.get_default("country")
  198. new_data = []
  199. for card in cards:
  200. new_items = []
  201. card = _dict(card)
  202. links = card.get('links', [])
  203. for item in links:
  204. item = _dict(item)
  205. # Condition: based on country
  206. if item.country and item.country != default_country:
  207. continue
  208. # Check if user is allowed to view
  209. if self.is_item_allowed(item.link_to, item.link_type):
  210. prepared_item = self._prepare_item(item)
  211. new_items.append(prepared_item)
  212. if new_items:
  213. if isinstance(card, _dict):
  214. new_card = card.copy()
  215. else:
  216. new_card = card.as_dict().copy()
  217. new_card["links"] = new_items
  218. new_card["label"] = _(new_card["label"])
  219. new_data.append(new_card)
  220. return new_data
  221. @handle_not_exist
  222. def get_charts(self):
  223. all_charts = []
  224. if frappe.has_permission("Dashboard Chart", throw=False):
  225. charts = self.doc.charts
  226. if len(self.extended_charts):
  227. charts = charts + self.extended_charts
  228. for chart in charts:
  229. if frappe.has_permission('Dashboard Chart', doc=chart.chart_name):
  230. # Translate label
  231. chart.label = _(chart.label) if chart.label else _(chart.chart_name)
  232. all_charts.append(chart)
  233. return all_charts
  234. @handle_not_exist
  235. def get_shortcuts(self):
  236. def _in_active_domains(item):
  237. if not item.restrict_to_domain:
  238. return True
  239. else:
  240. return item.restrict_to_domain in frappe.get_active_domains()
  241. items = []
  242. shortcuts = self.doc.shortcuts
  243. if len(self.extended_shortcuts):
  244. shortcuts = shortcuts + self.extended_shortcuts
  245. for item in shortcuts:
  246. new_item = item.as_dict().copy()
  247. if self.is_item_allowed(item.link_to, item.type) and _in_active_domains(item):
  248. if item.type == "Report":
  249. report = self.allowed_reports.get(item.link_to, {})
  250. if report.get("report_type") in ["Query Report", "Script Report", "Custom Report"]:
  251. new_item['is_query_report'] = 1
  252. else:
  253. new_item['ref_doctype'] = report.get('ref_doctype')
  254. # Translate label
  255. new_item["label"] = _(item.label) if item.label else _(item.link_to)
  256. items.append(new_item)
  257. return items
  258. @handle_not_exist
  259. def get_onboarding_steps(self):
  260. steps = []
  261. for doc in self.onboarding_doc.get_steps():
  262. step = doc.as_dict().copy()
  263. step.label = _(doc.title)
  264. if step.action == "Create Entry":
  265. step.is_submittable = frappe.db.get_value("DocType", step.reference_document, 'is_submittable', cache=True)
  266. steps.append(step)
  267. return steps
  268. @frappe.whitelist()
  269. @frappe.read_only()
  270. def get_desktop_page(page):
  271. """Applies permissions, customizations and returns the configruration for a page
  272. on desk.
  273. Args:
  274. page (string): page name
  275. Returns:
  276. dict: dictionary of cards, charts and shortcuts to be displayed on website
  277. """
  278. try:
  279. wspace = Workspace(page)
  280. wspace.build_workspace()
  281. return {
  282. 'charts': wspace.charts,
  283. 'shortcuts': wspace.shortcuts,
  284. 'cards': wspace.cards,
  285. 'onboarding': wspace.onboarding,
  286. 'allow_customization': not wspace.doc.disable_user_customization
  287. }
  288. except DoesNotExistError:
  289. return {}
  290. @frappe.whitelist()
  291. def get_desk_sidebar_items():
  292. """Get list of sidebar items for desk"""
  293. # don't get domain restricted pages
  294. blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules()
  295. filters = {
  296. 'restrict_to_domain': ['in', frappe.get_active_domains()],
  297. 'extends_another_page': 0,
  298. 'for_user': '',
  299. 'module': ['not in', blocked_modules]
  300. }
  301. if not frappe.local.conf.developer_mode:
  302. filters['developer_mode_only'] = '0'
  303. # pages sorted based on pinned to top and then by name
  304. order_by = "pin_to_top desc, pin_to_bottom asc, name asc"
  305. all_pages = frappe.get_all("Workspace", fields=["name", "category", "icon", "module"],
  306. filters=filters, order_by=order_by, ignore_permissions=True)
  307. pages = []
  308. # Filter Page based on Permission
  309. for page in all_pages:
  310. try:
  311. wspace = Workspace(page.get('name'), True)
  312. if wspace.is_page_allowed():
  313. pages.append(page)
  314. page['label'] = _(page.get('name'))
  315. except frappe.PermissionError:
  316. pass
  317. return pages
  318. def get_table_with_counts():
  319. counts = frappe.cache().get_value("information_schema:counts")
  320. if not counts:
  321. counts = build_table_count_cache()
  322. return counts
  323. def get_custom_reports_and_doctypes(module):
  324. return [
  325. _dict({
  326. "label": _("Custom Documents"),
  327. "links": get_custom_doctype_list(module)
  328. }),
  329. _dict({
  330. "label": _("Custom Reports"),
  331. "links": get_custom_report_list(module)
  332. }),
  333. ]
  334. def get_custom_doctype_list(module):
  335. doctypes = frappe.get_all("DocType", fields=["name"], filters={"custom": 1, "istable": 0, "module": module}, order_by="name")
  336. out = []
  337. for d in doctypes:
  338. out.append({
  339. "type": "Link",
  340. "link_type": "doctype",
  341. "link_to": d.name,
  342. "label": _(d.name)
  343. })
  344. return out
  345. def get_custom_report_list(module):
  346. """Returns list on new style reports for modules."""
  347. reports = frappe.get_all("Report", fields=["name", "ref_doctype", "report_type"], filters=
  348. {"is_standard": "No", "disabled": 0, "module": module},
  349. order_by="name")
  350. out = []
  351. for r in reports:
  352. out.append({
  353. "type": "Link",
  354. "link_type": "report",
  355. "doctype": r.ref_doctype,
  356. "is_query_report": 1 if r.report_type in ("Query Report", "Script Report", "Custom Report") else 0,
  357. "label": _(r.name),
  358. "link_to": r.name,
  359. })
  360. return out
  361. def get_custom_workspace_for_user(page):
  362. """Get custom page from workspace if exists or create one
  363. Args:
  364. page (stirng): Page name
  365. Returns:
  366. Object: Document object
  367. """
  368. filters = {
  369. 'extends': page,
  370. 'for_user': frappe.session.user
  371. }
  372. pages = frappe.get_list("Workspace", filters=filters)
  373. if pages:
  374. return frappe.get_doc("Workspace", pages[0])
  375. doc = frappe.new_doc("Workspace")
  376. doc.extends = page
  377. doc.for_user = frappe.session.user
  378. return doc
  379. @frappe.whitelist()
  380. def save_customization(page, config):
  381. """Save customizations as a separate doctype in Workspace per user
  382. Args:
  383. page (string): Name of the page to be edited
  384. config (dict): Dictionary config of al widgets
  385. Returns:
  386. Boolean: Customization saving status
  387. """
  388. original_page = frappe.get_doc("Workspace", page)
  389. page_doc = get_custom_workspace_for_user(page)
  390. # Update field values
  391. page_doc.update({
  392. "icon": original_page.icon,
  393. "charts_label": original_page.charts_label,
  394. "cards_label": original_page.cards_label,
  395. "shortcuts_label": original_page.shortcuts_label,
  396. "module": original_page.module,
  397. "onboarding": original_page.onboarding,
  398. "developer_mode_only": original_page.developer_mode_only,
  399. "category": original_page.category
  400. })
  401. config = _dict(loads(config))
  402. if config.charts:
  403. page_doc.charts = prepare_widget(config.charts, "Workspace Chart", "charts")
  404. if config.shortcuts:
  405. page_doc.shortcuts = prepare_widget(config.shortcuts, "Workspace Shortcut", "shortcuts")
  406. if config.cards:
  407. page_doc.build_links_table_from_cards(config.cards)
  408. # Set label
  409. page_doc.label = page + '-' + frappe.session.user
  410. try:
  411. if page_doc.is_new():
  412. page_doc.insert(ignore_permissions=True)
  413. else:
  414. page_doc.save(ignore_permissions=True)
  415. except (ValidationError, TypeError) as e:
  416. # Create a json string to log
  417. json_config = dumps(config, sort_keys=True, indent=4)
  418. # Error log body
  419. log = \
  420. """
  421. page: {0}
  422. config: {1}
  423. exception: {2}
  424. """.format(page, json_config, e)
  425. frappe.log_error(log, _("Could not save customization"))
  426. return False
  427. return True
  428. def prepare_widget(config, doctype, parentfield):
  429. """Create widget child table entries with parent details
  430. Args:
  431. config (dict): Dictionary containing widget config
  432. doctype (string): Doctype name of the child table
  433. parentfield (string): Parent field for the child table
  434. Returns:
  435. TYPE: List of Document objects
  436. """
  437. if not config:
  438. return []
  439. order = config.get('order')
  440. widgets = config.get('widgets')
  441. prepare_widget_list = []
  442. for idx, name in enumerate(order):
  443. wid_config = widgets[name].copy()
  444. # Some cleanup
  445. wid_config.pop("name", None)
  446. # New Doc
  447. doc = frappe.new_doc(doctype)
  448. doc.update(wid_config)
  449. # Manually Set IDX
  450. doc.idx = idx + 1
  451. # Set Parent Field
  452. doc.parentfield = parentfield
  453. prepare_widget_list.append(doc)
  454. return prepare_widget_list
  455. @frappe.whitelist()
  456. def update_onboarding_step(name, field, value):
  457. """Update status of onboaridng step
  458. Args:
  459. name (string): Name of the doc
  460. field (string): field to be updated
  461. value: Value to be updated
  462. """
  463. frappe.db.set_value("Onboarding Step", name, field, value)
  464. @frappe.whitelist()
  465. def reset_customization(page):
  466. """Reset workspace customizations for a user
  467. Args:
  468. page (string): Name of the page to be reset
  469. """
  470. page_doc = get_custom_workspace_for_user(page)
  471. page_doc.delete()
  472. def merge_cards_based_on_label(cards):
  473. """Merge cards with common label."""
  474. cards_dict = {}
  475. for card in cards:
  476. label = card.get('label')
  477. if label in cards_dict:
  478. links = cards_dict[label].links + card.links
  479. cards_dict[label].update(dict(links=links))
  480. cards_dict[label] = cards_dict.pop(label)
  481. else:
  482. cards_dict[label] = card
  483. return list(cards_dict.values())