Просмотр исходного кода

Merge branch 'develop' into twofactor

version-14
ckosiegbu 8 лет назад
Родитель
Сommit
86bfd7bf68
100 измененных файлов: 2237 добавлений и 1732 удалений
  1. +3
    -1
      .eslintrc
  2. +1
    -2
      .travis.yml
  3. +1
    -1
      frappe/__init__.py
  4. +2
    -0
      frappe/boot.py
  5. +0
    -1
      frappe/change_log/v6/v6_16_4.md
  6. +11
    -31
      frappe/commands/docs.py
  7. +4
    -6
      frappe/config/docs.py
  8. +1
    -2
      frappe/core/doctype/communication/feed.py
  9. +67
    -39
      frappe/core/doctype/system_settings/system_settings.json
  10. +1
    -0
      frappe/core/doctype/system_settings/system_settings.py
  11. +2
    -1
      frappe/core/doctype/test_runner/test_runner.js
  12. +3
    -2
      frappe/core/doctype/test_runner/test_runner.py
  13. +5
    -1
      frappe/core/doctype/user/user.js
  14. +1
    -1
      frappe/core/doctype/user/user.py
  15. +0
    -0
      frappe/core/doctype/user_permission/__init__.py
  16. +23
    -0
      frappe/core/doctype/user_permission/test_user_permission.js
  17. +10
    -0
      frappe/core/doctype/user_permission/test_user_permission.py
  18. +10
    -0
      frappe/core/doctype/user_permission/user_permission.js
  19. +188
    -0
      frappe/core/doctype/user_permission/user_permission.json
  20. +80
    -0
      frappe/core/doctype/user_permission/user_permission.py
  21. +16
    -8
      frappe/core/page/permission_manager/permission_manager.js
  22. +3
    -13
      frappe/core/page/permission_manager/permission_manager.py
  23. +0
    -1
      frappe/core/page/user_permissions/README.md
  24. +0
    -3
      frappe/core/page/user_permissions/__init__.py
  25. +0
    -365
      frappe/core/page/user_permissions/user_permissions.js
  26. +0
    -19
      frappe/core/page/user_permissions/user_permissions.json
  27. +0
    -109
      frappe/core/page/user_permissions/user_permissions.py
  28. +4
    -19
      frappe/defaults.py
  29. +0
    -11
      frappe/desk/form/load.py
  30. Двоичные данные
      frappe/docs/assets/img/desk/animated_line_graph.gif
  31. Двоичные данные
      frappe/docs/assets/img/desk/bar_graph.png
  32. Двоичные данные
      frappe/docs/assets/img/desk/line_graph.png
  33. Двоичные данные
      frappe/docs/assets/img/desk/line_graph_sales.png
  34. Двоичные данные
      frappe/docs/assets/img/desk/percentage_graph.png
  35. +0
    -10
      frappe/docs/contents.html
  36. +0
    -9
      frappe/docs/contents.py
  37. +0
    -57
      frappe/docs/index.html
  38. +0
    -6
      frappe/docs/index.txt
  39. +0
    -30
      frappe/docs/install.md
  40. +0
    -16
      frappe/docs/license.html
  41. +12
    -56
      frappe/docs/user/en/guides/app-development/generating-docs.md
  42. +94
    -55
      frappe/docs/user/en/guides/desk/making_graphs.md
  43. +1
    -1
      frappe/docs/user/en/guides/reports-and-printing/how-to-make-query-report.md
  44. +2
    -2
      frappe/docs/user/en/guides/reports-and-printing/how-to-make-script-reports.md
  45. +9
    -3
      frappe/docs/user/en/tutorial/before.md
  46. +3
    -2
      frappe/model/create_new.py
  47. +1
    -1
      frappe/model/db_query.py
  48. +8
    -3
      frappe/model/delete_doc.py
  49. +1
    -0
      frappe/patches.txt
  50. +0
    -0
      frappe/patches/v8_x/__init__.py
  51. +25
    -0
      frappe/patches/v8_x/update_user_permission.py
  52. +56
    -19
      frappe/permissions.py
  53. +3
    -2
      frappe/public/build.json
  54. +1
    -1
      frappe/public/css/desk.css
  55. +0
    -6
      frappe/public/css/docs.css
  56. +0
    -74
      frappe/public/css/form.css
  57. +274
    -0
      frappe/public/css/graphs.css
  58. +25
    -73
      frappe/public/css/website.css
  59. +1
    -5
      frappe/public/js/frappe/defaults.js
  60. +2
    -3
      frappe/public/js/frappe/form/dashboard.js
  61. +6
    -1
      frappe/public/js/frappe/list/list_renderer.js
  62. +3
    -3
      frappe/public/js/frappe/list/list_view.js
  63. +4
    -2
      frappe/public/js/frappe/model/create_new.js
  64. +0
    -1
      frappe/public/js/frappe/model/model.js
  65. +1
    -0
      frappe/public/js/frappe/ui/filters/filters.js
  66. +0
    -308
      frappe/public/js/frappe/ui/graph.js
  67. +569
    -0
      frappe/public/js/frappe/ui/graphs.js
  68. +3
    -3
      frappe/public/js/frappe/ui/messages.js
  69. +2
    -2
      frappe/public/js/frappe/views/reports/query_report.js
  70. +2
    -2
      frappe/public/js/frappe/views/reports/reportview.js
  71. +1
    -1
      frappe/public/less/desk.less
  72. +0
    -6
      frappe/public/less/docs.less
  73. +4
    -105
      frappe/public/less/form.less
  74. +319
    -0
      frappe/public/less/graphs.less
  75. +34
    -96
      frappe/public/less/website.less
  76. +4
    -4
      frappe/templates/autodoc/dev_home.html
  77. +7
    -49
      frappe/templates/autodoc/docs_home.html
  78. +3
    -1
      frappe/templates/autodoc/doctype.html
  79. +1
    -1
      frappe/templates/autodoc/macros.html
  80. +3
    -0
      frappe/templates/autodoc/models_home.html
  81. +3
    -0
      frappe/templates/autodoc/module_home.html
  82. +3
    -0
      frappe/templates/autodoc/package_index.html
  83. +3
    -0
      frappe/templates/autodoc/pymodule.html
  84. +19
    -0
      frappe/templates/includes/blog/blog.html
  85. +1
    -1
      frappe/templates/includes/comments/comment.html
  86. +3
    -1
      frappe/templates/includes/full_index.html
  87. +1
    -1
      frappe/templates/includes/web_sidebar.html
  88. +9
    -6
      frappe/templates/web.html
  89. +2
    -2
      frappe/test_runner.py
  90. +1
    -1
      frappe/tests/test_goal.py
  91. +77
    -31
      frappe/tests/test_permissions.py
  92. +19
    -0
      frappe/tests/ui/test_linked_with.js
  93. +2
    -1
      frappe/tests/ui/tests.txt
  94. +21
    -5
      frappe/utils/goal.py
  95. +10
    -3
      frappe/utils/jinja.py
  96. +47
    -6
      frappe/utils/setup_docs.py
  97. +28
    -13
      frappe/website/context.py
  98. +33
    -3
      frappe/website/doctype/blog_post/templates/blog_post.html
  99. +3
    -3
      frappe/website/doctype/blog_post/templates/blog_post_row.html
  100. +32
    -0
      frappe/website/purifycss.py

+ 3
- 1
.eslintrc Просмотреть файл

@@ -118,6 +118,8 @@
"getCookie": true, "getCookie": true,
"getCookies": true, "getCookies": true,
"get_url_arg": true, "get_url_arg": true,
"QUnit": true
"QUnit": true,
"Snap": true,
"mina": true
} }
} }

+ 1
- 2
.travis.yml Просмотреть файл

@@ -15,8 +15,7 @@ services:
- mysql - mysql


install: install:
- pip install flake8 # pytest
# stop the build if there are Python syntax errors or undefined names
- pip install flake8==3.3.0
- flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics
- sudo rm /etc/apt/sources.list.d/docker.list - sudo rm /etc/apt/sources.list.d/docker.list
- sudo apt-get purge -y mysql-common mysql-server mysql-client - sudo apt-get purge -y mysql-common mysql-server mysql-client


+ 1
- 1
frappe/__init__.py Просмотреть файл

@@ -14,7 +14,7 @@ import os, sys, importlib, inspect, json
from .exceptions import * from .exceptions import *
from .utils.jinja import get_jenv, get_template, render_template, get_email_from_template from .utils.jinja import get_jenv, get_template, render_template, get_email_from_template


__version__ = '8.6.2'
__version__ = '8.6.3'
__title__ = "Frappe Framework" __title__ = "Frappe Framework"


local = Local() local = Local()


+ 2
- 0
frappe/boot.py Просмотреть файл

@@ -17,6 +17,7 @@ from frappe.utils.change_log import get_versions
from frappe.translate import get_lang_dict from frappe.translate import get_lang_dict
from frappe.email.inbox import get_email_accounts from frappe.email.inbox import get_email_accounts
from frappe.core.doctype.feedback_trigger.feedback_trigger import get_enabled_feedback_trigger from frappe.core.doctype.feedback_trigger.feedback_trigger import get_enabled_feedback_trigger
from frappe.core.doctype.user_permission.user_permission import get_user_permissions


def get_bootinfo(): def get_bootinfo():
"""build and return boot info""" """build and return boot info"""
@@ -30,6 +31,7 @@ def get_bootinfo():


# system info # system info
bootinfo.sysdefaults = frappe.defaults.get_defaults() bootinfo.sysdefaults = frappe.defaults.get_defaults()
bootinfo.user_permissions = get_user_permissions()
bootinfo.server_date = frappe.utils.nowdate() bootinfo.server_date = frappe.utils.nowdate()


if frappe.session['user'] != 'Guest': if frappe.session['user'] != 'Guest':


+ 0
- 1
frappe/change_log/v6/v6_16_4.md Просмотреть файл

@@ -1,2 +1 @@
- Developer Tutorial [Videos](http://frappe.github.io/frappe/user/videos/)
- Increased uploaded file size limit upto 10MB - Increased uploaded file size limit upto 10MB

+ 11
- 31
frappe/commands/docs.py Просмотреть файл

@@ -1,31 +1,9 @@
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
import click import click
import os
import os, shutil
import frappe import frappe
from frappe.commands import pass_context from frappe.commands import pass_context



@click.command('write-docs')
@pass_context
@click.argument('app')
@click.option('--target', default=None)
@click.option('--local', default=False, is_flag=True, help='Run app locally')
def write_docs(context, app, target=None, local=False):
"Setup docs in target folder of target app"
from frappe.utils.setup_docs import setup_docs

if not target:
target = os.path.abspath(os.path.join("..", "docs", app))

for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
make = setup_docs(app)
make.make_docs(target, local)
finally:
frappe.destroy()

@click.command('build-docs') @click.command('build-docs')
@pass_context @pass_context
@click.argument('app') @click.argument('app')
@@ -36,23 +14,26 @@ def write_docs(context, app, target=None, local=False):
def build_docs(context, app, docs_version="current", target=None, local=False, watch=False): def build_docs(context, app, docs_version="current", target=None, local=False, watch=False):
"Setup docs in target folder of target app" "Setup docs in target folder of target app"
from frappe.utils import watch as start_watch from frappe.utils import watch as start_watch
if not target:
target = os.path.abspath(os.path.join("..", "docs", app))
from frappe.utils.setup_docs import add_breadcrumbs_tag


for site in context.sites: for site in context.sites:
_build_docs_once(site, app, docs_version, target, local) _build_docs_once(site, app, docs_version, target, local)


if watch: if watch:
def trigger_make(source_path, event_type): def trigger_make(source_path, event_type):
if "/templates/autodoc/" in source_path:
_build_docs_once(site, app, docs_version, target, local)
if "/docs/user/" in source_path:
# user file
target_path = frappe.get_app_path(target, 'www', 'docs', 'user',
os.path.relpath(source_path, start=frappe.get_app_path(app, 'docs', 'user')))
shutil.copy(source_path, target_path)
add_breadcrumbs_tag(target_path)


elif ("/docs.css" in source_path elif ("/docs.css" in source_path
or "/docs/" in source_path or "/docs/" in source_path
or "docs.py" in source_path): or "docs.py" in source_path):
_build_docs_once(site, app, docs_version, target, local, only_content_updated=True) _build_docs_once(site, app, docs_version, target, local, only_content_updated=True)


apps_path = frappe.get_app_path(app, "..", "..")
apps_path = frappe.get_app_path(app)
start_watch(apps_path, handler=trigger_make) start_watch(apps_path, handler=trigger_make)


def _build_docs_once(site, app, docs_version, target, local, only_content_updated=False): def _build_docs_once(site, app, docs_version, target, local, only_content_updated=False):
@@ -62,17 +43,16 @@ def _build_docs_once(site, app, docs_version, target, local, only_content_update


frappe.init(site=site) frappe.init(site=site)
frappe.connect() frappe.connect()
make = setup_docs(app)
make = setup_docs(app, target)


if not only_content_updated: if not only_content_updated:
make.build(docs_version) make.build(docs_version)


make.make_docs(target, local)
#make.make_docs(target, local)


finally: finally:
frappe.destroy() frappe.destroy()


commands = [ commands = [
build_docs, build_docs,
write_docs,
] ]

+ 4
- 6
frappe/config/docs.py Просмотреть файл

@@ -2,12 +2,10 @@


from __future__ import unicode_literals from __future__ import unicode_literals


docs_version = "7.x.x"

source_link = "https://github.com/frappe/frappe" source_link = "https://github.com/frappe/frappe"
docs_base_url = "https://frappe.github.io/frappe"
headline = "Superhero Web Framework"
sub_heading = "Build extensions to ERPNext or make your own app"
docs_base_url = "/docs"
headline = "Frappé Framework"
sub_heading = "Tutorials, API documentation and Model Reference"
hide_install = True hide_install = True
long_description = """Frappe is a full stack web application framework written in Python, long_description = """Frappe is a full stack web application framework written in Python,
Javascript, HTML/CSS with MySQL as the backend. It was built for ERPNext Javascript, HTML/CSS with MySQL as the backend. It was built for ERPNext
@@ -25,7 +23,7 @@ to ERPNext.
Frappe Framework was designed to build [ERPNext](https://erpnext.com), open source Frappe Framework was designed to build [ERPNext](https://erpnext.com), open source
ERP for managing small and medium sized businesses. ERP for managing small and medium sized businesses.


[Get started with the Tutorial](https://frappe.github.io/frappe/user/)
[Get started with the Tutorial](/docs/user/)
""" """
google_analytics_id = 'UA-8911157-23' google_analytics_id = 'UA-8911157-23'




+ 1
- 2
frappe/core/doctype/communication/feed.py Просмотреть файл

@@ -3,7 +3,6 @@


from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import frappe.defaults
import frappe.permissions import frappe.permissions
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import get_fullname from frappe.utils import get_fullname
@@ -68,7 +67,7 @@ def get_feed_match_conditions(user=None, force=True):


conditions = ['`tabCommunication`.owner="{user}" or `tabCommunication`.reference_owner="{user}"'.format(user=frappe.db.escape(user))] conditions = ['`tabCommunication`.owner="{user}" or `tabCommunication`.reference_owner="{user}"'.format(user=frappe.db.escape(user))]


user_permissions = frappe.defaults.get_user_permissions(user)
user_permissions = frappe.permissions.get_user_permissions(user)
can_read = frappe.get_user().get_can_read() can_read = frappe.get_user().get_can_read()


can_read_doctypes = ['"{}"'.format(doctype) for doctype in can_read_doctypes = ['"{}"'.format(doctype) for doctype in


+ 67
- 39
frappe/core/doctype/system_settings/system_settings.json Просмотреть файл

@@ -527,7 +527,7 @@
"bold": 0, "bold": 0,
"collapsible": 1, "collapsible": 1,
"columns": 0, "columns": 0,
"fieldname": "security",
"fieldname": "permissions",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
@@ -536,10 +536,11 @@
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Security",
"label": "Permissions",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
"precision": "",
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
@@ -556,10 +557,9 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"default": "06:00",
"description": "Session Expiry in Hours e.g. 06:00",
"fieldname": "session_expiry",
"fieldtype": "Data",
"description": "If Apply User Permissions is checked for Report DocType but no User Permissions are defined for Report for a User, then all Reports are shown to that User",
"fieldname": "ignore_user_permissions_if_missing",
"fieldtype": "Check",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
@@ -567,11 +567,11 @@
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Session Expiry",
"label": "Ignore User Permissions If Missing",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"options": "",
"permlevel": 0, "permlevel": 0,
"precision": "",
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
@@ -588,10 +588,10 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"default": "720:00",
"description": "In Hours",
"fieldname": "session_expiry_mobile",
"fieldtype": "Data",
"default": "0",
"description": "If Apply Strict User Permission is checked and User Permission is defined for a DocType for a User, then all the documents where value of the link is blank, will not be shown to that User",
"fieldname": "apply_strict_user_permissions",
"fieldtype": "Check",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
@@ -599,7 +599,7 @@
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Session Expiry Mobile",
"label": "Apply Strict User Permissions",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
@@ -614,16 +614,45 @@
"set_only_once": 0, "set_only_once": 0,
"unique": 0 "unique": 0
}, },
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"fieldname": "security",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Security",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"default": "0",
"description": "If enabled, the password strength will be enforced based on the Minimum Password Score value. A value of 2 being medium strong and 4 being very strong.",
"fieldname": "enable_password_policy",
"fieldtype": "Check",
"default": "06:00",
"description": "Session Expiry in Hours e.g. 06:00",
"fieldname": "session_expiry",
"fieldtype": "Data",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
@@ -631,11 +660,11 @@
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Enable Password Policy",
"label": "Session Expiry",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"options": "",
"permlevel": 0, "permlevel": 0,
"precision": "",
"print_hide": 0, "print_hide": 0,
"print_hide_if_no_value": 0, "print_hide_if_no_value": 0,
"read_only": 0, "read_only": 0,
@@ -652,10 +681,10 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"default": "2",
"depends_on": "eval:doc.enable_password_policy==1",
"fieldname": "minimum_password_score",
"fieldtype": "Select",
"default": "720:00",
"description": "In Hours",
"fieldname": "session_expiry_mobile",
"fieldtype": "Data",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
@@ -663,10 +692,9 @@
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Minimum Password Score",
"label": "Session Expiry Mobile",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"options": "2\n4",
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
@@ -823,6 +851,7 @@
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Enable Password Policy",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
@@ -843,9 +872,10 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"description": "Note: Multiple sessions will be allowed in case of mobile device",
"fieldname": "deny_multiple_sessions",
"fieldtype": "Check",
"default": "2",
"depends_on": "eval:doc.enable_password_policy==1",
"fieldname": "minimum_password_score",
"fieldtype": "Select",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
@@ -853,9 +883,10 @@
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Allow only one session per user",
"label": "Minimum Password Score",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"options": "2\n4",
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
@@ -874,9 +905,8 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"description": "If Apply User Permissions is checked for Report DocType but no User Permissions are defined for Report for a User, then all Reports are shown to that User",
"fieldname": "ignore_user_permissions_if_missing",
"fieldtype": "Check",
"fieldname": "column_break_13",
"fieldtype": "Column Break",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
@@ -884,7 +914,6 @@
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Ignore User Permissions If Missing",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
@@ -905,9 +934,8 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"default": "0",
"description": "If Apply Strict User Permission is checked and User Permission is defined for a DocType for a User, then all the documents where value of the link is blank, will not be shown to that User",
"fieldname": "apply_strict_user_permissions",
"description": "Note: Multiple sessions will be allowed in case of mobile device",
"fieldname": "deny_multiple_sessions",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
@@ -916,7 +944,7 @@
"in_global_search": 0, "in_global_search": 0,
"in_list_view": 0, "in_list_view": 0,
"in_standard_filter": 0, "in_standard_filter": 0,
"label": "Apply Strict User Permissions",
"label": "Allow only one session per user",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"permlevel": 0, "permlevel": 0,
@@ -1157,8 +1185,8 @@
"issingle": 1, "issingle": 1,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2017-07-29 13:33:49.201189",
"modified_by": "chude.osiegbu@manqala.com",
"modified": "2017-07-20 22:57:56.466867",
"modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "System Settings", "name": "System Settings",
"name_case": "", "name_case": "",


+ 1
- 0
frappe/core/doctype/system_settings/system_settings.py Просмотреть файл

@@ -35,6 +35,7 @@ class SystemSettings(Document):


frappe.cache().delete_value('system_settings') frappe.cache().delete_value('system_settings')
frappe.cache().delete_value('time_zone') frappe.cache().delete_value('time_zone')
frappe.local.system_settings = {}


@frappe.whitelist() @frappe.whitelist()
def load(): def load():


+ 2
- 1
frappe/core/doctype/test_runner/test_runner.js Просмотреть файл

@@ -11,7 +11,8 @@ frappe.ui.form.on('Test Runner', {


// all tests // all tests
frappe.call({ frappe.call({
method: 'frappe.core.doctype.test_runner.test_runner.get_test_js'
method: 'frappe.core.doctype.test_runner.test_runner.get_test_js',
args: { test_path: frm.doc.module_path }
}).always((data) => { }).always((data) => {
$("<div id='qunit'></div>").appendTo(wrapper.empty()); $("<div id='qunit'></div>").appendTo(wrapper.empty());
frm.events.run_tests(frm, data.message); frm.events.run_tests(frm, data.message);


+ 3
- 2
frappe/core/doctype/test_runner/test_runner.py Просмотреть файл

@@ -10,9 +10,10 @@ class TestRunner(Document):
pass pass


@frappe.whitelist() @frappe.whitelist()
def get_test_js():
def get_test_js(test_path=None):
'''Get test + data for app, example: app/tests/ui/test_name.js''' '''Get test + data for app, example: app/tests/ui/test_name.js'''
test_path = frappe.db.get_single_value('Test Runner', 'module_path')
if not test_path:
test_path = frappe.db.get_single_value('Test Runner', 'module_path')
test_js = [] test_js = []


# split # split


+ 5
- 1
frappe/core/doctype/user/user.js Просмотреть файл

@@ -59,9 +59,13 @@ frappe.ui.form.on('User', {
frappe.route_options = { frappe.route_options = {
"user": doc.name "user": doc.name
}; };
frappe.set_route("user-permissions");
frappe.set_route('List', 'User Permission');
}, null, "btn-default") }, null, "btn-default")


frm.add_custom_button(__('View Permitted Documents'),
() => frappe.set_route('query-report', 'Permitted Documents For User',
{user: frm.doc.name}));

frm.toggle_display(['sb1', 'sb3', 'modules_access'], true); frm.toggle_display(['sb1', 'sb3', 'modules_access'], true);
} }




+ 1
- 1
frappe/core/doctype/user/user.py Просмотреть файл

@@ -871,7 +871,7 @@ def notify_admin_access_to_system_manager(login_manager=None):
subject=_("Administrator Logged In"), subject=_("Administrator Logged In"),
template="administrator_logged_in", template="administrator_logged_in",
args={'access_message': access_message}, args={'access_message': access_message},
header=[subject, 'orange']
header=['Access Notification', 'orange']
) )


def extract_mentions(txt): def extract_mentions(txt):


+ 0
- 0
frappe/core/doctype/user_permission/__init__.py Просмотреть файл


+ 23
- 0
frappe/core/doctype/user_permission/test_user_permission.js Просмотреть файл

@@ -0,0 +1,23 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line

QUnit.test("test: User Permission", function (assert) {
let done = assert.async();

// number of asserts
assert.expect(1);

frappe.run_serially('User Permission', [
// insert a new User Permission
() => frappe.tests.make([
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);

});

+ 10
- 0
frappe/core/doctype/user_permission/test_user_permission.py Просмотреть файл

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals

#import frappe
import unittest

class TestUserPermission(unittest.TestCase):
pass

+ 10
- 0
frappe/core/doctype/user_permission/user_permission.js Просмотреть файл

@@ -0,0 +1,10 @@
// Copyright (c) 2017, Frappe Technologies and contributors
// For license information, please see license.txt

frappe.ui.form.on('User Permission', {
refresh: function(frm) {
frm.add_custom_button(__('View Permitted Documents'),
() => frappe.set_route('query-report', 'Permitted Documents For User',
{user: frm.doc.user}));
}
});

+ 188
- 0
frappe/core/doctype/user_permission/user_permission.json Просмотреть файл

@@ -0,0 +1,188 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2017-07-17 14:25:27.881871",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "user",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "User",
"length": 0,
"no_copy": 0,
"options": "User",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "allow",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Allow",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "for_value",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "For Value",
"length": 0,
"no_copy": 0,
"options": "allow",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 1,
"collapsible": 0,
"columns": 0,
"default": "1",
"description": "If you un-check this, you will have to apply manually for each Role + Document Type combination",
"fieldname": "apply_for_all_roles",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Apply for all Roles for this User",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-07-27 22:55:58.647315",
"modified_by": "Administrator",
"module": "Core",
"name": "User Permission",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "user",
"track_changes": 1,
"track_seen": 0
}

+ 80
- 0
frappe/core/doctype/user_permission/user_permission.py Просмотреть файл

@@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017, Frappe Technologies and contributors
# For license information, please see license.txt

from __future__ import unicode_literals
import frappe, json
from frappe.model.document import Document
from frappe.permissions import (get_valid_perms, update_permission_property)
from frappe import _

class UserPermission(Document):
def on_update(self):
frappe.cache().delete_value('user_permissions')

if self.apply_for_all_roles:
self.apply_user_permissions_to_all_roles()

def apply_user_permissions_to_all_roles(self):
# add apply user permissions for all roles that
# for this doctype
def show_progress(i, l):
if l > 2:
frappe.publish_realtime("progress",
dict(progress=[i, l], title=_('Updating...')),
user=frappe.session.user)


roles = frappe.get_roles(self.user)
linked = frappe.db.sql('''select distinct parent from tabDocField
where fieldtype="Link" and options=%s''', self.allow)
for i, link in enumerate(linked):
doctype = link[0]
for perm in get_valid_perms(doctype, self.user):
# if the role is applicable to the user
show_progress(i+1, len(linked))
if perm.role in roles:
if not perm.apply_user_permissions:
update_permission_property(doctype, perm.role, 0,
'apply_user_permissions', '1')

try:
user_permission_doctypes = json.loads(perm.user_permission_doctypes or '[]')
except ValueError:
user_permission_doctypes = []

if self.allow not in user_permission_doctypes:
user_permission_doctypes.append(self.allow)
update_permission_property(doctype, perm.role, 0,
'user_permission_doctypes', json.dumps(user_permission_doctypes), validate=False)

show_progress(len(linked), len(linked))

def on_trash(self): # pylint: disable=no-self-use
frappe.cache().delete_value('user_permissions')

def get_user_permissions(user=None):
'''Get all users permissions for the user as a dict of doctype'''
if not user:
user = frappe.session.user

out = frappe.cache().hget("user_permissions", user)

if not out:
out = {}
try:
for perm in frappe.get_all('User Permission',
fields=['allow', 'for_value'], filters=dict(user=user)):
out.setdefault(perm.allow, []).append(perm.for_value)

# add profile match
if user not in out.get("User", []):
out.setdefault("User", []).append(user)

frappe.cache().hset("user_permissions", user, out)
except frappe.SQLError, e:
if e.args[0]==1146:
# called from patch
pass

return out

+ 16
- 8
frappe/core/page/permission_manager/permission_manager.js Просмотреть файл

@@ -21,6 +21,7 @@ frappe.pages['permission-manager'].refresh = function(wrapper) {
frappe.PermissionEngine = Class.extend({ frappe.PermissionEngine = Class.extend({
init: function(wrapper) { init: function(wrapper) {
this.wrapper = wrapper; this.wrapper = wrapper;
this.page = wrapper.page;
this.body = $(this.wrapper).find(".perm-engine"); this.body = $(this.wrapper).find(".perm-engine");
this.make(); this.make();
this.refresh(); this.refresh();
@@ -55,6 +56,10 @@ frappe.PermissionEngine = Class.extend({
.change(function() { .change(function() {
me.refresh(); me.refresh();
}); });

this.page.add_inner_button(__('Set User Permissions'), () => {
return frappe.set_route('List', 'User Permission');
});
this.set_from_route(); this.set_from_route();
}, },
set_from_route: function() { set_from_route: function() {
@@ -133,11 +138,11 @@ frappe.PermissionEngine = Class.extend({
refresh: function() { refresh: function() {
var me = this; var me = this;
if(!me.doctype_select) { if(!me.doctype_select) {
this.body.html("<p class='text-muted'>" + __("Loading") + "...</div>");
this.body.html("<p class='text-muted'>" + __("Loading") + "...</p>");
return; return;
} }
if(!me.get_doctype() && !me.get_role()) { if(!me.get_doctype() && !me.get_role()) {
this.body.html("<p class='text-muted'>"+__("Select Document Type or Role to start.")+"</div>");
this.body.html("<p class='text-muted'>"+__("Select Document Type or Role to start.")+"</p>");
return; return;
} }
// get permissions // get permissions
@@ -247,10 +252,13 @@ frappe.PermissionEngine = Class.extend({


setup_user_permissions: function(d, role_cell) { setup_user_permissions: function(d, role_cell) {
var me = this; var me = this;
d.help = frappe.render('<ul class="user-permission-help small hidden" style="margin-left: -10px;">\
<li style="margin-top: 7px;"><a class="show-user-permission-doctypes grey">{%= __("Select Document Types") %}</a></li>\
<li style="margin-top: 3px;"><a class="show-user-permissions grey">{%= __("Show User Permissions") %}</a></li>\
</ul>', {});
d.help = `<ul class="user-permission-help small hidden"
style="margin-left: -10px;">
<li style="margin-top: 7px;"><a class="show-user-permission-doctypes">
${__("Select Document Types")}</a></li>
<li style="margin-top: 3px;"><a class="show-user-permissions">
${__("Show User Permissions")}</a></li>
</ul>`;


var checkbox = this.add_check(role_cell, d, "apply_user_permissions") var checkbox = this.add_check(role_cell, d, "apply_user_permissions")
.removeClass("col-md-4") .removeClass("col-md-4")
@@ -336,8 +344,8 @@ frappe.PermissionEngine = Class.extend({
var me = this; var me = this;


this.body.on("click", ".show-user-permissions", function() { this.body.on("click", ".show-user-permissions", function() {
frappe.route_options = { doctype: me.get_doctype() || "" };
frappe.set_route("user-permissions");
frappe.route_options = { allow: me.get_doctype() || "" };
frappe.set_route('List', 'User Permission');
}); });


this.body.on("click", "input[type='checkbox']", function() { this.body.on("click", "input[type='checkbox']", function() {


+ 3
- 13
frappe/core/page/permission_manager/permission_manager.py Просмотреть файл

@@ -7,7 +7,7 @@ import frappe.defaults
from frappe.modules.import_file import get_file_path, read_doc_from_file from frappe.modules.import_file import get_file_path, read_doc_from_file
from frappe.translate import send_translations from frappe.translate import send_translations
from frappe.permissions import (reset_perms, get_linked_doctypes, get_all_perms, from frappe.permissions import (reset_perms, get_linked_doctypes, get_all_perms,
setup_custom_perms, add_permission)
setup_custom_perms, add_permission, update_permission_property)
from frappe.core.doctype.doctype.doctype import (clear_permissions_cache, from frappe.core.doctype.doctype.doctype import (clear_permissions_cache,
validate_permissions_for_doctype) validate_permissions_for_doctype)
from frappe import _ from frappe import _
@@ -68,18 +68,8 @@ def add(parent, role, permlevel):
@frappe.whitelist() @frappe.whitelist()
def update(doctype, role, permlevel, ptype, value=None): def update(doctype, role, permlevel, ptype, value=None):
frappe.only_for("System Manager") frappe.only_for("System Manager")

out = None
if setup_custom_perms(doctype):
out = 'refresh'

name = frappe.get_value('Custom DocPerm', dict(parent=doctype, role=role, permlevel=permlevel))

frappe.db.sql("""update `tabCustom DocPerm` set `%s`=%s where name=%s"""\
% (frappe.db.escape(ptype), '%s', '%s'), (value, name))
validate_permissions_for_doctype(doctype)

return out
out = update_permission_property(doctype, role, permlevel, ptype, value)
return 'refresh' if out else None


@frappe.whitelist() @frappe.whitelist()
def remove(doctype, role, permlevel): def remove(doctype, role, permlevel):


+ 0
- 1
frappe/core/page/user_permissions/README.md Просмотреть файл

@@ -1 +0,0 @@
Interface to set user defaults (DefaultValue).

+ 0
- 3
frappe/core/page/user_permissions/__init__.py Просмотреть файл

@@ -1,3 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt


+ 0
- 365
frappe/core/page/user_permissions/user_permissions.js Просмотреть файл

@@ -1,365 +0,0 @@
frappe.pages['user-permissions'].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: __("User Permissions Manager"),
icon: "fa fa-shield",
single_column: true
});

frappe.breadcrumbs.add("Setup");

$("<div class='user-settings' \
style='min-height: 200px; padding: 15px;'></div>\
<p style='padding: 15px; padding-bottom: 0px;'>\
<a class='view-role-permissions grey'>" + __("Edit Role Permissions") + "</a>\
</p><hr><div style='padding: 0px 15px;'>\
<h4>"+__("Help for User Permissions")+":</h4>\
<ol>\
<li>"
+ __("Apart from Role based Permission Rules, you can apply User Permissions based on DocTypes.")
+ "</li>"

+ "<li>"
+ __("These permissions will apply for all transactions where the permitted record is linked. For example, if Company C is added to User Permissions of user X, user X will only be able to see transactions that has company C as a linked value.")
+ "</li>"

+ "<li>"
+ __("These will also be set as default values for those links, if only one such permission record is defined.")
+ "</li>"

+ "<li>"
+ __("A user can be permitted to multiple records of the same DocType.")
+ "</li>\
</ol></div>").appendTo(page.main);
wrapper.user_permissions = new frappe.UserPermissions(wrapper);
}

frappe.pages['user-permissions'].refresh = function(wrapper) {
wrapper.user_permissions.set_from_route();
}

frappe.UserPermissions = Class.extend({
init: function(wrapper) {
this.wrapper = wrapper;
this.body = $(this.wrapper).find(".user-settings");
this.filters = {};
this.make();
this.refresh();
},
make: function() {
var me = this;

$(this.wrapper).find(".view-role-permissions").on("click", function() {
frappe.route_options = { doctype: me.get_doctype() || "" };
frappe.set_route("permission-manager");
})

return frappe.call({
module:"frappe.core",
page:"user_permissions",
method: "get_users_and_links",
callback: function(r) {
me.options = r.message;

me.filters.user = me.wrapper.page.add_field({
fieldname: "user",
label: __("User"),
fieldtype: "Select",
options: ([__("Select User") + "..."].concat(r.message.users)).join("\n")
});

me.filters.doctype = me.wrapper.page.add_field({
fieldname: "doctype",
label: __("DocType"),
fieldtype: "Select",
options: ([__("Select DocType") + "..."].concat(me.get_link_names())).join("\n")
});

me.filters.user_permission = me.wrapper.page.add_field({
fieldname: "user_permission",
label: __("Name"),
fieldtype: "Link",
options: "[Select]"
});

if(frappe.user_roles.includes("System Manager")) {
me.download = me.wrapper.page.add_field({
fieldname: "download",
label: __("Download"),
fieldtype: "Button",
icon: "fa fa-download"
});

me.upload = me.wrapper.page.add_field({
fieldname: "upload",
label: __("Upload"),
fieldtype: "Button",
icon: "fa fa-upload"
});
}

// bind change event
$.each(me.filters, function(k, f) {
f.$input.on("change", function() {
me.refresh();
});
});

// change options in user_permission link
me.filters.doctype.$input.on("change", function() {
me.filters.user_permission.df.options = me.get_doctype();
});

me.set_from_route();
me.setup_download_upload();
}
});
},
setup_download_upload: function() {
var me = this;
me.download.$input.on("click", function() {
window.location.href = frappe.urllib.get_base_url()
+ "/api/method/frappe.core.page.user_permissions.user_permissions.get_user_permissions_csv";
});

me.upload.$input.on("click", function() {
var d = new frappe.ui.Dialog({
title: __("Upload User Permissions"),
fields: [
{
fieldtype:"HTML",
options: '<p class="text-muted"><ol>'+
"<li>"+__("Upload CSV file containing all user permissions in the same format as Download.")+"</li>"+
"<li><strong>"+__("Any existing permission will be deleted / overwritten.")+"</strong></li>"+
'</p>'
},
{
fieldtype:"Attach", fieldname:"attach",
}
],
primary_action_label: __("Upload and Sync"),
primary_action: function() {
var filedata = d.fields_dict.attach.get_value();
if(!filedata) {
frappe.msgprint(__("Please attach a file"));
return;
}
frappe.call({
method:"frappe.core.page.user_permissions.user_permissions.import_user_permissions",
args: {
filedata: filedata
},
callback: function(r) {
if(!r.exc) {
frappe.msgprint(__("Permissions Updated"));
d.hide();
}
}
});
}
});
d.show();
})
},
get_link_names: function() {
return this.options.link_fields;
},
set_from_route: function() {
var me = this;
if(frappe.route_options && this.filters && !$.isEmptyObject(this.filters)) {
$.each(frappe.route_options, function(key, value) {
if(me.filters[key] && frappe.route_options[key]!=null)
me.set_filter(key, value);
});
frappe.route_options = null;
}
this.refresh();
},
set_filter: function(key, value) {
this.filters[key].$input.val(value);
},
get_user: function() {
var user = this.filters.user.$input.val();
return user== __("Select User") + "..." ? null : user;
},
get_doctype: function() {
var doctype = this.filters.doctype.$input.val();
return doctype== __("Select DocType") + "..." ? null : doctype;
},
get_user_permission: function() {
// autosuggest hack!
var user_permission = this.filters.user_permission.$input.val();
return (user_permission === "%") ? null : user_permission;
},
render: function(prop_list) {
var me = this;
this.body.empty();
this.prop_list = prop_list;
if(!prop_list || !prop_list.length) {
this.add_message(__("No User Restrictions found."));
} else {
this.show_user_permissions_table();
}
this.show_add_user_permission();
if(this.get_user() && this.get_doctype()) {
$('<button class="btn btn-default btn-sm" style="margin-left: 10px;">\
Show Allowed Documents</button>').appendTo(this.body.find(".btn-area")).on("click", function() {
frappe.route_options = {doctype: me.get_doctype(), user:me.get_user() };
frappe.set_route("query-report/Permitted Documents For User");
});
}
},
add_message: function(txt) {
$('<p class="text-muted">' + txt + '</p>').appendTo(this.body);
},
refresh: function() {
var me = this;
if(!me.filters.user) {
this.body.html("<p class='text-muted'>"+__("Loading")+"...</p>");
return;
}
if(!me.get_user() && !me.get_doctype()) {
this.body.html("<p class='text-muted'>"+__("Select User or DocType to start.")+"</p>");
return;
}
// get permissions
return frappe.call({
module: "frappe.core",
page: "user_permissions",
method: "get_permissions",
args: {
parent: me.get_user(),
defkey: me.get_doctype(),
defvalue: me.get_user_permission()
},
callback: function(r) {
me.render(r.message);
}
});
},
show_user_permissions_table: function() {
var me = this;
this.table = $("<table class='table table-bordered'>\
<thead><tr></tr></thead>\
<tbody></tbody>\
</table>").appendTo(this.body);

$('<p class="text-muted small">'
+__("These restrictions will apply for Document Types where 'Apply User Permissions' is checked for the permission rule and a field with this value is present.")
+'</p>').appendTo(this.body);

$.each([[__("Allow User"), 150], [__("If Document Type"), 150], [__("Is"),150], ["", 50]],
function(i, col) {
$("<th>")
.html(col[0])
.css("width", col[1]+"px")
.appendTo(me.table.find("thead tr"));
});


$.each(this.prop_list, function(i, d) {
var row = $("<tr>").appendTo(me.table.find("tbody"));

$("<td>").html('<a class="grey" href="#Form/User/'+encodeURIComponent(d.parent)+'">'
+d.parent+'</a>').appendTo(row);
$("<td>").html(d.defkey).appendTo(row);
$("<td>").html(d.defvalue).appendTo(row);

me.add_delete_button(row, d);
});

},
add_delete_button: function(row, d) {
var me = this;
$("<button class='btn btn-sm btn-default'><i class='fa fa-remove'></i></button>")
.appendTo($("<td>").appendTo(row))
.attr("data-name", d.name)
.attr("data-user", d.parent)
.attr("data-defkey", d.defkey)
.attr("data-defvalue", d.defvalue)
.click(function() {
return frappe.call({
module: "frappe.core",
page: "user_permissions",
method: "remove",
args: {
name: $(this).attr("data-name"),
user: $(this).attr("data-user"),
defkey: $(this).attr("data-defkey"),
defvalue: $(this).attr("data-defvalue")
},
callback: function(r) {
if(r.exc) {
frappe.msgprint(__("Did not remove"));
} else {
me.refresh();
}
}
})
});
},

show_add_user_permission: function() {
var me = this;
$("<button class='btn btn-default btn-sm'>"+__("Add A User Restriction")+"</button>")
.appendTo($('<p class="btn-area">').appendTo(this.body))
.click(function() {
var d = new frappe.ui.Dialog({
title: __("Add A New Restriction"),
fields: [
{fieldtype:"Select", label:__("Allow User"),
options:me.options.users, reqd:1, fieldname:"user"},
{fieldtype:"Select", label: __("If Document Type"), fieldname:"defkey",
options:me.get_link_names(), reqd:1},
{fieldtype:"Link", label:__("Is"), fieldname:"defvalue",
options:'[Select]', reqd:1},
{fieldtype:"Button", label: __("Add"), fieldname:"add"},
]
});
if(me.get_user()) {
d.set_value("user", me.get_user());
d.get_input("user").prop("disabled", true);
}
if(me.get_doctype()) {
d.set_value("defkey", me.get_doctype());
d.get_input("defkey").prop("disabled", true);
}
if(me.get_user_permission()) {
d.set_value("defvalue", me.get_user_permission());
d.get_input("defvalue").prop("disabled", true);
}

d.fields_dict["defvalue"].get_query = function(txt) {
if(!d.get_value("defkey")) {
frappe.throw(__("Please select Document Type"));
}

return {
doctype: d.get_value("defkey")
}
};

d.get_input("add").click(function() {
var args = d.get_values();
if(!args) {
return;
}
frappe.call({
module: "frappe.core",
page: "user_permissions",
method: "add",
args: args,
callback: function(r) {
if(r.exc) {
frappe.msgprint(__("Did not add"));
} else {
me.refresh();
}
}
})
d.hide();
});
d.show();
});
}
})

+ 0
- 19
frappe/core/page/user_permissions/user_permissions.json Просмотреть файл

@@ -1,19 +0,0 @@
{
"content": null,
"creation": "2013-01-01 18:50:55",
"docstatus": 0,
"doctype": "Page",
"icon": "fa fa-user",
"idx": 1,
"modified": "2014-05-28 16:53:43.103533",
"modified_by": "Administrator",
"module": "Core",
"name": "user-permissions",
"owner": "Administrator",
"page_name": "user-permissions",
"roles": [],
"script": null,
"standard": "Yes",
"style": null,
"title": "User Permissions Manager"
}

+ 0
- 109
frappe/core/page/user_permissions/user_permissions.py Просмотреть файл

@@ -1,109 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt

from __future__ import unicode_literals
import frappe
from frappe import _
import frappe.defaults
from frappe.permissions import (can_set_user_permissions, add_user_permission,
remove_user_permission, get_valid_perms)
from frappe.core.doctype.user.user import get_system_users
from frappe.utils.csvutils import UnicodeWriter, read_csv_content_from_uploaded_file
from frappe.defaults import clear_default

@frappe.whitelist()
def get_users_and_links():
return {
"users": get_system_users(),
"link_fields": get_doctypes_for_user_permissions()
}

@frappe.whitelist()
def get_permissions(parent=None, defkey=None, defvalue=None):
if defkey and not can_set_user_permissions(defkey, defvalue):
raise frappe.PermissionError

conditions, values = _build_conditions(locals())

permissions = frappe.db.sql("""select name, parent, defkey, defvalue
from tabDefaultValue
where parent not in ('__default', '__global')
and substr(defkey,1,1)!='_'
and parenttype='User Permission'
{conditions}
order by parent, defkey""".format(conditions=conditions), values, as_dict=True)

if not defkey:
out = []
doctypes = get_doctypes_for_user_permissions()
for p in permissions:
if p.defkey in doctypes:
out.append(p)
permissions = out

return permissions

def _build_conditions(filters):
conditions = []
values = {}
for key, value in filters.items():
if filters.get(key):
conditions.append("and `{key}`=%({key})s".format(key=key))
values[key] = value

return "\n".join(conditions), values

@frappe.whitelist()
def remove(user, name, defkey, defvalue):
if not can_set_user_permissions(defkey, defvalue):
frappe.throw(_("Cannot remove permission for DocType: {0} and Name: {1}").format(
defkey, defvalue), frappe.PermissionError)

remove_user_permission(defkey, defvalue, user, name)

@frappe.whitelist()
def add(user, defkey, defvalue):
if not can_set_user_permissions(defkey, defvalue):
frappe.throw(_("Cannot set permission for DocType: {0} and Name: {1}").format(
defkey, defvalue), frappe.PermissionError)

add_user_permission(defkey, defvalue, user, with_message=True)

def get_doctypes_for_user_permissions():
'''Get doctypes for the current user where user permissions are applicable'''
user_roles = frappe.get_roles()

if "System Manager" in user_roles:
doctypes = set([p.parent for p in get_valid_perms()])
else:
doctypes = set([p.parent for p in get_valid_perms() if p.set_user_permissions])
single_doctypes = set([d.name for d in frappe.get_all("DocType", {"issingle": 1})])
return sorted(doctypes.difference(single_doctypes))


@frappe.whitelist()
def get_user_permissions_csv():
out = [["User Permissions"], ["User", "Document Type", "Value"]]
out += [[a.parent, a.defkey, a.defvalue] for a in get_permissions()]

csv = UnicodeWriter()
for row in out:
csv.writerow(row)

frappe.response['result'] = str(csv.getvalue())
frappe.response['type'] = 'csv'
frappe.response['doctype'] = "User Permissions"

@frappe.whitelist()
def import_user_permissions():
frappe.only_for("System Manager")
rows = read_csv_content_from_uploaded_file(ignore_encoding=True)
clear_default(parenttype="User Permission")

if rows[0][0]!="User Permissions" and rows[1][0] != "User":
frappe.throw(frappe._("Please upload using the same template as download."))

for row in rows[2:]:
add_user_permission(row[1], row[2], row[0])

+ 4
- 19
frappe/defaults.py Просмотреть файл

@@ -48,25 +48,10 @@ def is_a_user_permission_key(key):
return ":" not in key and key != frappe.scrub(key) return ":" not in key and key != frappe.scrub(key)


def get_user_permissions(user=None): def get_user_permissions(user=None):
if not user:
user = frappe.session.user

return build_user_permissions(user)

def build_user_permissions(user):
out = frappe.cache().hget("user_permissions", user)
if out==None:
out = {}
for key, value in frappe.db.sql("""select defkey, ifnull(defvalue, '') as defvalue
from tabDefaultValue where parent=%s and parenttype='User Permission'""", (user,)):
out.setdefault(key, []).append(value)

# add profile match
if user not in out.get("User", []):
out.setdefault("User", []).append(user)

frappe.cache().hset("user_permissions", user, out)
return out
from frappe.core.doctype.user_permission.user_permission \
import get_user_permissions as _get_user_permissions
'''Return frappe.core.doctype.user_permissions.user_permissions._get_user_permissions (kept for backward compatibility)'''
return _get_user_permissions(user)


def get_defaults(user=None): def get_defaults(user=None):
globald = get_defaults_for() globald = get_defaults_for()


+ 0
- 11
frappe/desk/form/load.py Просмотреть файл

@@ -70,7 +70,6 @@ def getdoctype(doctype, with_parent=False, cached_timestamp=None):
if not docs: if not docs:
docs = get_meta_bundle(doctype) docs = get_meta_bundle(doctype)


frappe.response['user_permissions'] = get_user_permissions(docs)
frappe.response['user_settings'] = get_user_settings(parent_dt or doctype) frappe.response['user_settings'] = get_user_settings(parent_dt or doctype)


if cached_timestamp and docs[0].modified==cached_timestamp: if cached_timestamp and docs[0].modified==cached_timestamp:
@@ -102,16 +101,6 @@ def get_docinfo(doc=None, doctype=None, name=None):
"rating": get_feedback_rating(doc.doctype, doc.name) "rating": get_feedback_rating(doc.doctype, doc.name)
} }


def get_user_permissions(meta):
out = {}
all_user_permissions = frappe.defaults.get_user_permissions()

for m in meta:
for df in m.get_fields_to_check_permissions(all_user_permissions):
out[df.options] = list(set(all_user_permissions[df.options]))

return out

def get_attachments(dt, dn): def get_attachments(dt, dn):
return frappe.get_all("File", fields=["name", "file_name", "file_url", "is_private"], return frappe.get_all("File", fields=["name", "file_name", "file_url", "is_private"],
filters = {"attached_to_name": dn, "attached_to_doctype": dt}) filters = {"attached_to_name": dn, "attached_to_doctype": dt})


Двоичные данные
frappe/docs/assets/img/desk/animated_line_graph.gif Просмотреть файл

До После
Ширина: 944  |  Высота: 298  |  Размер: 300 KiB

Двоичные данные
frappe/docs/assets/img/desk/bar_graph.png Просмотреть файл

До После
Ширина: 776  |  Высота: 296  |  Размер: 29 KiB Ширина: 921  |  Высота: 294  |  Размер: 27 KiB

Двоичные данные
frappe/docs/assets/img/desk/line_graph.png Просмотреть файл

До После
Ширина: 776  |  Высота: 295  |  Размер: 39 KiB

Двоичные данные
frappe/docs/assets/img/desk/line_graph_sales.png Просмотреть файл

До После
Ширина: 1104  |  Высота: 334  |  Размер: 60 KiB

Двоичные данные
frappe/docs/assets/img/desk/percentage_graph.png Просмотреть файл

До После
Ширина: 1132  |  Высота: 151  |  Размер: 17 KiB

+ 0
- 10
frappe/docs/contents.html Просмотреть файл

@@ -1,10 +0,0 @@
<!-- title: Table of Contents -->

<h1>Table of Contents</h1>
<br>

{% include "templates/includes/full_index.html" %}

<!-- autodoc -->
<!-- jinja -->
<!-- no-breadcrumbs -->

+ 0
- 9
frappe/docs/contents.py Просмотреть файл

@@ -1,9 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt

from __future__ import unicode_literals
import frappe
from frappe.website.utils import get_full_index

def get_context(context):
context.full_index = get_full_index()

+ 0
- 57
frappe/docs/index.html Просмотреть файл

@@ -1,57 +0,0 @@
<!-- title: Frappe Framework: Documentation -->
<!-- description: Superhero Web Framework -->
<!-- no-breadcrumbs -->
<style>
</style>

<!-- start-hero -->
<div class="splash">
<div class="container">
<div class="col-sm-10 col-sm-offset-1">
<div class="jumbotron">
<h1>Superhero Web Framework</h1>
<p>Build extensions to ERPNext or make your own app</p>
</div>
<div class="section" style="padding-top: 0px; margin-top: -30px;">
<div class="fake-browser-frame">
<img class="img-responsive browser-image feature-image"
src="assets/img/home.png">
</div>
</div>
</div>
</div>
</div>
<!-- end-hero -->


<div class="container">
<div class="col-sm-10 col-sm-offset-1">
<div class="section">
<p>Frappe is a full stack web application framework written in Python,
Javascript, HTML/CSS with MySQL as the backend. It was built for ERPNext
but is pretty generic and can be used to build database driven apps.</p>

<p>The key differece in Frappe compared to other frameworks is that Frappe
is that meta-data is also treated as data and is used to build front-ends
very easily. Frappe comes with a full blown admin UI called the <strong>Desk</strong>
that handles forms, navigation, lists, menus, permissions, file attachment
and much more out of the box.</p>

<p>Frappe also has a plug-in architecture that can be used to build plugins
to ERPNext.</p>

<p>Frappe Framework was designed to build <a href="https://erpnext.com">ERPNext</a>, open source
ERP for managing small and medium sized businesses.</p>

<p><a href="https://frappe.github.io/frappe/user/">Get started with the Tutorial</a></p>

</div>
</div>
</div>




<!-- autodoc -->
<!-- jinja -->

+ 0
- 6
frappe/docs/index.txt Просмотреть файл

@@ -1,6 +0,0 @@
assets
user
contents
current
install
license

+ 0
- 30
frappe/docs/install.md Просмотреть файл

@@ -1,30 +0,0 @@
<!-- title: Frappe Framework Installation -->

# Installation

Frappe Framework is based on the <a href="https://frappe.io">Frappe Framework</a>, a full stack web framework based on Python, MariaDB, Redis, Node.

To intall Frappe Framework, you will have to install the <a href="https://github.com/frappe/bench">Frappe Bench</a>, the command-line, package manager and site manager for Frappe Framework. For more details, read the Bench README.

After you have installed Frappe Bench, go to you bench folder, which is `frappe.bench` by default and setup **frappe**.

bench get-app frappe {{ source_link }}

Then create a new site to install the app.

bench new-site mysite

This will create a new folder in your `/sites` directory and create a new database for this site.

Next, install frappe in this site

bench --site mysite install-app frappe

To run this locally, run

bench start

Fire up your browser and go to http://localhost:8000 and you should see the login screen. Login as **Administrator** and **admin** (or the password you set at the time of `new-site`) and you are set.

<!-- jinja -->
<!-- autodoc -->

+ 0
- 16
frappe/docs/license.html Просмотреть файл

@@ -1,16 +0,0 @@
<!-- title: License MIT -->

<h1>MIT</h1>

<p>The MIT License (MIT)</p>

<p>Copyright (c) 2016 Frappe Technologies Pvt. Ltd.</p>

<p>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:</p>

<p>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.</p>

<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>


<!-- autodoc -->

+ 12
- 56
frappe/docs/user/en/guides/app-development/generating-docs.md Просмотреть файл

@@ -1,18 +1,17 @@
# Generating Documentation Website for your App # Generating Documentation Website for your App


Frappe version 6.7 onwards includes a full-blown documentation generator so that you can easily create a website for your app that has both user docs and developers docs (auto-generated). These pages are generated as static HTML pages so that you can add them as GitHub pages.
Frappe version 6.7 onwards includes a full-blown documentation generator so that you can easily create a website for your app that has both user docs and developers docs (auto-generated).

Version 8.7 onwards, these will be generated in a target app.


## Writing Docs ## Writing Docs


### 1. Setting up docs ### 1. Setting up docs


#### 1.1. Setup `docs.py`

The first step is to setup the docs folder. For that you must create a new file in your app `config/docs.py` if it is not auto-generated. In your `docs.py` file, add the following module properties. The first step is to setup the docs folder. For that you must create a new file in your app `config/docs.py` if it is not auto-generated. In your `docs.py` file, add the following module properties.




source_link = "https://github.com/[orgname]/[reponame]" source_link = "https://github.com/[orgname]/[reponame]"
docs_base_url = "https://[orgname].github.io/[reponame]"
headline = "This is what my app does" headline = "This is what my app does"
sub_heading = "Slightly more details with key features" sub_heading = "Slightly more details with key features"
long_description = """(long description in markdown)""" long_description = """(long description in markdown)"""
@@ -29,16 +28,6 @@ The first step is to setup the docs folder. For that you must create a new file


pass pass


#### 1.2. Generate `/docs`

To generate the docs for the `current` version, go to the command line and write

bench --site [site] build-docs [appname]
If you want to maintain versions of your docs, then you can add a version number instead of `current`

This will create a `/docs` folder in your app.

### 2. Add User Documentation ### 2. Add User Documentation


To add user documentation, add folders and pages in your `/docs/user` folder in the same way you would build a website pages in the `www` folder. To add user documentation, add folders and pages in your `/docs/user` folder in the same way you would build a website pages in the `www` folder.
@@ -54,61 +43,28 @@ Some quick tips:
While linking make sure you add `{{ docs_base_url }}` to all your links. While linking make sure you add `{{ docs_base_url }}` to all your links.




{% raw %}<a href="{{ docs_base_url }}/user/link/to/page.html">Link Description</a>{% endraw %}
{% raw %}<a href="/docs/user/link/to/page.html">Link Description</a>{% endraw %}




### 4. Adding Images ### 4. Adding Images


You can add images in the `/docs/assets` folder. You can add links to the images as follows: You can add images in the `/docs/assets` folder. You can add links to the images as follows:


{% raw %}<img src="{{ docs_base_url }}/assets/img/my-img/gif" class="screenshot">{% endraw %}

---

## Setting up output docs

The output docs are generated in your `docs/appname` folder using the `write-docs` command.

---

## Viewing Locally

To test your docs locally, add a `--local` option to the `write-docs` command.

bench --site [site] write-docs [appname] --local

Then it will build urls so that you can view these files locally. To view them locally in your browser, you can use the Python SimpleHTTPServer

Run this from your `docs/myapp` folder:

python -m SimpleHTTPServer 8080
{% raw %}<img src="/docs/assets/img/my-img/gif" class="screenshot">{% endraw %}


--- ---


## Publishing to GitHub Pages


To publish your docs on GitHub pages, you will have to create an empty and orphan branch in your repository called `gh-pages` and push your documentation there.
## Building Docs


1. To easily publish your docs on gh-pages, commit and push your `apps/docs` folder on you master branch first.
2. The `/docs` generation will also generate a `/docs` folder in your bench, parallel to your `/sites` folder. e.g. `/frappe-bench/docs`
3. Generate you documentation using the `write-docs` command.
4. Go to your docs folder `cd docs/myapp`
5. Checkout the gh-pages branch `git checkout --orphan gh-pages`
6. Push your documentation to Github.
You must create a new app that will have the output of the docs, which is called the "target" app. For example, the docs for ERPNext are hosted at erpnext.org, which is based on the app "foundation". You can create a new app just to push docs of any other app.


Note > The branch name `gh-pages` is only if you are using GitHub. If you are hosting this on any other static file server, you can create any other orphan branch instead.
To output docs to another app,


Putting it all together:
bench --site [site] build-docs [app] --target [target_app]


# build the apps/docs folder and write the compiled docs at docs/appname
bench --site [site] build-docs [appname]
This will create a new folder `/docs` inside the `www` folder of the target app and generate automatic docs (from code), model references and copy user docs and assets.


# commit to the gh-pages branch (for GitHub Pages)
cd docs/appname
git checkout --orphan gh-pages
git remote add origin [remote git repository]
git add *
git commit -m "Documentation Initialization"
git push origin gh-pages
To view the docs, just go the the `/docs` url on your target app. Example:


To check your documentation online go to: https://[orgname].github.io/[reponame]
https://erpnext.org/docs

+ 94
- 55
frappe/docs/user/en/guides/desk/making_graphs.md Просмотреть файл

@@ -1,61 +1,100 @@
# Making Graphs # Making Graphs


The Frappe UI **Graph** object enables you to render simple line and bar graphs for a discreet set of data points. You can also set special checkpoint values and summary stats.
The Frappe UI **Graph** object enables you to render simple line, bar or percentage graphs for single or multiple discreet sets of data points. You can also set special checkpoint values and summary stats.


### Example: Line graph ### Example: Line graph
Here's is an example of a simple sales graph:

render_graph: function() {
$('.form-graph').empty();

var months = ['Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'];
var values = [2410, 3100, 1700, 1200, 2700, 1600, 2740, 1000, 850, 1500, 400, 2013];

var goal = 2500;
var current_val = 2013;

new frappe.ui.Graph({
parent: $('.form-graph'),
width: 700,
height: 140,
mode: 'line-graph',

title: 'Sales',
subtitle: 'Monthly',
y_values: values,
x_points: months,

specific_values: [
{
name: "Goal",
line_type: "dashed", // "dashed" or "solid"
value: goal
},
],
summary_values: [
{
name: "This month",
color: 'green', // Indicator colors: 'grey', 'blue', 'red',
// 'green', 'orange', 'purple', 'darkgrey',
// 'black', 'yellow', 'lightblue'
value: '₹ ' + current_val
},
{
name: "Goal",
color: 'blue',
value: '₹ ' + goal
},
{
name: "Completed",
color: 'green',
value: (current_val/goal*100).toFixed(1) + "%"
}
]
});
},

<img src="{{docs_base_url}}/assets/img/desk/line_graph.png" class="screenshot">

Setting the mode to 'bar-graph':
Here's an example of a simple sales graph:

// Data
let months = ['August, 2016', 'September, 2016', 'October, 2016', 'November, 2016',
'December, 2016', 'January, 2017', 'February, 2017', 'March, 2017', 'April, 2017',
'May, 2017', 'June, 2017', 'July, 2017'];

let values1 = [24100, 31000, 17000, 12000, 27000, 16000, 27400, 11000, 8500, 15000, 4000, 20130];
let values2 = [17890, 10400, 12350, 20400, 17050, 23000, 7100, 13800, 16000, 20400, 11000, 13000];
let goal = 25000;
let current_val = 20130;

let g = new frappe.ui.Graph({
parent: $('.form-graph').empty(),
height: 200, // optional
mode: 'line', // 'line', 'bar' or 'percentage'

title: 'Sales',
subtitle: 'Monthly',

y: [
{
title: 'Data 1',
values: values1,
formatted: values1.map(d => '$ ' + d),
color: 'green' // Indicator colors: 'grey', 'blue', 'red',
// 'green', 'light-green', 'orange', 'purple', 'darkgrey',
// 'black', 'yellow', 'lightblue'
},
{
title: 'Data 2',
values: values2,
formatted: values2.map(d => '$ ' + d),
color: 'light-green'
}
],

x: {
values: months.map(d => d.substring(0, 3)),
formatted: months
},

specific_values: [
{
name: 'Goal',
line_type: 'dashed', // 'dashed' or 'solid'
value: goal
},
],

summary: [
{
name: 'This month',
color: 'orange',
value: '$ ' + current_val
},
{
name: 'Goal',
color: 'blue',
value: '$ ' + goal
},
{
name: 'Completed',
color: 'green',
value: (current_val/goal*100).toFixed(1) + "%"
}
]
});

<img src="{{docs_base_url}}/assets/img/desk/line_graph_sales.png" class="screenshot">

`bar` mode yeilds:


<img src="{{docs_base_url}}/assets/img/desk/bar_graph.png" class="screenshot"> <img src="{{docs_base_url}}/assets/img/desk/bar_graph.png" class="screenshot">

You can set the `colors` property of `x` to an array of color values for `percentage` mode:

<img src="{{docs_base_url}}/assets/img/desk/percentage_graph.png" class="screenshot">

You can also change the values of an existing graph with a new set of `y` values:

setTimeout(() => {
g.change_values([
{
values: data[2],
formatted: data[2].map(d => d + 'L')
},
{
values: data[3],
formatted: data[3].map(d => d + 'L')
}
]);
}, 1000);

<img src="{{docs_base_url}}/assets/img/desk/animated_line_graph.gif" class="screenshot">

+ 1
- 1
frappe/docs/user/en/guides/reports-and-printing/how-to-make-query-report.md Просмотреть файл

@@ -39,7 +39,7 @@ You can define complex queries such as:


### 4. Advanced (adding filters) ### 4. Advanced (adding filters)


If you are making a standard report, you can add filters in your query report just like [script reports](https://frappe.github.io/frappe/user/en/guides/reports-and-printing/how-to-make-script-reports) by adding a `.js` file in your query report folder. To include filters in your query, use `%(filter_key)s` where your filter value will be shown.
If you are making a standard report, you can add filters in your query report just like [script reports](https://frappe.io/docs/user/en/guides/reports-and-printing/how-to-make-script-reports) by adding a `.js` file in your query report folder. To include filters in your query, use `%(filter_key)s` where your filter value will be shown.


For example For example




+ 2
- 2
frappe/docs/user/en/guides/reports-and-printing/how-to-make-script-reports.md Просмотреть файл

@@ -4,9 +4,9 @@ You can create tabulated reports using server side scripts by creating a new Rep


> Note: You will need Administrator Permissions for this. > Note: You will need Administrator Permissions for this.


Since these reports give you unrestricted access via Python scripts, they can only be created by Administrators. The script part of the report becomes a part of the repository of the application. If you have not created an app, [read this](https://frappe.github.io/frappe/user/en/guides/app-development/).
Since these reports give you unrestricted access via Python scripts, they can only be created by Administrators. The script part of the report becomes a part of the repository of the application. If you have not created an app, [read this](https://frappe.io/docs/user/en/guides/app-development/).


> Note: You must be in [Developer Mode](https://frappe.github.io/frappe/user/en/guides/app-development/how-enable-developer-mode-in-frappe) to do this
> Note: You must be in [Developer Mode](https://frappe.io/docs/user/en/guides/app-development/how-enable-developer-mode-in-frappe) to do this


### 1. Create a new Report ### 1. Create a new Report




+ 9
- 3
frappe/docs/user/en/tutorial/before.md Просмотреть файл

@@ -6,11 +6,12 @@


#### 1. Python #### 1. Python


Frappe uses Python (v2.7) for server-side programming. It is highly recommended to learn Python before you start building apps with Frappe.
Frappe uses Python (v2.7) for server-side programming. It is highly recommended to learn Python before you start building apps with Frappe.


To write quality server-side code, you must also include automated tests. To write quality server-side code, you must also include automated tests.


Resources: Resources:

1. [Codecademy Tutorial for Python](https://www.codecademy.com/learn/python) 1. [Codecademy Tutorial for Python](https://www.codecademy.com/learn/python)
1. [Official Python Tutorial](https://docs.python.org/2.7/tutorial/index.html) 1. [Official Python Tutorial](https://docs.python.org/2.7/tutorial/index.html)
1. [Basics of Test-driven development](http://code.tutsplus.com/tutorials/beginning-test-driven-development-in-python--net-30137) 1. [Basics of Test-driven development](http://code.tutsplus.com/tutorials/beginning-test-driven-development-in-python--net-30137)
@@ -19,11 +20,12 @@ Resources:


#### 2. MariaDB / MySQL #### 2. MariaDB / MySQL


To create database-driven apps with Frappe, you must understand the basics of database management, like how to install, login, create new databases, and basic SQL queries.
To create database-driven apps with Frappe, you must understand the basics of database management, like how to install, login, create new databases, and basic SQL queries.


Resources: Resources:

1. [Codecademy Tutorial for SQL](https://www.codecademy.com/learn/learn-sql) 1. [Codecademy Tutorial for SQL](https://www.codecademy.com/learn/learn-sql)
1. [A basic MySQL tutorial by DigitalOcean](https://www.digitalocean.com/community/tutorials/a-basic-mysql-tutorial)
1. [A basic MySQL tutorial by DigitalOcean](https://www.digitalocean.com/community/tutorials/a-basic-mysql-tutorial)
1. [Getting started with MariaDB](https://mariadb.com/kb/en/mariadb/documentation/getting-started/) 1. [Getting started with MariaDB](https://mariadb.com/kb/en/mariadb/documentation/getting-started/)


--- ---
@@ -33,6 +35,7 @@ Resources:
If you want to build user interfaces using Frappe, you will need to learn basic HTML / CSS and the Boostrap CSS Framework. If you want to build user interfaces using Frappe, you will need to learn basic HTML / CSS and the Boostrap CSS Framework.


Resources: Resources:

1. [Codecademy Tutorial for HTML/CSS](https://www.codecademy.com/learn/learn-html-css) 1. [Codecademy Tutorial for HTML/CSS](https://www.codecademy.com/learn/learn-html-css)
1. [Getting started with Bootstrap](https://getbootstrap.com/getting-started/) 1. [Getting started with Bootstrap](https://getbootstrap.com/getting-started/)


@@ -44,6 +47,7 @@ To customize forms and create rich user interfaces, you should learn JavaScript




Resources: Resources:

1. [Codecademy Tutorial for JavaScript](https://www.codecademy.com/learn/learn-javascript) 1. [Codecademy Tutorial for JavaScript](https://www.codecademy.com/learn/learn-javascript)
1. [Codecademy Tutorial for jQuery](https://www.codecademy.com/learn/jquery) 1. [Codecademy Tutorial for jQuery](https://www.codecademy.com/learn/jquery)
--- ---
@@ -53,6 +57,7 @@ Resources:
If you are customizing Print templates or Web pages, you need to learn the Jinja Templating language. It is an easy way to create dynamic web pages (HTML). If you are customizing Print templates or Web pages, you need to learn the Jinja Templating language. It is an easy way to create dynamic web pages (HTML).


Resources: Resources:

1. [Primer on Jinja Templating](https://realpython.com/blog/python/primer-on-jinja-templating/) 1. [Primer on Jinja Templating](https://realpython.com/blog/python/primer-on-jinja-templating/)
1. [Official Documentation](http://jinja.pocoo.org/) 1. [Official Documentation](http://jinja.pocoo.org/)


@@ -63,6 +68,7 @@ Resources:
Learn how to contribute back to an open source project using Git and GitHub, two great tools to help you manage your code and share it with others. Learn how to contribute back to an open source project using Git and GitHub, two great tools to help you manage your code and share it with others.


Resources: Resources:

1. [Basic Git Tutorial](https://try.github.io) 1. [Basic Git Tutorial](https://try.github.io)
2. [How to contribute to Open Source](https://opensource.guide/how-to-contribute/) 2. [How to contribute to Open Source](https://opensource.guide/how-to-contribute/)




+ 3
- 2
frappe/model/create_new.py Просмотреть файл

@@ -11,6 +11,7 @@ from frappe.utils import nowdate, nowtime, now_datetime
import frappe.defaults import frappe.defaults
from frappe.model.db_schema import type_map from frappe.model.db_schema import type_map
import copy import copy
from frappe.core.doctype.user_permission.user_permission import get_user_permissions


def get_new_doc(doctype, parent_doc = None, parentfield = None, as_dict=False): def get_new_doc(doctype, parent_doc = None, parentfield = None, as_dict=False):
if doctype not in frappe.local.new_doc_templates: if doctype not in frappe.local.new_doc_templates:
@@ -47,7 +48,7 @@ def make_new_doc(doctype):
return doc return doc


def set_user_and_static_default_values(doc): def set_user_and_static_default_values(doc):
user_permissions = frappe.defaults.get_user_permissions()
user_permissions = get_user_permissions()
defaults = frappe.defaults.get_defaults() defaults = frappe.defaults.get_defaults()


for df in doc.meta.get("fields"): for df in doc.meta.get("fields"):
@@ -103,7 +104,7 @@ def get_static_default_value(df, user_permissions):


def set_dynamic_default_values(doc, parent_doc, parentfield): def set_dynamic_default_values(doc, parent_doc, parentfield):
# these values should not be cached # these values should not be cached
user_permissions = frappe.defaults.get_user_permissions()
user_permissions = get_user_permissions()


for df in frappe.get_meta(doc["doctype"]).get("fields"): for df in frappe.get_meta(doc["doctype"]).get("fields"):
if df.get("default"): if df.get("default"):


+ 1
- 1
frappe/model/db_query.py Просмотреть файл

@@ -388,7 +388,7 @@ class DatabaseQuery(object):
# apply user permissions? # apply user permissions?
if role_permissions.get("apply_user_permissions", {}).get("read"): if role_permissions.get("apply_user_permissions", {}).get("read"):
# get user permissions # get user permissions
user_permissions = frappe.defaults.get_user_permissions(self.user)
user_permissions = frappe.permissions.get_user_permissions(self.user)
self.add_user_permissions(user_permissions, self.add_user_permissions(user_permissions,
user_permission_doctypes=role_permissions.get("user_permission_doctypes").get("read")) user_permission_doctypes=role_permissions.get("user_permission_doctypes").get("read"))




+ 8
- 3
frappe/model/delete_doc.py Просмотреть файл

@@ -11,7 +11,7 @@ from frappe.utils.file_manager import remove_all
from frappe.utils.password import delete_all_passwords_for from frappe.utils.password import delete_all_passwords_for
from frappe import _ from frappe import _
from frappe.model.naming import revert_series_if_last from frappe.model.naming import revert_series_if_last
from frappe.utils.global_search import delete_for_document
from frappe.utils.global_search import delete_for_document


def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reload=False, def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reload=False,
ignore_permissions=False, flags=None, ignore_on_trash=False): ignore_permissions=False, flags=None, ignore_on_trash=False):
@@ -158,8 +158,13 @@ def update_flags(doc, flags=None, ignore_permissions=False):


def check_permission_and_not_submitted(doc): def check_permission_and_not_submitted(doc):
# permission # permission
if not doc.flags.ignore_permissions and frappe.session.user!="Administrator" and (not doc.has_permission("delete") or (doc.doctype=="DocType" and not doc.custom)):
frappe.msgprint(_("User not allowed to delete {0}: {1}").format(doc.doctype, doc.name), raise_exception=True)
if (not doc.flags.ignore_permissions
and frappe.session.user!="Administrator"
and (
not doc.has_permission("delete")
or (doc.doctype=="DocType" and not doc.custom))):
frappe.msgprint(_("User not allowed to delete {0}: {1}")
.format(doc.doctype, doc.name), raise_exception=frappe.PermissionError)


# check if submitted # check if submitted
if doc.docstatus == 1: if doc.docstatus == 1:


+ 1
- 0
frappe/patches.txt Просмотреть файл

@@ -189,3 +189,4 @@ frappe.patches.v8_1.enable_allow_error_traceback_in_system_settings
frappe.patches.v8_1.update_format_options_in_auto_email_report frappe.patches.v8_1.update_format_options_in_auto_email_report
frappe.patches.v8_1.delete_custom_docperm_if_doctype_not_exists frappe.patches.v8_1.delete_custom_docperm_if_doctype_not_exists
frappe.patches.v8_5.delete_email_group_member_with_invalid_emails frappe.patches.v8_5.delete_email_group_member_with_invalid_emails
frappe.patches.v8_x.update_user_permission

+ 0
- 0
frappe/patches/v8_x/__init__.py Просмотреть файл


+ 25
- 0
frappe/patches/v8_x/update_user_permission.py Просмотреть файл

@@ -0,0 +1,25 @@
import frappe

def execute():
frappe.reload_doc('core', 'doctype', 'user_permission')
frappe.delete_doc('core', 'page', 'user-permissions')
for perm in frappe.db.sql("""
select
name, parent, defkey, defvalue
from
tabDefaultValue
where
parent not in ('__default', '__global')
and
substr(defkey,1,1)!='_'
and
parenttype='User Permission'
""", as_dict=True):
frappe.get_doc(dict(
doctype='User Permission',
user=perm.parent,
allow=perm.defkey,
for_value=perm.defvalue
)).insert(ignore_permissions = True)

frappe.db.sql('delete from tabDefaultValue where parenttype="User Permission"')

+ 56
- 19
frappe/permissions.py Просмотреть файл

@@ -7,7 +7,6 @@ import frappe, copy, json
from frappe import _, msgprint from frappe import _, msgprint
from frappe.utils import cint from frappe.utils import cint
import frappe.share import frappe.share

rights = ("read", "write", "create", "delete", "submit", "cancel", "amend", rights = ("read", "write", "create", "delete", "submit", "cancel", "amend",
"print", "email", "report", "import", "export", "set_user_permissions", "share") "print", "email", "report", "import", "export", "set_user_permissions", "share")


@@ -25,6 +24,9 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None):
""" """
if not user: user = frappe.session.user if not user: user = frappe.session.user


if verbose:
print('--- Checking for {0} {1} ---'.format(doctype, doc.name if doc else '-'))

if frappe.is_table(doctype): if frappe.is_table(doctype):
if verbose: print("Table type, always true") if verbose: print("Table type, always true")
return True return True
@@ -40,7 +42,7 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None):
return False return False


if user=="Administrator": if user=="Administrator":
if verbose: print("Administrator")
if verbose: print("Allowing Administrator")
return True return True


def false_if_not_shared(): def false_if_not_shared():
@@ -210,7 +212,10 @@ def get_role_permissions(meta, user=None, verbose=False):


if p.user_permission_doctypes: if p.user_permission_doctypes:
# set user_permission_doctypes in perms # set user_permission_doctypes in perms
user_permission_doctypes = json.loads(p.user_permission_doctypes)
try:
user_permission_doctypes = json.loads(p.user_permission_doctypes)
except ValueError:
user_permission_doctypes = []
else: else:
user_permission_doctypes = get_linked_doctypes(meta.name) user_permission_doctypes = get_linked_doctypes(meta.name)


@@ -247,8 +252,12 @@ def get_role_permissions(meta, user=None, verbose=False):


return frappe.local.role_permissions[cache_key] return frappe.local.role_permissions[cache_key]


def get_user_permissions(user):
from frappe.core.doctype.user_permission.user_permission import get_user_permissions
return get_user_permissions(user)

def user_has_permission(doc, verbose=True, user=None, user_permission_doctypes=None): def user_has_permission(doc, verbose=True, user=None, user_permission_doctypes=None):
from frappe.defaults import get_user_permissions
from frappe.core.doctype.user_permission.user_permission import get_user_permissions
user_permissions = get_user_permissions(user) user_permissions = get_user_permissions(user)
user_permission_doctypes = get_user_permission_doctypes(user_permission_doctypes, user_permissions) user_permission_doctypes = get_user_permission_doctypes(user_permission_doctypes, user_permissions)


@@ -258,6 +267,10 @@ def user_has_permission(doc, verbose=True, user=None, user_permission_doctypes=N


messages = {} messages = {}


if not user_permission_doctypes:
# no doctypes restricted
end_result = True

# check multiple sets of user_permission_doctypes using OR condition # check multiple sets of user_permission_doctypes using OR condition
for doctypes in user_permission_doctypes: for doctypes in user_permission_doctypes:
result = True result = True
@@ -309,9 +322,9 @@ def has_controller_permissions(doc, ptype, user=None):
def get_doctypes_with_read(): def get_doctypes_with_read():
return list(set([p.parent for p in get_valid_perms()])) return list(set([p.parent for p in get_valid_perms()]))


def get_valid_perms(doctype=None):
def get_valid_perms(doctype=None, user=None):
'''Get valid permissions for the current user from DocPerm and Custom DocPerm''' '''Get valid permissions for the current user from DocPerm and Custom DocPerm'''
roles = get_roles()
roles = get_roles(user)


perms = get_perms_for(roles) perms = get_perms_for(roles)
custom_perms = get_perms_for(roles, 'Custom DocPerm') custom_perms = get_perms_for(roles, 'Custom DocPerm')
@@ -360,7 +373,8 @@ def get_roles(user=None, with_standard=True):


def get_perms_for(roles, perm_doctype='DocPerm'): def get_perms_for(roles, perm_doctype='DocPerm'):
'''Get perms for given roles''' '''Get perms for given roles'''
return frappe.db.sql("""select * from `tab{doctype}` where docstatus=0
return frappe.db.sql("""
select * from `tab{doctype}` where docstatus=0
and ifnull(permlevel,0)=0 and ifnull(permlevel,0)=0
and role in ({roles})""".format(doctype = perm_doctype, and role in ({roles})""".format(doctype = perm_doctype,
roles=", ".join(["%s"]*len(roles))), tuple(roles), as_dict=1) roles=", ".join(["%s"]*len(roles))), tuple(roles), as_dict=1)
@@ -386,22 +400,28 @@ def set_user_permission_if_allowed(doctype, name, user, with_message=False):
if get_role_permissions(frappe.get_meta(doctype), user).set_user_permissions!=1: if get_role_permissions(frappe.get_meta(doctype), user).set_user_permissions!=1:
add_user_permission(doctype, name, user, with_message) add_user_permission(doctype, name, user, with_message)


def add_user_permission(doctype, name, user, with_message=False):
'''Add user default'''
if name not in frappe.defaults.get_user_permissions(user).get(doctype, []):
def add_user_permission(doctype, name, user, apply=False):
'''Add user permission'''
from frappe.core.doctype.user_permission.user_permission import get_user_permissions
if name not in get_user_permissions(user).get(doctype, []):
if not frappe.db.exists(doctype, name): if not frappe.db.exists(doctype, name):
frappe.throw(_("{0} {1} not found").format(_(doctype), name), frappe.DoesNotExistError) frappe.throw(_("{0} {1} not found").format(_(doctype), name), frappe.DoesNotExistError)


frappe.defaults.add_default(doctype, name, user, "User Permission")
elif with_message:
msgprint(_("Permission already set"))
frappe.get_doc(dict(
doctype='User Permission',
user=user,
allow=doctype,
for_value=name,
apply_for_all_roles=apply
)).insert()


def remove_user_permission(doctype, name, user, default_value_name=None):
frappe.defaults.clear_default(key=doctype, value=name, parent=user, parenttype="User Permission",
name=default_value_name)
def remove_user_permission(doctype, name, user):
user_permission_name = frappe.db.get_value('User Permission',
dict(user=user, allow=doctype, for_value=name))
frappe.delete_doc('User Permission', user_permission_name)


def clear_user_permissions_for_doctype(doctype): def clear_user_permissions_for_doctype(doctype):
frappe.defaults.clear_default(parenttype="User Permission", key=doctype)
frappe.cache().delete_value('user_permissions')


def can_import(doctype, raise_exception=False): def can_import(doctype, raise_exception=False):
if not ("System Manager" in frappe.get_roles() or has_permission(doctype, "import")): if not ("System Manager" in frappe.get_roles() or has_permission(doctype, "import")):
@@ -426,9 +446,10 @@ def apply_user_permissions(doctype, ptype, user=None):


def get_user_permission_doctypes(user_permission_doctypes, user_permissions): def get_user_permission_doctypes(user_permission_doctypes, user_permissions):
"""returns a list of list like [["User", "Blog Post"], ["User"]]""" """returns a list of list like [["User", "Blog Post"], ["User"]]"""
if cint(frappe.db.get_single_value("System Settings", "ignore_user_permissions_if_missing")):
if cint(frappe.get_system_settings('ignore_user_permissions_if_missing')):
# select those user permission doctypes for which user permissions exist! # select those user permission doctypes for which user permissions exist!
user_permission_doctypes = [list(set(doctypes).intersection(set(user_permissions.keys())))
user_permission_doctypes = [
list(set(doctypes).intersection(set(user_permissions.keys())))
for doctypes in user_permission_doctypes] for doctypes in user_permission_doctypes]


if len(user_permission_doctypes) > 1: if len(user_permission_doctypes) > 1:
@@ -452,6 +473,22 @@ def get_user_permission_doctypes(user_permission_doctypes, user_permissions):


return user_permission_doctypes return user_permission_doctypes


def update_permission_property(doctype, role, permlevel, ptype, value=None, validate=True):
'''Update a property in Custom Perm'''
from frappe.core.doctype.doctype.doctype import validate_permissions_for_doctype
out = setup_custom_perms(doctype)

name = frappe.get_value('Custom DocPerm', dict(parent=doctype, role=role,
permlevel=permlevel))

frappe.db.sql("""
update `tabCustom DocPerm`
set `{0}`=%s where name=%s""".format(ptype), (value, name))
if validate:
validate_permissions_for_doctype(doctype)

return out

def setup_custom_perms(parent): def setup_custom_perms(parent):
'''if custom permssions are not setup for the current doctype, set them up''' '''if custom permssions are not setup for the current doctype, set them up'''
if not frappe.db.exists('Custom DocPerm', dict(parent=parent)): if not frappe.db.exists('Custom DocPerm', dict(parent=parent)):


+ 3
- 2
frappe/public/build.json Просмотреть файл

@@ -52,7 +52,8 @@
"public/css/desktop.css", "public/css/desktop.css",
"public/css/form.css", "public/css/form.css",
"public/css/mobile.css", "public/css/mobile.css",
"public/css/kanban.css"
"public/css/kanban.css",
"public/css/graphs.css"
], ],
"css/frappe-rtl.css": [ "css/frappe-rtl.css": [
"public/css/bootstrap-rtl.css", "public/css/bootstrap-rtl.css",
@@ -164,7 +165,7 @@
"public/js/frappe/query_string.js", "public/js/frappe/query_string.js",


"public/js/frappe/ui/charts.js", "public/js/frappe/ui/charts.js",
"public/js/frappe/ui/graph.js",
"public/js/frappe/ui/graphs.js",
"public/js/frappe/ui/comment.js", "public/js/frappe/ui/comment.js",
"public/js/frappe/misc/rating_icons.html", "public/js/frappe/misc/rating_icons.html",




+ 1
- 1
frappe/public/css/desk.css Просмотреть файл

@@ -258,7 +258,7 @@ a[disabled="disabled"]:hover {
} }
.link-btn { .link-btn {
position: absolute; position: absolute;
top: 2px;
top: 3px;
right: 4px; right: 4px;
border-radius: 2px; border-radius: 2px;
padding: 3px; padding: 3px;


+ 0
- 6
frappe/public/css/docs.css Просмотреть файл

@@ -597,9 +597,3 @@ a.edit:visited,
.page-content-wrapper > .row .col-sm-4 { .page-content-wrapper > .row .col-sm-4 {
display: none; display: none;
} }
.screenshot {
border: 2px solid #d1d8dd;
box-shadow: 1px 1px 7px rgba(0, 0, 0, 0.15);
margin: 15px 0px;
max-width: 100%;
}

+ 0
- 74
frappe/public/css/form.css Просмотреть файл

@@ -678,80 +678,6 @@ select.form-control {
padding: 10px; padding: 10px;
margin: 10px; margin: 10px;
} }
.graph-container .graphics {
margin-top: 10px;
padding: 10px 0px;
}
.graph-container .stats-group {
display: flex;
justify-content: space-around;
flex: 1;
}
.graph-container .stats-container {
display: flex;
justify-content: space-around;
}
.graph-container .stats-container .stats {
padding-bottom: 15px;
}
.graph-container .stats-container .stats-title {
color: #8D99A6;
}
.graph-container .stats-container .stats-value {
font-size: 20px;
font-weight: 300;
}
.graph-container .stats-container .stats-description {
font-size: 12px;
color: #8D99A6;
}
.graph-container .stats-container .graph-data .stats-value {
color: #98d85b;
}
.bar-graph .axis,
.line-graph .axis {
font-size: 10px;
fill: #6a737d;
}
.bar-graph .axis line,
.line-graph .axis line {
stroke: rgba(27, 31, 35, 0.1);
}
.data-points circle {
fill: #28a745;
stroke: #fff;
stroke-width: 2;
}
.data-points g.mini {
fill: #98d85b;
}
.data-points path {
fill: none;
stroke: #28a745;
stroke-opacity: 1;
stroke-width: 2px;
}
.line-graph .path {
fill: none;
stroke: #28a745;
stroke-opacity: 1;
stroke-width: 2px;
}
line.dashed {
stroke-dasharray: 5,3;
}
.tick.x-axis-label {
display: block;
}
.tick .specific-value {
text-anchor: start;
}
.tick .y-value-text {
text-anchor: end;
}
.tick .x-value-text {
text-anchor: middle;
}
body[data-route^="Form/Communication"] textarea[data-fieldname="subject"] { body[data-route^="Form/Communication"] textarea[data-fieldname="subject"] {
height: 80px !important; height: 80px !important;
} }

+ 274
- 0
frappe/public/css/graphs.css Просмотреть файл

@@ -0,0 +1,274 @@
/* graphs */
.graph-container .graph-focus-margin {
margin: 0px 5%;
}
.graph-container .graph-graphics {
margin-top: 10px;
padding: 10px 0px;
position: relative;
}
.graph-container .graph-stats-group {
display: flex;
justify-content: space-around;
flex: 1;
}
.graph-container .graph-stats-container {
display: flex;
justify-content: space-around;
padding-top: 10px;
}
.graph-container .graph-stats-container .stats {
padding-bottom: 15px;
}
.graph-container .graph-stats-container .stats-title {
color: #8D99A6;
}
.graph-container .graph-stats-container .stats-value {
font-size: 20px;
font-weight: 300;
}
.graph-container .graph-stats-container .stats-description {
font-size: 12px;
color: #8D99A6;
}
.graph-container .graph-stats-container .graph-data .stats-value {
color: #98d85b;
}
.graph-container .bar-graph .axis,
.graph-container .line-graph .axis {
font-size: 10px;
fill: #6a737d;
}
.graph-container .bar-graph .axis line,
.graph-container .line-graph .axis line {
stroke: rgba(27, 31, 35, 0.1);
}
.graph-container .percentage-graph {
margin-top: 35px;
}
.graph-container .percentage-graph .progress {
margin-bottom: 0px;
}
.graph-container .graph-data-points circle {
stroke: #fff;
stroke-width: 2;
}
.graph-container .graph-data-points path {
fill: none;
stroke-opacity: 1;
stroke-width: 2px;
}
.graph-container line.graph-dashed {
stroke-dasharray: 5,3;
}
.graph-container .tick.x-axis-label {
display: block;
}
.graph-container .tick .specific-value {
text-anchor: start;
}
.graph-container .tick .y-value-text {
text-anchor: end;
}
.graph-container .tick .x-value-text {
text-anchor: middle;
}
.graph-container .graph-svg-tip {
position: absolute;
z-index: 99999;
padding: 10px;
font-size: 12px;
color: #959da5;
text-align: center;
background: rgba(0, 0, 0, 0.8);
border-radius: 3px;
}
.graph-container .graph-svg-tip.comparison {
padding: 0;
text-align: left;
pointer-events: none;
}
.graph-container .graph-svg-tip.comparison .title {
display: block;
padding: 10px;
margin: 0;
font-weight: 600;
line-height: 1;
pointer-events: none;
}
.graph-container .graph-svg-tip.comparison ul {
margin: 0;
white-space: nowrap;
list-style: none;
}
.graph-container .graph-svg-tip.comparison li {
display: inline-block;
padding: 5px 10px;
}
.graph-container .graph-svg-tip ul,
.graph-container .graph-svg-tip ol {
padding-left: 0;
display: flex;
}
.graph-container .graph-svg-tip ul.data-point-list li {
min-width: 90px;
flex: 1;
}
.graph-container .graph-svg-tip strong {
color: #dfe2e5;
}
.graph-container .graph-svg-tip::after {
position: absolute;
bottom: -10px;
left: 50%;
width: 5px;
height: 5px;
margin: 0 0 0 -5px;
content: " ";
border: 5px solid transparent;
border-top-color: rgba(0, 0, 0, 0.8);
}
.graph-container .stroke.grey {
stroke: #F0F4F7;
}
.graph-container .stroke.blue {
stroke: #5e64ff;
}
.graph-container .stroke.red {
stroke: #ff5858;
}
.graph-container .stroke.light-green {
stroke: #98d85b;
}
.graph-container .stroke.green {
stroke: #28a745;
}
.graph-container .stroke.orange {
stroke: #ffa00a;
}
.graph-container .stroke.purple {
stroke: #743ee2;
}
.graph-container .stroke.darkgrey {
stroke: #b8c2cc;
}
.graph-container .stroke.black {
stroke: #36414C;
}
.graph-container .stroke.yellow {
stroke: #FEEF72;
}
.graph-container .stroke.light-blue {
stroke: #7CD6FD;
}
.graph-container .stroke.lightblue {
stroke: #7CD6FD;
}
.graph-container .fill.grey {
fill: #F0F4F7;
}
.graph-container .fill.blue {
fill: #5e64ff;
}
.graph-container .fill.red {
fill: #ff5858;
}
.graph-container .fill.light-green {
fill: #98d85b;
}
.graph-container .fill.green {
fill: #28a745;
}
.graph-container .fill.orange {
fill: #ffa00a;
}
.graph-container .fill.purple {
fill: #743ee2;
}
.graph-container .fill.darkgrey {
fill: #b8c2cc;
}
.graph-container .fill.black {
fill: #36414C;
}
.graph-container .fill.yellow {
fill: #FEEF72;
}
.graph-container .fill.light-blue {
fill: #7CD6FD;
}
.graph-container .fill.lightblue {
fill: #7CD6FD;
}
.graph-container .background.grey {
background: #F0F4F7;
}
.graph-container .background.blue {
background: #5e64ff;
}
.graph-container .background.red {
background: #ff5858;
}
.graph-container .background.light-green {
background: #98d85b;
}
.graph-container .background.green {
background: #28a745;
}
.graph-container .background.orange {
background: #ffa00a;
}
.graph-container .background.purple {
background: #743ee2;
}
.graph-container .background.darkgrey {
background: #b8c2cc;
}
.graph-container .background.black {
background: #36414C;
}
.graph-container .background.yellow {
background: #FEEF72;
}
.graph-container .background.light-blue {
background: #7CD6FD;
}
.graph-container .background.lightblue {
background: #7CD6FD;
}
.graph-container .border-top.grey {
border-top: 3px solid #F0F4F7;
}
.graph-container .border-top.blue {
border-top: 3px solid #5e64ff;
}
.graph-container .border-top.red {
border-top: 3px solid #ff5858;
}
.graph-container .border-top.light-green {
border-top: 3px solid #98d85b;
}
.graph-container .border-top.green {
border-top: 3px solid #28a745;
}
.graph-container .border-top.orange {
border-top: 3px solid #ffa00a;
}
.graph-container .border-top.purple {
border-top: 3px solid #743ee2;
}
.graph-container .border-top.darkgrey {
border-top: 3px solid #b8c2cc;
}
.graph-container .border-top.black {
border-top: 3px solid #36414C;
}
.graph-container .border-top.yellow {
border-top: 3px solid #FEEF72;
}
.graph-container .border-top.light-blue {
border-top: 3px solid #7CD6FD;
}
.graph-container .border-top.lightblue {
border-top: 3px solid #7CD6FD;
}

+ 25
- 73
frappe/public/css/website.css Просмотреть файл

@@ -430,6 +430,9 @@ h6 a {
color: inherit !important; color: inherit !important;
text-decoration: none; text-decoration: none;
} }
li {
line-height: 1.7em;
}
.navbar-brand { .navbar-brand {
max-width: none; max-width: none;
} }
@@ -503,6 +506,9 @@ h6 a {
min-height: 140px; min-height: 140px;
border-top: 1px solid #EBEFF2; border-top: 1px solid #EBEFF2;
} }
.page_content {
padding-bottom: 30px;
}
.carousel-control .icon { .carousel-control .icon {
position: absolute; position: absolute;
top: 50%; top: 50%;
@@ -599,7 +605,7 @@ fieldset {
} }
.web-sidebar .sidebar-item { .web-sidebar .sidebar-item {
margin: 0px; margin: 0px;
padding: 12px 0px;
padding-bottom: 12px;
border: none; border: none;
color: #8D99A6; color: #8D99A6;
font-size: 12px; font-size: 12px;
@@ -607,21 +613,14 @@ fieldset {
.web-sidebar .sidebar-item .badge { .web-sidebar .sidebar-item .badge {
font-weight: normal; font-weight: normal;
} }
.web-sidebar .sidebar-item:first-child {
padding-top: 10px;
}
.web-sidebar .sidebar-item:last-child {
padding-bottom: 10px;
}
.web-sidebar .sidebar-item a { .web-sidebar .sidebar-item a {
color: #8D99A6;
color: #36414C !important;
} }
.web-sidebar .sidebar-item a.active { .web-sidebar .sidebar-item a.active {
color: #36414C !important; color: #36414C !important;
font-weight: 500 !important; font-weight: 500 !important;
} }
.web-sidebar .sidebar-items { .web-sidebar .sidebar-items {
margin-top: -10px;
margin-bottom: 30px; margin-bottom: 30px;
} }
.web-sidebar .sidebar-items .title { .web-sidebar .sidebar-items .title {
@@ -675,69 +674,25 @@ fieldset {
.web-list-item:last-child { .web-list-item:last-child {
border-bottom: 0px; border-bottom: 0px;
} }
.blog-info {
text-align: center;
margin-top: 30px;
}
.post-description {
padding-bottom: 8px;
}
.post-description p {
margin-bottom: 8px;
}
.blog-footer {
padding: 5px 15px;
border-top: 1px solid #EBEFF2;
margin: 0px -15px -20px -15px;
}
.blog-list-content .website-list .result {
.website-list .result {
border: 0px; border: 0px;
} }
.blog-list-content .web-list-item:hover {
.web-list-item:hover {
background: transparent; background: transparent;
} }
.blog-category {
letter-spacing: 0.5px;
text-align: center;
margin-bottom: 30px;
}
.author {
letter-spacing: 0.5px;
border-bottom: 1px solid #EBEFF2;
padding-bottom: 30px;
}
.blogger {
padding-top: 0px;
padding-bottom: 50px;
}
.blog-dot:before {
.spacer-dot:before {
padding-right: 8px; padding-right: 8px;
padding-left: 8px; padding-left: 8px;
content: "\2022"; content: "\2022";
} }
.blog-list-item {
margin-top: 30px;
margin-bottom: 30px;
}
.blog-list-item .blog-header {
font-size: 1.6em;
}
.blog-header {
font-weight: 700;
font-size: 2em;
}
.add-comment-section { .add-comment-section {
padding-bottom: 30px; padding-bottom: 30px;
} }
.blog-comments {
position: relative;
border-top: 1px solid #d1d8dd;
}
.blog-comment-row {
.comment-row {
margin: 0px -15px; margin: 0px -15px;
padding: 15px; padding: 15px;
} }
.blog-comment-row:last-child {
.comment-row:last-child {
margin-bottom: 30px; margin-bottom: 30px;
border-bottom: 0px; border-bottom: 0px;
} }
@@ -837,7 +792,7 @@ a.active {
} }
.sidebar-block, .sidebar-block,
.page-content { .page-content {
padding-top: 50px;
padding-top: 30px;
padding-bottom: 50px; padding-bottom: 50px;
} }
.your-account-info { .your-account-info {
@@ -871,19 +826,6 @@ a.active {
li.footer-child-item { li.footer-child-item {
margin: 15px 0px; margin: 15px 0px;
} }
.blog-info {
text-align: center;
margin-top: 30px;
}
.blog-text {
padding-top: 50px;
padding-bottom: 50px;
font-size: 18px;
line-height: 1.5;
}
.blog-text p {
margin-bottom: 30px;
}
.comment-view { .comment-view {
padding-bottom: 30px; padding-bottom: 30px;
} }
@@ -927,7 +869,7 @@ li.footer-child-item {
overflow: hidden; overflow: hidden;
} }
.vert-line > div + div { .vert-line > div + div {
border-left: 1px solid #EBEFF2;
border-left: 1px solid #d1d8dd;
} }
.vert-line > div { .vert-line > div {
padding-bottom: 2000px; padding-bottom: 2000px;
@@ -994,3 +936,13 @@ li.footer-child-item {
padding: 10px; padding: 10px;
border-radius: 4px; border-radius: 4px;
} }
.docfields pre {
background-color: transparent;
border: none;
}
.screenshot {
border: 1px solid #d1d8dd;
box-shadow: 1px 1px 7px rgba(0, 0, 0, 0.15);
margin: 15px 0px;
max-width: 100%;
}

+ 1
- 5
frappe/public/js/frappe/defaults.js Просмотреть файл

@@ -77,10 +77,6 @@ frappe.defaults = {
}, },


get_user_permissions: function() { get_user_permissions: function() {
return frappe.defaults.user_permissions;
return frappe.boot.user_permissions;
}, },
set_user_permissions: function(user_permissions) {
if(!user_permissions) return;
frappe.defaults.user_permissions = $.extend(frappe.defaults.user_permissions || {}, user_permissions);
}
} }

+ 2
- 3
frappe/public/js/frappe/form/dashboard.js Просмотреть файл

@@ -418,9 +418,8 @@ frappe.ui.form.Dashboard = Class.extend({
this.graph_area.empty().removeClass('hidden'); this.graph_area.empty().removeClass('hidden');
$.extend(args, { $.extend(args, {
parent: me.graph_area, parent: me.graph_area,
width: 710,
height: 140,
mode: 'line-graph'
mode: 'line',
height: 140
}); });


new frappe.ui.Graph(args); new frappe.ui.Graph(args);


+ 6
- 1
frappe/public/js/frappe/list/list_renderer.js Просмотреть файл

@@ -271,7 +271,10 @@ frappe.views.ListRenderer = Class.extend({


setup_filterable: function () { setup_filterable: function () {
var me = this; var me = this;

this.list_view.wrapper &&
this.list_view.wrapper.on('click', '.result-list .filterable', function (e) { this.list_view.wrapper.on('click', '.result-list .filterable', function (e) {
e.stopPropagation();
var filters = $(this).attr('data-filter').split('|'); var filters = $(this).attr('data-filter').split('|');
var added = false; var added = false;


@@ -294,7 +297,9 @@ frappe.views.ListRenderer = Class.extend({
me.list_view.refresh(true); me.list_view.refresh(true);
} }
}); });
this.wrapper.on('click', '.list-item', function (e) {

this.list_view.wrapper &&
this.list_view.wrapper.on('click', '.list-item', function (e) {
// don't open in case of checkbox, like, filterable // don't open in case of checkbox, like, filterable
if ($(e.target).hasClass('filterable') if ($(e.target).hasClass('filterable')
|| $(e.target).hasClass('octicon-heart') || $(e.target).hasClass('octicon-heart')


+ 3
- 3
frappe/public/js/frappe/list/list_view.js Просмотреть файл

@@ -599,9 +599,9 @@ frappe.views.ListView = frappe.ui.BaseList.extend({
}, true); }, true);
} }
if (frappe.model.can_set_user_permissions(this.doctype)) { if (frappe.model.can_set_user_permissions(this.doctype)) {
this.page.add_menu_item(__('User Permissions Manager'), function () {
frappe.set_route('user-permissions', {
doctype: me.doctype
this.page.add_menu_item(__('User Permissions'), function () {
frappe.set_route('List', 'User Permission', {
allow: me.doctype
}); });
}, true); }, true);
} }


+ 4
- 2
frappe/public/js/frappe/model/create_new.js Просмотреть файл

@@ -127,8 +127,10 @@ $.extend(frappe.model, {
var user_default = ""; var user_default = "";
var user_permissions = frappe.defaults.get_user_permissions(); var user_permissions = frappe.defaults.get_user_permissions();
var meta = frappe.get_meta(doc.doctype); var meta = frappe.get_meta(doc.doctype);
var has_user_permissions = (df.fieldtype==="Link" && user_permissions
&& df.ignore_user_permissions != 1 && user_permissions[df.options]);
var has_user_permissions = (df.fieldtype==="Link"
&& user_permissions
&& df.ignore_user_permissions != 1
&& user_permissions[df.options]);


// don't set defaults for "User" link field using User Permissions! // don't set defaults for "User" link field using User Permissions!
if (df.fieldtype==="Link" && df.options!=="User") { if (df.fieldtype==="Link" && df.options!=="User") {


+ 0
- 1
frappe/public/js/frappe/model/model.js Просмотреть файл

@@ -112,7 +112,6 @@ $.extend(frappe.model, {
localStorage["_doctype:" + doctype] = JSON.stringify(r.docs); localStorage["_doctype:" + doctype] = JSON.stringify(r.docs);
} }
frappe.model.init_doctype(doctype); frappe.model.init_doctype(doctype);
frappe.defaults.set_user_permissions(r.user_permissions);


if(r.user_settings) { if(r.user_settings) {
// remember filters and other settings from last view // remember filters and other settings from last view


+ 1
- 0
frappe/public/js/frappe/ui/filters/filters.js Просмотреть файл

@@ -87,6 +87,7 @@ frappe.ui.FilterList = Class.extend({
} }


var filter = this.push_new_filter(doctype, fieldname, condition, value); var filter = this.push_new_filter(doctype, fieldname, condition, value);
if (!filter) return;


if(this.wrapper.find('.clear-filters').hasClass("hide")) { if(this.wrapper.find('.clear-filters').hasClass("hide")) {
this.wrapper.find('.clear-filters').removeClass("hide"); this.wrapper.find('.clear-filters').removeClass("hide");


+ 0
- 308
frappe/public/js/frappe/ui/graph.js Просмотреть файл

@@ -1,308 +0,0 @@
// specific_values = [
// {
// name: "Average",
// line_type: "dashed", // "dashed" or "solid"
// value: 10
// },

// summary_values = [
// {
// name: "Total",
// color: 'blue', // Indicator colors: 'grey', 'blue', 'red', 'green', 'orange',
// // 'purple', 'darkgrey', 'black', 'yellow', 'lightblue'
// value: 80
// }
// ]

frappe.ui.Graph = class Graph {
constructor({
parent = null,

width = 0, height = 0,
title = '', subtitle = '',

y_values = [],
x_points = [],

specific_values = [],
summary_values = [],

color = '',
mode = '',
} = {}) {

if(Object.getPrototypeOf(this) === frappe.ui.Graph.prototype) {
if(mode === 'line-graph') {
return new frappe.ui.LineGraph(arguments[0]);
} else if(mode === 'bar-graph') {
return new frappe.ui.BarGraph(arguments[0]);
}
}

this.parent = parent;

this.width = width;
this.height = height;

this.title = title;
this.subtitle = subtitle;

this.y_values = y_values;
this.x_points = x_points;

this.specific_values = specific_values;
this.summary_values = summary_values;

this.color = color;
this.mode = mode;

this.$graph = null;

frappe.require("assets/frappe/js/lib/snap.svg-min.js", this.setup.bind(this));
}

setup() {
this.setup_container();
this.refresh();
}

refresh() {
this.setup_values();
this.setup_components();
this.make_y_axis();
this.make_x_axis();
this.make_units();
if(this.specific_values.length > 0) {
this.show_specific_values();
}
this.setup_group();

if(this.summary_values.length > 0) {
this.show_summary();
}
}

setup_container() {
this.container = $('<div>')
.addClass('graph-container')
.append($(`<h6 class="title" style="margin-top: 15px;">${this.title}</h6>`))
.append($(`<h6 class="sub-title uppercase">${this.subtitle}</h6>`))
.append($(`<div class="graphics"></div>`))
.append($(`<div class="stats-container"></div>`))
.appendTo(this.parent);

let $graphics = this.container.find('.graphics');
this.$stats_container = this.container.find('.stats-container');

this.$graph = $('<div>')
.addClass(this.mode)
.appendTo($graphics);

this.$svg = $(`<svg class="svg" width="${this.width}" height="${this.height}"></svg>`);
this.$graph.append(this.$svg);

this.snap = new Snap(this.$svg[0]);
}

setup_values() {
this.upper_graph_bound = this.get_upper_limit_and_parts(this.y_values)[0];
this.y_axis = this.get_y_axis(this.y_values);
this.avg_unit_width = (this.width-100)/(this.x_points.length - 1);
}

setup_components() {
this.y_axis_group = this.snap.g().attr({
class: "y axis"
});

this.x_axis_group = this.snap.g().attr({
class: "x axis"
});

this.graph_list = this.snap.g().attr({
class: "data-points",
});

this.specific_y_lines = this.snap.g().attr({
class: "specific axis",
});
}

setup_group() {
this.snap.g(
this.y_axis_group,
this.x_axis_group,
this.graph_list,
this.specific_y_lines
).attr({
transform: "translate(60, 10)" // default
});
}

show_specific_values() {
this.specific_values.map(d => {
this.specific_y_lines.add(this.snap.g(
this.snap.line(0, 0, this.width - 70, 0).attr({
class: d.line_type === "dashed" ? "dashed": ""
}),
this.snap.text(this.width - 95, 0, d.name.toUpperCase()).attr({
dy: ".32em",
class: "specific-value",
})
).attr({
class: "tick",
transform: `translate(0, ${100 - 100/(this.upper_graph_bound/d.value) })`
}));
});
}

show_summary() {
this.summary_values.map(d => {
this.$stats_container.append($(`<div class="stats">
<span class="indicator ${d.color}">${d.name}: ${d.value}</span>
</div>`));
});
}

// Helpers
get_upper_limit_and_parts(array) {
let specific_values = this.specific_values.map(d => d.value);
let max_val = parseInt(Math.max(...array, ...specific_values));
if((max_val+"").length <= 1) {
return [10, 5];
} else {
let multiplier = Math.pow(10, ((max_val+"").length - 1));
let significant = Math.ceil(max_val/multiplier);
if(significant % 2 !== 0) significant++;
let parts = (significant < 5) ? significant : significant/2;
return [significant * multiplier, parts];
}
}

get_y_axis(array) {
let upper_limit, parts;
[upper_limit, parts] = this.get_upper_limit_and_parts(array);
let y_axis = [];
for(var i = 0; i <= parts; i++){
y_axis.push(upper_limit / parts * i);
}
return y_axis;
}
};

frappe.ui.BarGraph = class BarGraph extends frappe.ui.Graph {
constructor(args = {}) {
super(args);
}

setup_values() {
super.setup_values();
this.avg_unit_width = (this.width-50)/(this.x_points.length + 2);
}

make_y_axis() {
this.y_axis.map((point) => {
this.y_axis_group.add(this.snap.g(
this.snap.line(0, 0, this.width, 0),
this.snap.text(-3, 0, point+"").attr({
dy: ".32em",
class: "y-value-text"
})
).attr({
class: "tick",
transform: `translate(0, ${100 - (100/(this.y_axis.length-1) * this.y_axis.indexOf(point)) })`
}));
});
}

make_x_axis() {
this.x_axis_group.attr({
transform: "translate(0,100)"
});
this.x_points.map((point, i) => {
this.x_axis_group.add(this.snap.g(
this.snap.line(0, 0, 0, 6),
this.snap.text(0, 9, point).attr({
dy: ".71em",
class: "x-value-text"
})
).attr({
class: "tick x-axis-label",
transform: `translate(${ ((this.avg_unit_width - 5)*3/2) + i * (this.avg_unit_width + 5) }, 0)`
}));
});
}

make_units() {
this.y_values.map((value, i) => {
this.graph_list.add(this.snap.g(
this.snap.rect(
0,
(100 - 100/(this.upper_graph_bound/value)),
this.avg_unit_width - 5,
100/(this.upper_graph_bound/value)
)
).attr({
class: "bar mini",
transform: `translate(${ (this.avg_unit_width - 5) + i * (this.avg_unit_width + 5) }, 0)`,
}));
});
}
};

frappe.ui.LineGraph = class LineGraph extends frappe.ui.Graph {
constructor(args = {}) {
super(args);
}

make_y_axis() {
this.y_axis.map((point) => {
this.y_axis_group.add(this.snap.g(
this.snap.line(0, 0, -6, 0),
this.snap.text(-9, 0, point+"").attr({
dy: ".32em",
class: "y-value-text"
})
).attr({
class: "tick",
transform: `translate(0, ${100 - (100/(this.y_axis.length-1)
* this.y_axis.indexOf(point)) })`
}));
});
}

make_x_axis() {
this.x_axis_group.attr({
transform: "translate(0,-7)"
});
this.x_points.map((point, i) => {
this.x_axis_group.add(this.snap.g(
this.snap.line(0, 0, 0, this.height - 25),
this.snap.text(0, this.height - 15, point).attr({
dy: ".71em",
class: "x-value-text"
})
).attr({
class: "tick",
transform: `translate(${ i * this.avg_unit_width }, 0)`
}));
});
}

make_units() {
let points_list = [];
this.y_values.map((value, i) => {
let x = i * this.avg_unit_width;
let y = (100 - 100/(this.upper_graph_bound/value));
this.graph_list.add(this.snap.circle( x, y, 4));
points_list.push(x+","+y);
});

this.make_path("M"+points_list.join("L"));
}

make_path(path_str) {
this.graph_list.prepend(this.snap.path(path_str));
}

};

+ 569
- 0
frappe/public/js/frappe/ui/graphs.js Просмотреть файл

@@ -0,0 +1,569 @@
// specific_values = [
// {
// name: "Average",
// line_type: "dashed", // "dashed" or "solid"
// value: 10
// },

// summary = [
// {
// name: "Total",
// color: 'blue', // Indicator colors: 'grey', 'blue', 'red', 'green', 'orange',
// // 'purple', 'darkgrey', 'black', 'yellow', 'lightblue'
// value: 80
// }
// ]

// Graph: Abstract object
frappe.ui.Graph = class Graph {
constructor({
parent = null,
height = 240,

title = '', subtitle = '',

y = [],
x = [],

specific_values = [],
summary = [],

color = 'blue',
mode = '',
}) {

if(Object.getPrototypeOf(this) === frappe.ui.Graph.prototype) {
if(mode === 'line') {
return new frappe.ui.LineGraph(arguments[0]);
} else if(mode === 'bar') {
return new frappe.ui.BarGraph(arguments[0]);
} else if(mode === 'percentage') {
return new frappe.ui.PercentageGraph(arguments[0]);
}
}

this.parent = parent;
this.base_height = height;
this.height = height - 40;

this.translate_x = 60;
this.translate_y = 10;

this.title = title;
this.subtitle = subtitle;

this.y = y;
this.x = x;

this.specific_values = specific_values;
this.summary = summary;

this.color = color;
this.mode = mode;

this.$graph = null;

// Validate all arguments

frappe.require("assets/frappe/js/lib/snap.svg-min.js", this.setup.bind(this));
}

setup() {
this.bind_window_event();
this.refresh();
}

bind_window_event() {
$(window).on('resize orientationChange', () => {
this.refresh();
});
}

refresh() {

this.base_width = this.parent.width() - 20;
this.width = this.base_width - 100;

this.setup_container();

this.setup_values();

this.setup_utils();

this.setup_components();
this.make_graph_components();

this.make_tooltip();

if(this.summary.length > 0) {
this.show_custom_summary();
} else {
this.show_summary();
}
}

setup_container() {
// Graph needs a dedicated parent element
this.parent.empty();

this.container = $('<div>')
.addClass('graph-container')
.append($(`<h6 class="title" style="margin-top: 15px;">${this.title}</h6>`))
.append($(`<h6 class="sub-title uppercase">${this.subtitle}</h6>`))
.append($(`<div class="graph-graphics"></div>`))
.append($(`<div class="graph-stats-container"></div>`))
.appendTo(this.parent);

this.$graphics = this.container.find('.graph-graphics');
this.$stats_container = this.container.find('.graph-stats-container');

this.$graph = $('<div>')
.addClass(this.mode + '-graph')
.appendTo(this.$graphics);

this.$graph.append(this.make_graph_area());
}

make_graph_area() {
this.$svg = $(`<svg class="svg" width="${this.base_width}" height="${this.base_height}"></svg>`);
this.snap = new Snap(this.$svg[0]);
return this.$svg;
}

setup_values() {
// Multiplier
let all_values = this.specific_values.map(d => d.value);
this.y.map(d => {
all_values = all_values.concat(d.values);
});
[this.upper_limit, this.parts] = this.get_upper_limit_and_parts(all_values);
this.multiplier = this.height / this.upper_limit;

// Baselines
this.set_avg_unit_width_and_x_offset();

this.x_axis_values = this.x.values.map((d, i) => this.x_offset + i * this.avg_unit_width);
this.y_axis_values = this.get_y_axis_values(this.upper_limit, this.parts);

// Data points
this.y.map(d => {
d.y_tops = d.values.map( val => this.height - val * this.multiplier );
d.data_units = [];
});

this.calc_min_tops();
}

set_avg_unit_width_and_x_offset() {
this.avg_unit_width = this.width/(this.x.values.length - 1);
this.x_offset = 0;
}

calc_min_tops() {
this.y_min_tops = new Array(this.x_axis_values.length).fill(9999);
this.y.map(d => {
d.y_tops.map( (y_top, i) => {
if(y_top < this.y_min_tops[i]) {
this.y_min_tops[i] = y_top;
}
});
});
}

setup_components() {
this.y_axis_group = this.snap.g().attr({ class: "y axis" });
this.x_axis_group = this.snap.g().attr({ class: "x axis" });
this.data_units = this.snap.g().attr({ class: "graph-data-points" });
this.specific_y_lines = this.snap.g().attr({ class: "specific axis" });
}

make_graph_components() {
this.make_y_axis();
this.make_x_axis();

this.y.map((d, i) => {
this.make_units(d.y_tops, d.color, i);
this.make_path(d);
});

if(this.specific_values.length > 0) {
this.show_specific_values();
}
this.setup_group();
}

setup_group() {
this.snap.g(
this.y_axis_group,
this.x_axis_group,
this.data_units,
this.specific_y_lines
).attr({
transform: `translate(${this.translate_x}, ${this.translate_y})`
});
}

// make HORIZONTAL lines for y values
make_y_axis() {
let width, text_end_at = -9, label_class = '', start_at = 0;
if(this.y_axis_mode === 'span') { // long spanning lines
width = this.width + 6;
start_at = -6;
} else if(this.y_axis_mode === 'tick'){ // short label lines
width = -6;
label_class = 'y-axis-label';
}

this.y_axis_values.map((point) => {
this.y_axis_group.add(this.snap.g(
this.snap.line(start_at, 0, width, 0),
this.snap.text(text_end_at, 0, point+"").attr({
dy: ".32em",
class: "y-value-text"
})
).attr({
class: `tick ${label_class}`,
transform: `translate(0, ${this.height - point * this.multiplier })`
}));
});
}

// make VERTICAL lines for x values
make_x_axis() {
let start_at, height, text_start_at, label_class = '';
if(this.x_axis_mode === 'span') { // long spanning lines
start_at = -7;
height = this.height + 15;
text_start_at = this.height + 25;
} else if(this.x_axis_mode === 'tick'){ // short label lines
start_at = this.height;
height = 6;
text_start_at = 9;
label_class = 'x-axis-label';
}

this.x_axis_group.attr({
transform: `translate(0,${start_at})`
});
this.x.values.map((point, i) => {
this.x_axis_group.add(this.snap.g(
this.snap.line(0, 0, 0, height),
this.snap.text(0, text_start_at, point).attr({
dy: ".71em",
class: "x-value-text"
})
).attr({
class: `tick ${label_class}`,
transform: `translate(${ this.x_axis_values[i] }, 0)`
}));
});
}

make_units(y_values, color, dataset_index) {
let d = this.unit_args;
y_values.map((y, i) => {
let data_unit = this.draw[d.type](this.x_axis_values[i],
y, d.args, color, dataset_index);
this.data_units.add(data_unit);
this.y[dataset_index].data_units.push(data_unit);
});
}

make_path() { }

make_tooltip() {
this.tip = $(`<div class="graph-svg-tip comparison">
<span class="title"></span>
<ul class="data-point-list">
</ul>
</div>`).attr({
style: `top: 0px; left: 0px; opacity: 0; pointer-events: none;`
}).appendTo(this.$graphics);

this.tip_title = this.tip.find('.title');
this.tip_data_point_list = this.tip.find('.data-point-list');

this.bind_tooltip();
}

bind_tooltip() {
this.$graphics.on('mousemove', (e) => {
let offset = $(this.$graphics).offset();
var relX = e.pageX - offset.left - this.translate_x;
var relY = e.pageY - offset.top - this.translate_y;

if(relY < this.height) {
for(var i=this.x_axis_values.length - 1; i >= 0 ; i--) {
let x_val = this.x_axis_values[i];
if(relX > x_val - this.avg_unit_width/2) {
let x = x_val - this.tip.width()/2 + this.translate_x;
let y = this.y_min_tops[i] - this.tip.height() + this.translate_y;

this.fill_tooltip(i);

this.tip.attr({
style: `top: ${y}px; left: ${x-0.5}px; opacity: 1; pointer-events: none;`
});
break;
}
}
} else {
this.tip.attr({
style: `top: 0px; left: 0px; opacity: 0; pointer-events: none;`
});
}
});

this.$graphics.on('mouseleave', () => {
this.tip.attr({
style: `top: 0px; left: 0px; opacity: 0; pointer-events: none;`
});
});
}

fill_tooltip(i) {
this.tip_title.html(this.x.formatted && this.x.formatted.length>0
? this.x.formatted[i] : this.x.values[i]);
this.tip_data_point_list.empty();
this.y.map(y_set => {
let $li = $(`<li>
<strong style="display: block;">
${y_set.formatted ? y_set.formatted[i] : y_set.values[i]}
</strong>
${y_set.title ? y_set.title : '' }
</li>`).addClass(`border-top ${y_set.color}`);
this.tip_data_point_list.append($li);
});
}

show_specific_values() {
this.specific_values.map(d => {
this.specific_y_lines.add(this.snap.g(
this.snap.line(0, 0, this.width, 0).attr({
class: d.line_type === "dashed" ? "graph-dashed": ""
}),
this.snap.text(this.width + 5, 0, d.name.toUpperCase()).attr({
dy: ".32em",
class: "specific-value",
})
).attr({
class: "tick",
transform: `translate(0, ${this.height - d.value * this.multiplier })`
}));
});
}

show_summary() { }

show_custom_summary() {
this.summary.map(d => {
this.$stats_container.append($(`<div class="stats">
<span class="indicator ${d.color}">${d.name}: ${d.value}</span>
</div>`));
});
}

change_values(new_y) {
let u = this.unit_args;
this.y.map((d, i) => {
let new_d = new_y[i];
new_d.y_tops = new_d.values.map(val => this.height - val * this.multiplier);

// below is equal to this.y[i].data_units..
d.data_units.map((unit, j) => {
let current_y_top = d.y_tops[j];
let current_height = this.height - current_y_top;

let new_y_top = new_d.y_tops[j];
let new_height = current_height - (new_y_top - current_y_top);

this.animate[u.type](unit, new_y_top, {new_height: new_height});
});
});

// Replace values and formatted and tops
this.y.map((d, i) => {
let new_d = new_y[i];
[d.values, d.formatted, d.y_tops] = [new_d.values, new_d.formatted, new_d.y_tops];
});

this.calc_min_tops();

// create new x,y pair string and animate path
if(this.y[0].path) {
new_y.map((e, i) => {
let new_points_list = e.y_tops.map((y, i) => (this.x_axis_values[i] + ',' + y));
let new_path_str = "M"+new_points_list.join("L");
this.y[i].path.animate({d:new_path_str}, 300, mina.easein);
});
}
}

// Helpers
get_strwidth(string) {
return string.length * 8;
}

get_upper_limit_and_parts(array) {
let max_val = parseInt(Math.max(...array));
if((max_val+"").length <= 1) {
return [10, 5];
} else {
let multiplier = Math.pow(10, ((max_val+"").length - 1));
let significant = Math.ceil(max_val/multiplier);
if(significant % 2 !== 0) significant++;
let parts = (significant < 5) ? significant : significant/2;
return [significant * multiplier, parts];
}
}

get_y_axis_values(upper_limit, parts) {
let y_axis = [];
for(var i = 0; i <= parts; i++){
y_axis.push(upper_limit / parts * i);
}
return y_axis;
}

// Objects
setup_utils() {
this.draw = {
'bar': (x, y, args, color, index) => {
let total_width = this.avg_unit_width - args.space_width;
let start_x = x - total_width/2;

let width = total_width / args.no_of_datasets;
let current_x = start_x + width * index;
return this.snap.rect(current_x, y, width, this.height - y).attr({
class: `bar mini fill ${color}`
});
},
'dot': (x, y, args, color) => {
return this.snap.circle(x, y, args.radius).attr({
class: `fill ${color}`
});
}
};

this.animate = {
'bar': (bar, new_y, args) => {
bar.animate({height: args.new_height, y: new_y}, 300, mina.easein);
},
'dot': (dot, new_y) => {
dot.animate({cy: new_y}, 300, mina.easein);
}
};
}
};

frappe.ui.BarGraph = class BarGraph extends frappe.ui.Graph {
constructor(args = {}) {
super(args);
}

setup_values() {
var me = this;
super.setup_values();
this.x_offset = this.avg_unit_width;
this.y_axis_mode = 'span';
this.x_axis_mode = 'tick';
this.unit_args = {
type: 'bar',
args: {
space_width: this.y.length > 1 ?
me.avg_unit_width/2 : me.avg_unit_width/8,
no_of_datasets: this.y.length
}
};
}

set_avg_unit_width_and_x_offset() {
this.avg_unit_width = this.width/(this.x.values.length + 1);
this.x_offset = this.avg_unit_width;
}
};

frappe.ui.LineGraph = class LineGraph extends frappe.ui.Graph {
constructor(args = {}) {
super(args);
}

setup_values() {
super.setup_values();
this.y_axis_mode = 'tick';
this.x_axis_mode = 'span';
this.unit_args = {
type: 'dot',
args: { radius: 4 }
};
}

make_path(d) {
let points_list = d.y_tops.map((y, i) => (this.x_axis_values[i] + ',' + y));
let path_str = "M"+points_list.join("L");
d.path = this.snap.path(path_str).attr({class: `stroke ${d.color}`});
this.data_units.prepend(d.path);
}
};

frappe.ui.PercentageGraph = class PercentageGraph extends frappe.ui.Graph {
constructor(args = {}) {
super(args);
}

make_graph_area() {
this.$graphics.addClass('graph-focus-margin');
this.$stats_container.addClass('graph-focus-margin').attr({
style: `padding-top: 0px; margin-bottom: 30px;`
});
this.$div = $(`<div class="div" width="${this.base_width}"
height="${this.base_height}">
<div class="progress-chart"></div>
</div>`);
this.$chart = this.$div.find('.progress-chart');
return this.$div;
}

setup_values() {
this.x.totals = this.x.values.map((d, i) => {
let total = 0;
this.y.map(e => {
total += e.values[i];
});
return total;
});

// Calculate x unit distances for tooltips
}

setup_utils() { }
setup_components() {
this.$percentage_bar = $(`<div class="progress">
</div>`).appendTo(this.$chart);
}

make_graph_components() {
let grand_total = this.x.totals.reduce((a, b) => a + b, 0);
this.x.units = [];
this.x.totals.map((total, i) => {
let $part = $(`<div class="progress-bar background ${this.x.colors[i]}"
style="width: ${total*100/grand_total}%"></div>`);
this.x.units.push($part);
this.$percentage_bar.append($part);
});
}

make_tooltip() { }

show_summary() {
let values = this.x.formatted.length > 0 ? this.x.formatted : this.x.values;
this.x.totals.map((d, i) => {
this.$stats_container.append($(`<div class="stats">
<span class="indicator ${this.x.colors[i]}">
<span class="text-muted">${values[i]}:</span>
${d}
</span>
</div>`));
});
}
};

+ 3
- 3
frappe/public/js/frappe/ui/messages.js Просмотреть файл

@@ -141,17 +141,17 @@ frappe.msgprint = function(msg, title) {
msg = replace_newlines(data.message); msg = replace_newlines(data.message);
} }


var msg_exists = false;
if(data.clear) { if(data.clear) {
msg_dialog.msg_area.empty(); msg_dialog.msg_area.empty();
var msg_exists = false;
} else { } else {
var msg_exists = msg_dialog.msg_area.html();
msg_exists = msg_dialog.msg_area.html();
} }


if(data.title || !msg_exists) { if(data.title || !msg_exists) {
// set title only if it is explicitly given // set title only if it is explicitly given
// and no existing title exists // and no existing title exists
msg_dialog.set_title(data.title || __('Message'))
msg_dialog.set_title(data.title || __('Message'));
} }


// show / hide indicator // show / hide indicator


+ 2
- 2
frappe/public/js/frappe/views/reports/query_report.js Просмотреть файл

@@ -43,7 +43,7 @@ frappe.views.QueryReport = Class.extend({
this.wrapper = $("<div>").appendTo(this.page.main); this.wrapper = $("<div>").appendTo(this.page.main);
$('<div class="waiting-area" style="display: none;"></div>\ $('<div class="waiting-area" style="display: none;"></div>\
<div class="no-report-area msg-box no-border" style="display: none;"></div>\ <div class="no-report-area msg-box no-border" style="display: none;"></div>\
<div class="chart_area" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 1px"></div>\
<div class="chart_area" style="border-bottom: 1px solid #d1d8dd; padding: 0px 5%"></div>\
<div class="results" style="display: none;">\ <div class="results" style="display: none;">\
<div class="result-area" style="height:400px;"></div>\ <div class="result-area" style="height:400px;"></div>\
<button class="btn btn-secondary btn-default btn-xs expand-all hidden" style="margin: 10px;">'+__('Expand All')+'</button>\ <button class="btn btn-secondary btn-default btn-xs expand-all hidden" style="margin: 10px;">'+__('Expand All')+'</button>\
@@ -103,7 +103,7 @@ frappe.views.QueryReport = Class.extend({
doctype: "Report", doctype: "Report",
name: me.report_name name: me.report_name
}; };
frappe.set_route("user-permissions");
frappe.set_route('List', 'User Permission');
}, true); }, true);
} }




+ 2
- 2
frappe/public/js/frappe/views/reports/reportview.js Просмотреть файл

@@ -821,12 +821,12 @@ frappe.views.ReportView = frappe.ui.BaseList.extend({
make_user_permissions: function() { make_user_permissions: function() {
var me = this; var me = this;
if(this.docname && frappe.model.can_set_user_permissions("Report")) { if(this.docname && frappe.model.can_set_user_permissions("Report")) {
this.page.add_menu_item(__("User Permissions Manager"), function() {
this.page.add_menu_item(__("User Permissions"), function() {
frappe.route_options = { frappe.route_options = {
doctype: "Report", doctype: "Report",
name: me.docname name: me.docname
}; };
frappe.set_route("user-permissions");
frappe.set_route('List', 'User Permission');
}, true); }, true);
} }
}, },


+ 1
- 1
frappe/public/less/desk.less Просмотреть файл

@@ -24,7 +24,7 @@ a[disabled="disabled"] {


.link-btn { .link-btn {
position: absolute; position: absolute;
top: 2px;
top: 3px;
right: 4px; right: 4px;
border-radius: 2px; border-radius: 2px;
padding: 3px; padding: 3px;


+ 0
- 6
frappe/public/less/docs.less Просмотреть файл

@@ -403,10 +403,4 @@ a.edit, a.edit:hover, a.edit:focus, a.edit:visited, .edit-container .icon {
} }
} }


.screenshot {
border: 2px solid @border-color;
box-shadow: 1px 1px 7px rgba(0,0,0,0.15);
margin: 15px 0px;
max-width: 100%;
}



+ 4
- 105
frappe/public/less/form.less Просмотреть файл

@@ -708,10 +708,10 @@ select.form-control {
} }


.password-strength-indicator { .password-strength-indicator {
float: right;
padding: 15px;
margin-top: -41px;
margin-right: -7px;
float: right;
padding: 15px;
margin-top: -41px;
margin-right: -7px;
} }


.password-strength-message { .password-strength-message {
@@ -856,7 +856,6 @@ select.form-control {
} }


/* goals */ /* goals */

.goals-page-container { .goals-page-container {
background-color: #fafbfc; background-color: #fafbfc;
padding-top: 1px; padding-top: 1px;
@@ -870,106 +869,6 @@ select.form-control {
} }
} }


.graph-container {
.graphics {
margin-top: 10px;
padding: 10px 0px;
}

.stats-group {
display: flex;
justify-content: space-around;
flex: 1;
}

.stats-container {
display: flex;
justify-content: space-around;

.stats {
padding-bottom: 15px;
}

.stats-title {
color: #8D99A6;
}
.stats-value {
font-size: 20px;
font-weight: 300;
}
.stats-description {
font-size: 12px;
color: #8D99A6;
}
.graph-data .stats-value {
color: #98d85b;
}
}
}

.bar-graph, .line-graph {

.axis {
font-size: 10px;
fill: #6a737d;

line {
stroke: rgba(27,31,35,0.1);
}
}
}

.data-points {
circle {
fill: #28a745;
stroke: #fff;
stroke-width: 2;
}

g.mini {
fill: #98d85b;
}

path {
fill: none;
stroke: #28a745;
stroke-opacity: 1;
stroke-width: 2px;
}
}

.line-graph {
.path {
fill: none;
stroke: #28a745;
stroke-opacity: 1;
stroke-width: 2px;
}
}

line.dashed {
stroke-dasharray: 5,3;
}

.tick {
&.x-axis-label {
display: block;
}

.specific-value {
text-anchor: start;
}

.y-value-text {
text-anchor: end;
}

.x-value-text {
text-anchor: middle;
}
}


body[data-route^="Form/Communication"] textarea[data-fieldname="subject"] { body[data-route^="Form/Communication"] textarea[data-fieldname="subject"] {
height: 80px !important; height: 80px !important;
} }


+ 319
- 0
frappe/public/less/graphs.less Просмотреть файл

@@ -0,0 +1,319 @@

/* graphs */
.graph-container {
.graph-focus-margin {
margin: 0px 5%;
}

.graph-graphics {
margin-top: 10px;
padding: 10px 0px;
position: relative;
}

.graph-stats-group {
display: flex;
justify-content: space-around;
flex: 1;
}

.graph-stats-container {
display: flex;
justify-content: space-around;
padding-top: 10px;

.stats {
padding-bottom: 15px;
}

// Custom (impactified) stats style
.stats-title {
color: #8D99A6;
}
.stats-value {
font-size: 20px;
font-weight: 300;
}
.stats-description {
font-size: 12px;
color: #8D99A6;
}
.graph-data .stats-value {
color: #98d85b;
}
}

.bar-graph, .line-graph {

// baselines
.axis {
font-size: 10px;
fill: #6a737d;

line {
stroke: rgba(27,31,35,0.1);
}
}
}

.percentage-graph {
margin-top: 35px;

.progress {
margin-bottom: 0px;
}
}

.graph-data-points {
circle {
// fill: #28a745;
stroke: #fff;
stroke-width: 2;
}

g.mini {
// fill: #98d85b;
}

path {
fill: none;
// stroke: #28a745;
stroke-opacity: 1;
stroke-width: 2px;
}
}

line.graph-dashed {
stroke-dasharray: 5,3;
}

.tick {
&.x-axis-label {
display: block;
}

.specific-value {
text-anchor: start;
}

.y-value-text {
text-anchor: end;
}

.x-value-text {
text-anchor: middle;
}
}

.graph-svg-tip {
position: absolute;
z-index: 99999;
padding: 10px;
font-size: 12px;
color: #959da5;
text-align: center;
background: rgba(0,0,0,0.8);
border-radius: 3px;

&.comparison {
padding: 0;
text-align: left;
pointer-events: none;

.title {
display: block;
padding: 10px;
margin: 0;
font-weight: 600;
line-height: 1;
pointer-events: none;
}

ul {
margin: 0;
white-space: nowrap;
list-style: none;
}

li {
display: inline-block;
padding: 5px 10px;
}
}

ul, ol {
padding-left: 0;
display: flex;
}

ul.data-point-list li {
min-width: 90px;
flex: 1;
}

strong {
color: #dfe2e5;
}

&::after {
position: absolute;
bottom: -10px;
left: 50%;
width: 5px;
height: 5px;
margin: 0 0 0 -5px;
content: " ";
border: 5px solid transparent;
border-top-color: rgba(0,0,0,0.8);
}
}

.stroke.grey {
stroke: #F0F4F7;
}
.stroke.blue {
stroke: #5e64ff;
}
.stroke.red {
stroke: #ff5858;
}
.stroke.light-green {
stroke: #98d85b;
}
.stroke.green {
stroke: #28a745;
}
.stroke.orange {
stroke: #ffa00a;
}
.stroke.purple {
stroke: #743ee2;
}
.stroke.darkgrey {
stroke: #b8c2cc;
}
.stroke.black {
stroke: #36414C;
}
.stroke.yellow {
stroke: #FEEF72;
}
.stroke.light-blue {
stroke: #7CD6FD;
}
.stroke.lightblue {
stroke: #7CD6FD;
}

.fill.grey {
fill: #F0F4F7;
}
.fill.blue {
fill: #5e64ff;
}
.fill.red {
fill: #ff5858;
}
.fill.light-green {
fill: #98d85b;
}
.fill.green {
fill: #28a745;
}
.fill.orange {
fill: #ffa00a;
}
.fill.purple {
fill: #743ee2;
}
.fill.darkgrey {
fill: #b8c2cc;
}
.fill.black {
fill: #36414C;
}
.fill.yellow {
fill: #FEEF72;
}
.fill.light-blue {
fill: #7CD6FD;
}
.fill.lightblue {
fill: #7CD6FD;
}

.background.grey {
background: #F0F4F7;
}
.background.blue {
background: #5e64ff;
}
.background.red {
background: #ff5858;
}
.background.light-green {
background: #98d85b;
}
.background.green {
background: #28a745;
}
.background.orange {
background: #ffa00a;
}
.background.purple {
background: #743ee2;
}
.background.darkgrey {
background: #b8c2cc;
}
.background.black {
background: #36414C;
}
.background.yellow {
background: #FEEF72;
}
.background.light-blue {
background: #7CD6FD;
}
.background.lightblue {
background: #7CD6FD;
}

.border-top.grey {
border-top: 3px solid #F0F4F7;
}
.border-top.blue {
border-top: 3px solid #5e64ff;
}
.border-top.red {
border-top: 3px solid #ff5858;
}
.border-top.light-green {
border-top: 3px solid #98d85b;
}
.border-top.green {
border-top: 3px solid #28a745;
}
.border-top.orange {
border-top: 3px solid #ffa00a;
}
.border-top.purple {
border-top: 3px solid #743ee2;
}
.border-top.darkgrey {
border-top: 3px solid #b8c2cc;
}
.border-top.black {
border-top: 3px solid #36414C;
}
.border-top.yellow {
border-top: 3px solid #FEEF72;
}
.border-top.light-blue {
border-top: 3px solid #7CD6FD;
}
.border-top.lightblue {
border-top: 3px solid #7CD6FD;
}

}

+ 34
- 96
frappe/public/less/website.less Просмотреть файл

@@ -29,6 +29,10 @@ h1, h2, h3, h4, h5, h6 {


} }


li {
line-height: 1.7em;
}



.navbar-brand { .navbar-brand {
max-width: none; max-width: none;
@@ -120,6 +124,10 @@ h1, h2, h3, h4, h5, h6 {
border-top: 1px solid @light-border-color; border-top: 1px solid @light-border-color;
} }


.page_content {
padding-bottom: 30px;
}

.carousel-control .icon { .carousel-control .icon {
position: absolute; position: absolute;
top: 50%; top: 50%;
@@ -235,7 +243,7 @@ fieldset {


.sidebar-item { .sidebar-item {
margin: 0px; margin: 0px;
padding: 12px 0px;
padding-bottom: 12px;
border: none; border: none;
color: @text-muted; color: @text-muted;
font-size: 12px; font-size: 12px;
@@ -246,25 +254,17 @@ fieldset {


} }


.sidebar-item:first-child {
padding-top: 10px;
}
.sidebar-item:last-child {
padding-bottom: 10px;
}

.sidebar-item a { .sidebar-item a {
color: @text-muted;
color: @text-color !important;
} }


.sidebar-item a.active { .sidebar-item a.active {
color: @text-color !important; color: @text-color !important;
font-weight:500 !important;
font-weight: 500 !important;
} }


.sidebar-items { .sidebar-items {
// margin-top:30px; // margin-top:30px;
margin-top: -10px;
margin-bottom:30px; margin-bottom:30px;
.title{ .title{
font-size: 14px; font-size: 14px;
@@ -331,92 +331,30 @@ fieldset {
border-bottom: 0px; border-bottom: 0px;
} }


// .web-list-item:hover {
// background: @panel-bg;
// }

.blog-info {
text-align:center;
margin-top: 30px;
}

.post-description {
padding-bottom: 8px;
p {
margin-bottom: 8px;
}
}


.blog-footer {
padding: 5px 15px;
border-top: 1px solid @light-border-color;
margin: 0px -15px -20px -15px;
}

.blog-list-content .website-list .result {
.website-list .result {
border: 0px; border: 0px;
} }


.blog-list-content .web-list-item:hover {
.web-list-item:hover {
background: transparent; background: transparent;
} }


.blog-category {
letter-spacing: 0.5px;
text-align: center;
margin-bottom: 30px;
}

.author {
letter-spacing: 0.5px;
border-bottom: 1px solid @light-border-color;
padding-bottom:30px;
}

.blogger {
padding-top: 0px;
padding-bottom: 50px;
}

.blog-dot:before{
.spacer-dot:before{
padding-right:8px; padding-right:8px;
padding-left:8px; padding-left:8px;
content:"\2022"; content:"\2022";
} }


.blog-list-item {
margin-top: 30px;
margin-bottom: 30px;

.blog-header {
font-size: 1.6em;
}
}

.blog-header {
font-weight: 700;
font-size: 2em;
}

.add-comment-section { .add-comment-section {
padding-bottom: 30px; padding-bottom: 30px;
} }


.blog-comments,
.help-article-comments {
}
.blog-comments {
position: relative;
border-top: 1px solid @border-color;
}

.blog-comment-row {
.comment-row {
margin: 0px -15px; margin: 0px -15px;
padding: 15px; padding: 15px;
} }


.blog-comment-row:last-child {
.comment-row:last-child {
margin-bottom: 30px; margin-bottom: 30px;
border-bottom: 0px; border-bottom: 0px;
} }
@@ -540,7 +478,7 @@ a.active {
} }


.sidebar-block, .page-content { .sidebar-block, .page-content {
padding-top: 50px;
padding-top: 30px;
padding-bottom: 50px; padding-bottom: 50px;
} }
.your-account-info { .your-account-info {
@@ -584,21 +522,6 @@ li.footer-child-item {
margin: 15px 0px; margin: 15px 0px;
} }


.blog-info {
text-align:center;
margin-top: 30px;
}

.blog-text {
padding-top: 50px;
padding-bottom: 50px;
font-size: 18px;
line-height: 1.5;
p {
margin-bottom: 30px;
}
}

.comment-view { .comment-view {
padding-bottom: 30px; padding-bottom: 30px;
} }
@@ -643,7 +566,7 @@ li.footer-child-item {
} }


.vert-line {overflow:hidden;} .vert-line {overflow:hidden;}
.vert-line>div+div{border-left:1px solid @light-border-color;}
.vert-line>div+div{border-left:1px solid @border-color;}
.vert-line>div{ .vert-line>div{
padding-bottom:2000px; margin-bottom:-2000px;} padding-bottom:2000px; margin-bottom:-2000px;}


@@ -717,4 +640,19 @@ li.footer-child-item {
border: 1px solid @border-color; border: 1px solid @border-color;
padding: 10px; padding: 10px;
border-radius: 4px; border-radius: 4px;
}
}

// docs
.docfields {
pre {
background-color: transparent;
border: none;
}
}

.screenshot {
border: 1px solid @border-color;
box-shadow: 1px 1px 7px rgba(0,0,0,0.15);
margin: 15px 0px;
max-width: 100%;
}

+ 4
- 4
frappe/templates/autodoc/dev_home.html Просмотреть файл

@@ -7,9 +7,9 @@
{{ source_link(app, app.name, True) }} {{ source_link(app, app.name, True) }}
</div> </div>


<table class="table table-bordered">
<table class="table table-bordered" style='max-width: 400px;'>
<tr> <tr>
<td style="width: 20%">
<td style="width: 40%">
App Name App Name
</td> </td>
<td> <td>
@@ -37,10 +37,10 @@
<h3>Contents</h3> <h3>Contents</h3>
<ul> <ul>
<li> <li>
<a href="models">Models (DocTypes)</a>
<a href="/docs/current/models">Models (DocTypes)</a>
</li> </li>
<li> <li>
<a href="api">Server-side API</a>
<a href="/docs/current/api">Server-side API</a>
</li> </li>
</ul> </ul>




+ 7
- 49
frappe/templates/autodoc/docs_home.html Просмотреть файл

@@ -5,57 +5,15 @@
{% if app.style %} {{ app.style }} {% endif %} {% if app.style %} {{ app.style }} {% endif %}
</style> </style>


<!-- start-hero -->
<div class="splash">
<div class="container">
<div class="col-sm-10 col-sm-offset-1">
<div class="jumbotron">
<h1>{{ app.headline }}</h1>
<p>{{ app.sub_heading }}</p>
</div>
<div class="section" style="padding-top: 0px; margin-top: -30px;">
<div class="fake-browser-frame">
<img class="img-responsive browser-image feature-image"
src="assets/img/home.png">
</div>
</div>
</div>
</div>
</div>
<!-- end-hero -->

{% if app.long_description %}
<div class="container">
<div class="col-sm-10 col-sm-offset-1">
<div class="section">
{{ app.long_description|markdown }}
</div>
</div>
</div>
{% if app.headline %}
<h1>{{ app.headline }}</h1>
{% endif %}
{% if app.sub_heading %}
<h3 style='margin-top: 10px; margin-bottom: 30px;'>{{ app.sub_heading }}</h3>
{% endif %} {% endif %}


{% if not app.hide_install %}
<div class="container">
<div class="col-sm-10 col-sm-offset-1">
<div class="section" id="install">
<h2>Install</h2>
<h4>From your site</h4>
<p>To install this app, login to your site and click on "Installer". Search for <b>{{ app.title }}</b> and click on "Install"</p>
<h4>Using Bench</h4>
<p>Go to your bench folder and setup the new app</p>
<pre><code class="sh">$ bench get-app {{app.name}} {{app.source_link}}
$ bench new-site testsite
$ bench --site testsite install-app {{app.name}}</code></pre>
<p>Login to your site to configure the app.</p>
<p><a href="install.html">Detailed Installation Steps</a></p>
</div>
<div class="section">
<h2>Author</h2>

<p>{{ app.publisher }} ({{ app.email }})</p>
</div>
</div>
</div>
{% if app.long_description %}
{{ app.long_description|markdown }}
{% endif %} {% endif %}


<!-- autodoc --> <!-- autodoc -->


+ 3
- 1
frappe/templates/autodoc/doctype.html Просмотреть файл

@@ -1,10 +1,12 @@
<!-- title: {{ doctype }} --> <!-- title: {{ doctype }} -->
<!-- add-breadcrumbs -->


{% from "templates/autodoc/macros.html" import automodule, version, {% from "templates/autodoc/macros.html" import automodule, version,
source_link, doctype_link %} source_link, doctype_link %}
{% set doc = frappe.get_doc("DocType", doctype) %} {% set doc = frappe.get_doc("DocType", doctype) %}
{% set controller = autodoc.get_controller(doctype) %} {% set controller = autodoc.get_controller(doctype) %}


<h1>{{ doctype }}</h1>
<div class="dev-header"> <div class="dev-header">
{{ version(doctype) }} {{ version(doctype) }}
{{ source_link(app, app.name + "/" + scrub(doc.module) {{ source_link(app, app.name + "/" + scrub(doc.module)
@@ -22,7 +24,7 @@


<h3>Fields</h3> <h3>Fields</h3>


<table class="table table-bordered">
<table class="table table-bordered docfields">
<thead> <thead>
<tr> <tr>
<th style="width: 5%">Sr</th> <th style="width: 5%">Sr</th>


+ 1
- 1
frappe/templates/autodoc/macros.html Просмотреть файл

@@ -59,7 +59,7 @@
{% macro doctype_link(app, doctype) %} {% macro doctype_link(app, doctype) %}
{% set module = frappe.db.get_value("DocType", doctype, "module") %} {% set module = frappe.db.get_value("DocType", doctype, "module") %}
{% if doctype and module %} {% if doctype and module %}
<a href="{{ app.docs_base_url }}/{{ app.docs_version }}/models/{{
<a href="{{ docs_base_url }}/{{ app.docs_version }}/models/{{
scrub(module) }}/{{ scrub(doctype) }}">{{ doctype }}</a> scrub(module) }}/{{ scrub(doctype) }}">{{ doctype }}</a>
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}

+ 3
- 0
frappe/templates/autodoc/models_home.html Просмотреть файл

@@ -1,6 +1,9 @@
<!-- title: {{ app.title }}: Models (DocTypes) --> <!-- title: {{ app.title }}: Models (DocTypes) -->
<!-- add-breadcrumbs -->


{% from "templates/autodoc/macros.html" import source_link, version %} {% from "templates/autodoc/macros.html" import source_link, version %}

<h1>{{ app.title }}: Models (DocTypes)</h1>
<div class="dev-header"> <div class="dev-header">
{{ version(app.name) }} {{ version(app.name) }}
{{ source_link(app, app.name, True) }} {{ source_link(app, app.name, True) }}


+ 3
- 0
frappe/templates/autodoc/module_home.html Просмотреть файл

@@ -1,6 +1,9 @@
<!-- title: Module {{ name }} --> <!-- title: Module {{ name }} -->
<!-- add-breadcrumbs -->


{% from "templates/autodoc/macros.html" import source_link, version %} {% from "templates/autodoc/macros.html" import source_link, version %}

<h1>Module {{ name }}</h1>
<div class="dev-header"> <div class="dev-header">
{{ version(app.name) }} {{ version(app.name) }}
{{ source_link(app, app.name + "/" + scrub(name), True) }} {{ source_link(app, app.name + "/" + scrub(name), True) }}


+ 3
- 0
frappe/templates/autodoc/package_index.html Просмотреть файл

@@ -1,6 +1,9 @@
<!-- title: {{ title }} --> <!-- title: {{ title }} -->
<!-- add-breadcrumbs -->


{% from "templates/autodoc/macros.html" import source_link, version %} {% from "templates/autodoc/macros.html" import source_link, version %}

<h1>{{ title }}</h1>
<div class="dev-header"> <div class="dev-header">
{{ version(app.name) }} {{ version(app.name) }}
{{ source_link(app, title, True) }} {{ source_link(app, title, True) }}


+ 3
- 0
frappe/templates/autodoc/pymodule.html Просмотреть файл

@@ -1,7 +1,10 @@
<!-- title: {{ name }} --> <!-- title: {{ name }} -->
<!-- add-breadcrumbs -->


{%- from "templates/autodoc/macros.html" import automodule, source_link, {%- from "templates/autodoc/macros.html" import automodule, source_link,
version -%} version -%}

<h1>{{ name }}</h1>
<div class="dev-header"> <div class="dev-header">
{{ version(app.name) }} {{ version(app.name) }}
{{ source_link(app, full_module_name.replace(".", "/") + ".py") }} {{ source_link(app, full_module_name.replace(".", "/") + ".py") }}


+ 19
- 0
frappe/templates/includes/blog/blog.html Просмотреть файл

@@ -5,6 +5,25 @@
{% block hero %}{% endblock %} {% block hero %}{% endblock %}


{% block page_content %} {% block page_content %}
<style>
.blog-list-content {
border: 0px;
background: transparent;
}
.blog-list-item {
margin-top: 30px;
margin-bottom: 30px;
}
.blog-list-item .blog-header {
font-size: 1.6em;
}
.post-description {
padding-bottom: 8px;
}
.post-description p {
margin-bottom: 8px;
}
</style>
<!-- no-header --> <!-- no-header -->
<!-- no-breadcrumbs --> <!-- no-breadcrumbs -->
<div class="blog-list-content"> <div class="blog-list-content">


+ 1
- 1
frappe/templates/includes/comments/comment.html Просмотреть файл

@@ -1,4 +1,4 @@
<div class="blog-comment-row">
<div class="comment-row">
<div class="inline-block" style="vertical-align: top"> <div class="inline-block" style="vertical-align: top">
<div class="avatar avatar-medium" style="margin-top: 11px;"> <div class="avatar avatar-medium" style="margin-top: 11px;">
<img itemprop="thumbnailUrl" src="{{ frappe.get_gravatar(comment.sender) }}" /> <img itemprop="thumbnailUrl" src="{{ frappe.get_gravatar(comment.sender) }}" />


+ 3
- 1
frappe/templates/includes/full_index.html Просмотреть файл

@@ -2,10 +2,12 @@
<ol> <ol>
{% for item in children_map[route] %} {% for item in children_map[route] %}
<li> <li>
<a href="{{ url_prefix }}{{ item.route }}{{ item.extn or "" }}">{{ item.title }}</a>
<a href="{{ url_prefix }}{{ item.route }}">{{ item.title }}</a>
{#
{% if children_map[item.route] %} {% if children_map[item.route] %}
{{ make_item_list(item.route, children_map) }} {{ make_item_list(item.route, children_map) }}
{% endif %} {% endif %}
#}
</li> </li>
{% endfor %} {% endfor %}
</ol> </ol>


+ 1
- 1
frappe/templates/includes/web_sidebar.html Просмотреть файл

@@ -22,7 +22,7 @@
{% endif %} {% endif %}
{% for item in sidebar_items -%} {% for item in sidebar_items -%}
<li class="sidebar-item"> <li class="sidebar-item">
<a href="{{ item.route }}" class="text-muted {{ 'active' if pathname==item.route else '' }}"
<a href="{{ item.route }}" class="gray {{ 'active' if pathname==item.route else '' }}"
{% if item.target %}target="{{ item.target }}"{% endif %}> {% if item.target %}target="{{ item.target }}"{% endif %}>
{{ _(item.title or item.label) }} {{ _(item.title or item.label) }}
</a> </a>


+ 9
- 6
frappe/templates/web.html Просмотреть файл

@@ -2,8 +2,10 @@


{% block hero %}{% endblock %} {% block hero %}{% endblock %}
{% block content %} {% block content %}
<div class="page-container" id="page-{{ name or route }}" data-path="{{ pathname }}"
{% if page_or_generator=="Generator" %}data-doctype="{{ doctype }}"{% endif %}>
<div class="page-container" id="page-{{ name or route }}"
data-path="{{ pathname }}"
{%- if page_or_generator=="Generator" %}
data-doctype="{{ doctype }}"{% endif %}>
<div class="row {% if show_sidebar %}vert-line{% endif %}"> <div class="row {% if show_sidebar %}vert-line{% endif %}">
{% if show_sidebar %} {% if show_sidebar %}
<div class="col-sm-3 sidebar-block hidden-xs"> <div class="col-sm-3 sidebar-block hidden-xs">
@@ -29,7 +31,8 @@
{% if show_search %} {% if show_search %}
<div class="page-search-block"> <div class="page-search-block">
{% block search %} {% block search %}
{% include 'templates/includes/search_box.html' %}
{% include
'templates/includes/search_box.html' %}
{% endblock %} {% endblock %}
</div> </div>
{% endif %} {% endif %}
@@ -41,9 +44,9 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div>
<div class="page_content">
{%- block page_content -%}{%- endblock -%}
<div class="page_content">
{%- block page_content -%}{%- endblock -%}
</div>
</div> </div>
</div> </div>
</div> </div>


+ 2
- 2
frappe/test_runner.py Просмотреть файл

@@ -266,12 +266,12 @@ def make_test_records_for_doctype(doctype, verbose=0, force=False):
frappe.local.test_objects[doctype] += test_module._make_test_records(verbose) frappe.local.test_objects[doctype] += test_module._make_test_records(verbose)


elif hasattr(test_module, "test_records"): elif hasattr(test_module, "test_records"):
frappe.local.test_objects[doctype] += make_test_objects(doctype, test_module.test_records, verbose)
frappe.local.test_objects[doctype] += make_test_objects(doctype, test_module.test_records, verbose, force)


else: else:
test_records = frappe.get_test_records(doctype) test_records = frappe.get_test_records(doctype)
if test_records: if test_records:
frappe.local.test_objects[doctype] += make_test_objects(doctype, test_records, verbose)
frappe.local.test_objects[doctype] += make_test_objects(doctype, test_records, verbose, force)


elif verbose: elif verbose:
print_mandatory_fields(doctype) print_mandatory_fields(doctype)


+ 1
- 1
frappe/tests/test_goal.py Просмотреть файл

@@ -31,4 +31,4 @@ class TestGoal(unittest.TestCase):
frappe.db.set_value('Event', docname, 'description', 1) frappe.db.set_value('Event', docname, 'description', 1)
data = get_monthly_goal_graph_data('Test', 'Event', docname, 'description', 'description', 'description', data = get_monthly_goal_graph_data('Test', 'Event', docname, 'description', 'description', 'description',
'Event', '', 'description', 'creation', 'starts_on = "2014-01-01"', 'count') 'Event', '', 'description', 'creation', 'starts_on = "2014-01-01"', 'count')
self.assertEquals(float(data['y_values'][-1]), 1)
self.assertEquals(float(data['y'][0]['values'][-1]), 1)

+ 77
- 31
frappe/tests/test_permissions.py Просмотреть файл

@@ -9,9 +9,11 @@ import frappe.defaults
import unittest import unittest
import json import json
import frappe.model.meta import frappe.model.meta
from frappe.core.page.user_permissions.user_permissions import add, remove, get_permissions
from frappe.permissions import clear_user_permissions_for_doctype, get_doc_permissions
from frappe.permissions import (add_user_permission, remove_user_permission,
clear_user_permissions_for_doctype, get_doc_permissions, add_permission,
get_valid_perms)
from frappe.core.page.permission_manager.permission_manager import update, reset from frappe.core.page.permission_manager.permission_manager import update, reset
from frappe.test_runner import make_test_records_for_doctype


test_records = frappe.get_test_records('Blog Post') test_records = frappe.get_test_records('Blog Post')


@@ -24,6 +26,7 @@ class TestPermissions(unittest.TestCase):


user = frappe.get_doc("User", "test1@example.com") user = frappe.get_doc("User", "test1@example.com")
user.add_roles("Website Manager") user.add_roles("Website Manager")
user.add_roles("System Manager")


user = frappe.get_doc("User", "test2@example.com") user = frappe.get_doc("User", "test2@example.com")
user.add_roles("Blogger") user.add_roles("Blogger")
@@ -36,6 +39,8 @@ class TestPermissions(unittest.TestCase):
reset('Contact') reset('Contact')
reset('Salutation') reset('Salutation')


frappe.db.sql('delete from `tabUser Permission`')

self.set_ignore_user_permissions_if_missing(0) self.set_ignore_user_permissions_if_missing(0)


frappe.set_user("test1@example.com") frappe.set_user("test1@example.com")
@@ -78,7 +83,7 @@ class TestPermissions(unittest.TestCase):
def test_user_permissions_in_doc(self): def test_user_permissions_in_doc(self):
self.set_user_permission_doctypes(["Blog Category"]) self.set_user_permission_doctypes(["Blog Category"])


frappe.permissions.add_user_permission("Blog Category", "_Test Blog Category 1",
add_user_permission("Blog Category", "_Test Blog Category 1",
"test2@example.com") "test2@example.com")


frappe.set_user("test2@example.com") frappe.set_user("test2@example.com")
@@ -94,7 +99,7 @@ class TestPermissions(unittest.TestCase):
def test_user_permissions_in_report(self): def test_user_permissions_in_report(self):
self.set_user_permission_doctypes(["Blog Category"]) self.set_user_permission_doctypes(["Blog Category"])


frappe.permissions.add_user_permission("Blog Category", "_Test Blog Category 1", "test2@example.com")
add_user_permission("Blog Category", "_Test Blog Category 1", "test2@example.com")


frappe.set_user("test2@example.com") frappe.set_user("test2@example.com")
names = [d.name for d in frappe.get_list("Blog Post", fields=["name", "blog_category"])] names = [d.name for d in frappe.get_list("Blog Post", fields=["name", "blog_category"])]
@@ -103,7 +108,7 @@ class TestPermissions(unittest.TestCase):
self.assertFalse("-test-blog-post" in names) self.assertFalse("-test-blog-post" in names)


def test_default_values(self): def test_default_values(self):
frappe.permissions.add_user_permission("Blog Category", "_Test Blog Category 1", "test2@example.com")
add_user_permission("Blog Category", "_Test Blog Category 1", "test2@example.com")


frappe.set_user("test2@example.com") frappe.set_user("test2@example.com")
doc = frappe.new_doc("Blog Post") doc = frappe.new_doc("Blog Post")
@@ -139,14 +144,14 @@ class TestPermissions(unittest.TestCase):


def test_set_user_permissions(self): def test_set_user_permissions(self):
frappe.set_user("test1@example.com") frappe.set_user("test1@example.com")
add("test2@example.com", "Blog Post", "-test-blog-post")
add_user_permission("Blog Post", "-test-blog-post", "test2@example.com")


def test_not_allowed_to_set_user_permissions(self): def test_not_allowed_to_set_user_permissions(self):
frappe.set_user("test2@example.com") frappe.set_user("test2@example.com")


# this user can't add user permissions # this user can't add user permissions
self.assertRaises(frappe.PermissionError, add,
"test2@example.com", "Blog Post", "-test-blog-post")
self.assertRaises(frappe.PermissionError, add_user_permission,
"Blog Post", "-test-blog-post", "test2@example.com")


def test_read_if_explicit_user_permissions_are_set(self): def test_read_if_explicit_user_permissions_are_set(self):
self.set_user_permission_doctypes(["Blog Post"]) self.set_user_permission_doctypes(["Blog Post"])
@@ -165,13 +170,12 @@ class TestPermissions(unittest.TestCase):


def test_not_allowed_to_remove_user_permissions(self): def test_not_allowed_to_remove_user_permissions(self):
self.test_set_user_permissions() self.test_set_user_permissions()
defname = get_permissions("test2@example.com", "Blog Post", "-test-blog-post")[0].name


frappe.set_user("test2@example.com") frappe.set_user("test2@example.com")


# user cannot remove their own user permissions # user cannot remove their own user permissions
self.assertRaises(frappe.PermissionError, remove,
"test2@example.com", defname, "Blog Post", "-test-blog-post")
self.assertRaises(frappe.PermissionError, remove_user_permission,
"Blog Post", "-test-blog-post", "test2@example.com")


def test_user_permissions_based_on_blogger(self): def test_user_permissions_based_on_blogger(self):
frappe.set_user("test2@example.com") frappe.set_user("test2@example.com")
@@ -181,7 +185,7 @@ class TestPermissions(unittest.TestCase):
self.set_user_permission_doctypes(["Blog Post"]) self.set_user_permission_doctypes(["Blog Post"])


frappe.set_user("test1@example.com") frappe.set_user("test1@example.com")
add("test2@example.com", "Blog Post", "-test-blog-post")
add_user_permission("Blog Post", "-test-blog-post", "test2@example.com")


frappe.set_user("test2@example.com") frappe.set_user("test2@example.com")
doc = frappe.get_doc("Blog Post", "-test-blog-post-1") doc = frappe.get_doc("Blog Post", "-test-blog-post-1")
@@ -199,9 +203,9 @@ class TestPermissions(unittest.TestCase):
blog_post.get_field("title").set_only_once = 0 blog_post.get_field("title").set_only_once = 0


def test_user_permission_doctypes(self): def test_user_permission_doctypes(self):
frappe.permissions.add_user_permission("Blog Category", "_Test Blog Category 1",
add_user_permission("Blog Category", "_Test Blog Category 1",
"test2@example.com") "test2@example.com")
frappe.permissions.add_user_permission("Blogger", "_Test Blogger 1",
add_user_permission("Blogger", "_Test Blogger 1",
"test2@example.com") "test2@example.com")


frappe.set_user("test2@example.com") frappe.set_user("test2@example.com")
@@ -221,9 +225,9 @@ class TestPermissions(unittest.TestCase):
def if_owner_setup(self): def if_owner_setup(self):
update('Blog Post', 'Blogger', 0, 'if_owner', 1) update('Blog Post', 'Blogger', 0, 'if_owner', 1)


frappe.permissions.add_user_permission("Blog Category", "_Test Blog Category 1",
add_user_permission("Blog Category", "_Test Blog Category 1",
"test2@example.com") "test2@example.com")
frappe.permissions.add_user_permission("Blogger", "_Test Blogger 1",
add_user_permission("Blogger", "_Test Blogger 1",
"test2@example.com") "test2@example.com")


update('Blog Post', 'Blogger', 0, 'user_permission_doctypes', json.dumps(["Blog Category"])) update('Blog Post', 'Blogger', 0, 'user_permission_doctypes', json.dumps(["Blog Category"]))
@@ -231,11 +235,13 @@ class TestPermissions(unittest.TestCase):
frappe.model.meta.clear_cache("Blog Post") frappe.model.meta.clear_cache("Blog Post")


def set_user_permission_doctypes(self, user_permission_doctypes): def set_user_permission_doctypes(self, user_permission_doctypes):
set_user_permission_doctypes(doctype="Blog Post", role="Blogger",
set_user_permission_doctypes(["Blog Post"], role="Blogger",
apply_user_permissions=1, user_permission_doctypes=user_permission_doctypes) apply_user_permissions=1, user_permission_doctypes=user_permission_doctypes)


def test_insert_if_owner_with_user_permissions(self): def test_insert_if_owner_with_user_permissions(self):
"""If `If Owner` is checked for a Role, check if that document is allowed to be read, updated, submitted, etc. except be created, even if the document is restricted based on User Permissions.""" """If `If Owner` is checked for a Role, check if that document is allowed to be read, updated, submitted, etc. except be created, even if the document is restricted based on User Permissions."""
frappe.delete_doc('Blog Post', '-test-blog-post-title')

self.set_user_permission_doctypes(["Blog Category"]) self.set_user_permission_doctypes(["Blog Category"])
self.if_owner_setup() self.if_owner_setup()


@@ -252,7 +258,7 @@ class TestPermissions(unittest.TestCase):
self.assertRaises(frappe.PermissionError, doc.insert) self.assertRaises(frappe.PermissionError, doc.insert)


frappe.set_user("Administrator") frappe.set_user("Administrator")
frappe.permissions.add_user_permission("Blog Category", "_Test Blog Category",
add_user_permission("Blog Category", "_Test Blog Category",
"test2@example.com") "test2@example.com")


frappe.set_user("test2@example.com") frappe.set_user("test2@example.com")
@@ -273,8 +279,8 @@ class TestPermissions(unittest.TestCase):
self.set_user_permission_doctypes(['Blog Category', 'Blog Post', 'Blogger']) self.set_user_permission_doctypes(['Blog Category', 'Blog Post', 'Blogger'])


frappe.set_user("Administrator") frappe.set_user("Administrator")
frappe.permissions.add_user_permission("Blog Category", "_Test Blog Category",
"test2@example.com")
# add_user_permission("Blog Category", "_Test Blog Category",
# "test2@example.com")
frappe.set_user("test2@example.com") frappe.set_user("test2@example.com")


doc = frappe.get_doc({ doc = frappe.get_doc({
@@ -294,33 +300,73 @@ class TestPermissions(unittest.TestCase):
self.assertTrue(doc.has_permission("write")) self.assertTrue(doc.has_permission("write"))


def test_strict_user_permissions(self): def test_strict_user_permissions(self):
"""If `Strict User Permissions` is checked in System Settings, show records even if User Permissions are missing for a linked doctype"""
set_user_permission_doctypes(doctype="Contact", role="Sales User",
"""If `Strict User Permissions` is checked in System Settings,
show records even if User Permissions are missing for a linked
doctype"""

frappe.set_user("Administrator")
frappe.db.sql('delete from tabContact')
make_test_records_for_doctype('Contact', force=True)

set_user_permission_doctypes("Contact", role="Sales User",
apply_user_permissions=1, user_permission_doctypes=['Salutation']) apply_user_permissions=1, user_permission_doctypes=['Salutation'])
set_user_permission_doctypes(doctype="Salutation", role="All",
set_user_permission_doctypes("Salutation", role="All",
apply_user_permissions=1, user_permission_doctypes=['Salutation']) apply_user_permissions=1, user_permission_doctypes=['Salutation'])


frappe.set_user("Administrator")
frappe.permissions.add_user_permission("Salutation", "Mr", "test3@example.com")
add_user_permission("Salutation", "Mr", "test3@example.com")
self.set_strict_user_permissions(0) self.set_strict_user_permissions(0)


frappe.set_user("test3@example.com") frappe.set_user("test3@example.com")
self.assertEquals(len(frappe.get_list("Contact")),2)
self.assertEquals(len(frappe.get_list("Contact")), 2)


frappe.set_user("Administrator") frappe.set_user("Administrator")
self.set_strict_user_permissions(1) self.set_strict_user_permissions(1)


frappe.set_user("test3@example.com") frappe.set_user("test3@example.com")
self.assertTrue(len(frappe.get_list("Contact")),1)
self.assertTrue(len(frappe.get_list("Contact")), 1)


frappe.set_user("Administrator") frappe.set_user("Administrator")
self.set_strict_user_permissions(0) self.set_strict_user_permissions(0)


def test_automatic_apply_user_permissions(self):
'''Test user permissions are automatically applied when a user permission
is created'''
# create a user
frappe.get_doc(dict(doctype='User', email='test_user_perm@example.com',
first_name='tester')).insert(ignore_if_duplicate=True)
frappe.get_doc(dict(doctype='Role', role_name='Test Role User Perm')
).insert(ignore_if_duplicate=True)

# add a permission for event
add_permission('DocType', 'Test Role User Perm')
frappe.get_doc('User', 'test_user_perm@example.com').add_roles('Test Role User Perm')


def set_user_permission_doctypes(doctype, role, apply_user_permissions, user_permission_doctypes):

# add user permission
add_user_permission('Module Def', 'Core', 'test_user_perm@example.com', True)

# check if user permission is applied in the new role
_perm = None
for perm in get_valid_perms('DocType', 'test_user_perm@example.com'):
if perm.role == 'Test Role User Perm':
_perm = perm

self.assertEqual(_perm.apply_user_permissions, 1)

# restrict by module
self.assertTrue('Module Def' in json.loads(_perm.user_permission_doctypes))


def set_user_permission_doctypes(doctypes, role, apply_user_permissions,
user_permission_doctypes):
user_permission_doctypes = None if not user_permission_doctypes else json.dumps(user_permission_doctypes) user_permission_doctypes = None if not user_permission_doctypes else json.dumps(user_permission_doctypes)


update(doctype, role, 0, 'apply_user_permissions', 1)
update(doctype, role, 0, 'user_permission_doctypes', user_permission_doctypes)
if isinstance(doctypes, basestring):
doctypes = [doctypes]

for doctype in doctypes:
update(doctype, role, 0, 'apply_user_permissions', 1)
update(doctype, role, 0, 'user_permission_doctypes',
user_permission_doctypes)


frappe.clear_cache(doctype=doctype)
frappe.clear_cache(doctype=doctype)

+ 19
- 0
frappe/tests/ui/test_linked_with.js Просмотреть файл

@@ -0,0 +1,19 @@
QUnit.module('form');

QUnit.test("Test Linked With", function(assert) {
assert.expect(2);
const done = assert.async();

frappe.run_serially([
() => frappe.set_route('Form', 'Module Def', 'Contacts'),
() => frappe.tests.click_page_head_item('Menu'),
() => frappe.tests.click_dropdown_item('Links'),
() => frappe.timeout(4),
() => {
assert.equal(cur_dialog.title, 'Linked With', 'Linked with dialog is opened');
const link_tables_count = cur_dialog.$wrapper.find('.list-item-table').length;
assert.equal(link_tables_count, 2, 'Two DocTypes are linked with Contacts');
},
done
]);
});

+ 2
- 1
frappe/tests/ui/tests.txt Просмотреть файл

@@ -7,4 +7,5 @@ frappe/tests/ui/test_kanban/test_kanban_creation.js
frappe/tests/ui/test_kanban/test_kanban_view.js frappe/tests/ui/test_kanban/test_kanban_view.js
frappe/tests/ui/test_kanban/test_kanban_filters.js frappe/tests/ui/test_kanban/test_kanban_filters.js
frappe/tests/ui/test_kanban/test_kanban_column.js frappe/tests/ui/test_kanban/test_kanban_column.js
frappe/core/doctype/report/test_query_report.js
frappe/core/doctype/report/test_query_report.js
frappe/tests/ui/test_linked_with.js

+ 21
- 5
frappe/utils/goal.py Просмотреть файл

@@ -76,15 +76,21 @@ def get_monthly_goal_graph_data(title, doctype, docname, goal_value_field, goal_
month_to_value_dict[current_month_year] = current_month_value month_to_value_dict[current_month_year] = current_month_value


months = [] months = []
months_formatted = []
values = [] values = []
values_formatted = []
for i in xrange(0, 12): for i in xrange(0, 12):
month_value = formatdate(add_months(today(), -i), "MM-yyyy") month_value = formatdate(add_months(today(), -i), "MM-yyyy")
month_word = getdate(month_value).strftime('%b') month_word = getdate(month_value).strftime('%b')
month_year = getdate(month_value).strftime('%B') + ', ' + getdate(month_value).strftime('%Y')
months.insert(0, month_word) months.insert(0, month_word)
months_formatted.insert(0, month_year)
if month_value in month_to_value_dict: if month_value in month_to_value_dict:
values.insert(0, month_to_value_dict[month_value])
val = month_to_value_dict[month_value]
else: else:
values.insert(0, 0)
val = 0
values.insert(0, val)
values_formatted.insert(0, format_value(val, meta.get_field(goal_total_field), doc))


specific_values = [] specific_values = []
summary_values = [ summary_values = [
@@ -119,10 +125,20 @@ def get_monthly_goal_graph_data(title, doctype, docname, goal_value_field, goal_
data = { data = {
'title': title, 'title': title,
# 'subtitle': # 'subtitle':
'y_values': values,
'x_points': months,
'y': [
{
'color': 'green',
'values': values,
'formatted': values_formatted
}
],
'x': {
'values': months,
'formatted': months_formatted
},

'specific_values': specific_values, 'specific_values': specific_values,
'summary_values': summary_values
'summary': summary_values
} }


return data return data

+ 10
- 3
frappe/utils/jinja.py Просмотреть файл

@@ -75,7 +75,7 @@ def get_allowed_functions_for_jenv():
import frappe.utils.data import frappe.utils.data
from frappe.utils.autodoc import automodule, get_version from frappe.utils.autodoc import automodule, get_version
from frappe.model.document import get_controller from frappe.model.document import get_controller
from frappe.website.utils import get_shade
from frappe.website.utils import (get_shade, get_toc, get_next_link)
from frappe.modules import scrub from frappe.modules import scrub
import mimetypes import mimetypes
from html2text import html2text from html2text import html2text
@@ -128,11 +128,16 @@ def get_allowed_functions_for_jenv():
'csrf_token': frappe.local.session.data.csrf_token if getattr(frappe.local, "session", None) else '' 'csrf_token': frappe.local.session.data.csrf_token if getattr(frappe.local, "session", None) else ''
}, },
}, },
'style': {
'border_color': '#d1d8dd'
},
"autodoc": { "autodoc": {
"get_version": get_version, "get_version": get_version,
"automodule": automodule, "automodule": automodule,
"get_controller": get_controller "get_controller": get_controller
}, },
'get_toc': get_toc,
'get_next_link': get_next_link,
"_": frappe._, "_": frappe._,
"get_shade": get_shade, "get_shade": get_shade,
"scrub": scrub, "scrub": scrub,
@@ -159,8 +164,10 @@ def get_jloader():
if frappe.local.flags.in_setup_help: if frappe.local.flags.in_setup_help:
apps = ['frappe'] apps = ['frappe']
else: else:
apps = frappe.local.flags.web_pages_apps or frappe.get_installed_apps(sort=True)
apps.reverse()
apps = frappe.get_hooks('template_apps')
if not apps:
apps = frappe.local.flags.web_pages_apps or frappe.get_installed_apps(sort=True)
apps.reverse()


if not "frappe" in apps: if not "frappe" in apps:
apps.append('frappe') apps.append('frappe')


+ 47
- 6
frappe/utils/setup_docs.py Просмотреть файл

@@ -13,12 +13,12 @@ from frappe.utils import markdown
from six import iteritems from six import iteritems


class setup_docs(object): class setup_docs(object):
def __init__(self, app):
def __init__(self, app, target_app):
"""Generate source templates for models reference and module API """Generate source templates for models reference and module API
and templates at `templates/autodoc` and templates at `templates/autodoc`
""" """
self.app = app self.app = app
self.target_app = target_app


frappe.flags.web_pages_folders = ['docs',] frappe.flags.web_pages_folders = ['docs',]
frappe.flags.web_pages_apps = [self.app,] frappe.flags.web_pages_apps = [self.app,]
@@ -44,7 +44,6 @@ class setup_docs(object):
"sub_heading": self.docs_config.sub_heading, "sub_heading": self.docs_config.sub_heading,
"source_link": self.docs_config.source_link, "source_link": self.docs_config.source_link,
"hide_install": getattr(self.docs_config, "hide_install", False), "hide_install": getattr(self.docs_config, "hide_install", False),
"docs_base_url": self.docs_config.docs_base_url,
"long_description": markdown(getattr(self.docs_config, "long_description", "")), "long_description": markdown(getattr(self.docs_config, "long_description", "")),
"license": self.hooks.get("app_license")[0], "license": self.hooks.get("app_license")[0],
"branch": getattr(self.docs_config, "branch", None) or "develop", "branch": getattr(self.docs_config, "branch", None) or "develop",
@@ -59,7 +58,7 @@ class setup_docs(object):


def build(self, docs_version): def build(self, docs_version):
"""Build templates for docs models and Python API""" """Build templates for docs models and Python API"""
self.docs_path = frappe.get_app_path(self.app, "docs")
self.docs_path = frappe.get_app_path(self.target_app, 'www', "docs")
self.path = os.path.join(self.docs_path, docs_version) self.path = os.path.join(self.docs_path, docs_version)
self.app_context["app"]["docs_version"] = docs_version self.app_context["app"]["docs_version"] = docs_version


@@ -91,14 +90,50 @@ class setup_docs(object):
#print parts #print parts
module, doctype = parts[-3], parts[-1] module, doctype = parts[-3], parts[-1]


if doctype not in ("doctype", "boilerplate"):
if doctype != "boilerplate":
self.write_model_file(basepath, module, doctype) self.write_model_file(basepath, module, doctype)


# standard python module # standard python module
if self.is_py_module(basepath, folders, files): if self.is_py_module(basepath, folders, files):
self.write_modules(basepath, folders, files) self.write_modules(basepath, folders, files)


self.build_user_docs()
#self.build_user_docs()
self.copy_user_assets()
self.add_sidebars()
self.add_breadcrumbs_for_user_pages()

def add_breadcrumbs_for_user_pages(self):
for basepath, folders, files in os.walk(os.path.join(self.docs_path,
'user')): # pylint: disable=unused-variable
for fname in files:
if fname.endswith('.md') or fname.endswith('.html'):
add_breadcrumbs_tag(os.path.join(basepath, fname))

def add_sidebars(self):
'''Add _sidebar.json in each folder in docs'''
for basepath, folders, files in os.walk(self.docs_path): # pylint: disable=unused-variable
with open(os.path.join(basepath, '_sidebar.json'), 'w') as sidebarfile:
sidebarfile.write(frappe.as_json([
{"title": "Docs Home", "route": "/docs"},
{"title": "User Guide", "route": "/docs/user"},
{"title": "Server API", "route": "/docs/current/api"},
{"title": "Models (Reference)", "route": "/docs/current/models"},
{"title": "Improve Docs", "route":
"{0}/tree/develop/{1}/docs".format(self.docs_config.source_link, self.app)}
]))


def copy_user_assets(self):
'''Copy docs/user and docs/assets to the target app'''
print('Copying docs/user and docs/assets...')
shutil.rmtree(os.path.join(self.docs_path, 'user'),
ignore_errors=True)
shutil.rmtree(os.path.join(self.docs_path, 'assets'),
ignore_errors=True)
shutil.copytree(os.path.join(self.app_path, 'docs', 'user'),
os.path.join(self.docs_path, 'user'))
shutil.copytree(os.path.join(self.app_path, 'docs', 'assets'),
frappe.get_app_path(self.target_app, 'www', 'docs', 'assets'))


def make_home_pages(self): def make_home_pages(self):
"""Make standard home pages for docs, developer docs, api and models """Make standard home pages for docs, developer docs, api and models
@@ -463,3 +498,9 @@ edit_link = '''
</div> </div>
</div> </div>
</div>''' </div>'''

def add_breadcrumbs_tag(path):
with open(path, 'r') as f:
content = frappe.as_unicode(f.read())
with open(path, 'w') as f:
f.write(('<!-- add-breadcrumbs -->\n' + content).encode('utf-8'))

+ 28
- 13
frappe/website/context.py Просмотреть файл

@@ -16,15 +16,17 @@ def get_context(path, args=None):
if args: if args:
context.update(args) context.update(args)


context = build_context(context)

if hasattr(frappe.local, 'request'): if hasattr(frappe.local, 'request'):
# for <body data-path=""> (remove leading slash) # for <body data-path=""> (remove leading slash)
# path could be overriden in render.resolve_from_map # path could be overriden in render.resolve_from_map
context["path"] = frappe.local.request.path[1:]
context["path"] = frappe.local.request.path.strip('/ ')
else: else:
context["path"] = path context["path"] = path


context.route = context.path

context = build_context(context)

# set using frappe.respond_as_web_page # set using frappe.respond_as_web_page
if hasattr(frappe.local, 'response') and frappe.local.response.get('context'): if hasattr(frappe.local, 'response') and frappe.local.response.get('context'):
context.update(frappe.local.response.context) context.update(frappe.local.response.context)
@@ -69,6 +71,9 @@ def build_context(context):
if context.url_prefix and context.url_prefix[-1]!='/': if context.url_prefix and context.url_prefix[-1]!='/':
context.url_prefix += '/' context.url_prefix += '/'


# for backward compatibility
context.docs_base_url = '/docs'

context.update(get_website_settings()) context.update(get_website_settings())
context.update(frappe.local.conf.get("website_context") or {}) context.update(frappe.local.conf.get("website_context") or {})


@@ -105,7 +110,21 @@ def build_context(context):
update_controller_context(context, extension) update_controller_context(context, extension)


add_metatags(context) add_metatags(context)
add_sidebar_and_breadcrumbs(context)

# determine templates to be used
if not context.base_template_path:
app_base = frappe.get_hooks("base_template")
context.base_template_path = app_base[0] if app_base else "templates/base.html"

if context.title_prefix and context.title and not context.title.startswith(context.title_prefix):
context.title = '{0} - {1}'.format(context.title_prefix, context.title)


return context

def add_sidebar_and_breadcrumbs(context):
'''Add sidebar and breadcrumbs to context'''
from frappe.website.router import get_page_info_from_template
if context.show_sidebar: if context.show_sidebar:
context.no_cache = 1 context.no_cache = 1
add_sidebar_data(context) add_sidebar_data(context)
@@ -117,16 +136,12 @@ def build_context(context):
context.sidebar_items = json.loads(sidebarfile.read()) context.sidebar_items = json.loads(sidebarfile.read())
context.show_sidebar = 1 context.show_sidebar = 1



# determine templates to be used
if not context.base_template_path:
app_base = frappe.get_hooks("base_template")
context.base_template_path = app_base[0] if app_base else "templates/base.html"

if context.title_prefix and context.title and not context.title.startswith(context.title_prefix):
context.title = '{0} - {1}'.format(context.title_prefix, context.title)

return context
if context.add_breadcrumbs and not context.parents:
if context.basepath:
parent_path = os.path.dirname(context.path).rstrip('/')
page_info = get_page_info_from_template(parent_path)
if page_info:
context.parents = [dict(route=parent_path, title=page_info.title)]


def add_sidebar_data(context): def add_sidebar_data(context):
from frappe.utils.user import get_fullname_and_avatar from frappe.utils.user import get_fullname_and_avatar


+ 33
- 3
frappe/website/doctype/blog_post/templates/blog_post.html Просмотреть файл

@@ -1,6 +1,36 @@
{% extends "templates/web.html" %} {% extends "templates/web.html" %}


{% block page_content %} {% block page_content %}
<style>
.blog-header {
font-weight: 700;
font-size: 2em;
}
.blog-comments {
position: relative;
border-top: 1px solid {{ style.border_color }};
}
.blog-info {
text-align:center;
margin-top: 30px;
}
.blog-text {
padding-top: 50px;
padding-bottom: 50px;
font-size: 18px;
line-height: 1.5;
}
.blog-text p {
margin-bottom: 30px;
}
.blogger {
padding-top: 0px;
padding-bottom: 50px;
}
.blogger-name {
margin-top: 0px;
}
</style>
<div class="blog-container"> <div class="blog-container">
<article class="blog-content" itemscope itemtype="http://schema.org/BlogPosting"> <article class="blog-content" itemscope itemtype="http://schema.org/BlogPosting">
<meta itemprop="datePublished" content="{{ frappe.format_date(published_on) }}"></meta> <meta itemprop="datePublished" content="{{ frappe.format_date(published_on) }}"></meta>
@@ -10,9 +40,9 @@
<h1 itemprop="headline" class="blog-header">{{ title }}</h1> <h1 itemprop="headline" class="blog-header">{{ title }}</h1>
<p class="post-by text-muted"> <p class="post-by text-muted">
<a href="/blog?blogger={{ blogger }}" class="no-decoration">{{ _("By") }} {{ blogger_info and blogger_info.full_name or full_name }}</a> <a href="/blog?blogger={{ blogger }}" class="no-decoration">{{ _("By") }} {{ blogger_info and blogger_info.full_name or full_name }}</a>
<i class="blog-dot"></i> {{ frappe.format_date(published_on) }}
<i class="blog-dot"></i> <a href="/blog?blog_category={{ blog_category }}" class="no-decoration">{{ category.title }}</a>
<i class="blog-dot"></i> {{ comment_text }}
<i class="spacer-dot"></i> {{ frappe.format_date(published_on) }}
<i class="spacer-dot"></i> <a href="/blog?blog_category={{ blog_category }}" class="no-decoration">{{ category.title }}</a>
<i class="spacer-dot"></i> {{ comment_text }}
</p> </p>
</div> </div>
<div itemprop="articleBody" class="longform blog-text"> <div itemprop="articleBody" class="longform blog-text">


+ 3
- 3
frappe/website/doctype/blog_post/templates/blog_post_row.html Просмотреть файл

@@ -12,11 +12,11 @@
<p class="post-by text-muted small"> <p class="post-by text-muted small">
<a href="/blog?blogger={{ post.blogger }}" <a href="/blog?blogger={{ post.blogger }}"
class="no-decoration">{{ _("By") }} {{ post.full_name }}</a> class="no-decoration">{{ _("By") }} {{ post.full_name }}</a>
<i class="blog-dot"></i> {{ frappe.format_date(post.published_on) }}
<i class="blog-dot"></i>
<i class="spacer-dot"></i> {{ frappe.format_date(post.published_on) }}
<i class="spacer-dot"></i>
<a href="/blog?blog_category={{ post.blog_category }}" <a href="/blog?blog_category={{ post.blog_category }}"
class="no-decoration">{{ post.category.title }}</a> class="no-decoration">{{ post.category.title }}</a>
<i class="blog-dot"></i> {{ post.comment_text }}
<i class="spacer-dot"></i> {{ post.comment_text }}
</p> </p>
</div> </div>
</div> </div>


+ 32
- 0
frappe/website/purifycss.py Просмотреть файл

@@ -0,0 +1,32 @@
import frappe, re, os

def purifycss():
source = frappe.get_app_path('frappe_theme', 'public', 'less', 'frappe_theme.less')
target_apps = ['erpnext_com', 'frappe_io', 'translator', 'chart_of_accounts_builder', 'frappe_theme']
with open(source, 'r') as f:
src = f.read()

classes = []
for line in src.splitlines():
line = line.strip()
if not line:
continue
if line[0]=='@':
continue
classes.extend(re.findall('\.([^0-9][^ :&.{,(]*)', line))

classes = list(set(classes))

for app in target_apps:
for basepath, folders, files in os.walk(frappe.get_app_path(app)):
for fname in files:
if fname.endswith('.html') or fname.endswith('.md'):
#print 'checking {0}...'.format(fname)
with open(os.path.join(basepath, fname), 'r') as f:
src = f.read()
for c in classes:
if c in src:
classes.remove(c)

for c in sorted(classes):
print c

Некоторые файлы не были показаны из-за большого количества измененных файлов

Загрузка…
Отмена
Сохранить