Преглед на файлове

Merge branch 'develop' of https://github.com/frappe/frappe into rebrand-ui

version-14
Suraj Shetty преди 4 години
родител
ревизия
b94d5778a8
променени са 100 файла, в които са добавени 1993 реда и са изтрити 926 реда
  1. +56
    -1
      cypress/integration/depends_on.js
  2. +38
    -3
      cypress/support/commands.js
  3. +16
    -0
      frappe/__init__.py
  4. +51
    -15
      frappe/app.py
  5. +6
    -4
      frappe/automation/doctype/auto_repeat/auto_repeat.js
  6. +7
    -7
      frappe/automation/doctype/auto_repeat/auto_repeat.json
  7. +13
    -0
      frappe/cache_manager.py
  8. +1
    -1
      frappe/core/doctype/comment/comment.py
  9. +10
    -1
      frappe/core/doctype/custom_docperm/custom_docperm.json
  10. +1
    -1
      frappe/core/doctype/data_import/importer.py
  11. +59
    -605
      frappe/core/doctype/docperm/docperm.json
  12. +9
    -10
      frappe/core/doctype/doctype/doctype.py
  13. +11
    -0
      frappe/core/doctype/document_naming_rule/document_naming_rule.py
  14. +2
    -2
      frappe/core/doctype/server_script/server_script.json
  15. +1
    -0
      frappe/core/doctype/server_script/server_script_utils.py
  16. +2
    -2
      frappe/core/doctype/system_settings/system_settings.json
  17. +5
    -0
      frappe/core/doctype/user/user.json
  18. +3
    -3
      frappe/core/doctype/version/version_view.html
  19. +1
    -1
      frappe/core/page/permission_manager/permission_manager.js
  20. +13
    -1
      frappe/core/page/permission_manager/permission_manager.py
  21. +0
    -1
      frappe/desk/form/save.py
  22. +2
    -1
      frappe/desk/search.py
  23. +1
    -1
      frappe/email/doctype/email_account/email_account.py
  24. +12
    -2
      frappe/event_streaming/doctype/event_producer/event_producer.py
  25. +96
    -0
      frappe/geo/utils.py
  26. +1
    -1
      frappe/hooks.py
  27. +0
    -0
      frappe/integrations/doctype/connected_app/__init__.py
  28. +38
    -0
      frappe/integrations/doctype/connected_app/connected_app.js
  29. +166
    -0
      frappe/integrations/doctype/connected_app/connected_app.json
  30. +133
    -0
      frappe/integrations/doctype/connected_app/connected_app.py
  31. +162
    -0
      frappe/integrations/doctype/connected_app/test_connected_app.py
  32. +13
    -0
      frappe/integrations/doctype/connected_app/test_records.json
  33. +1
    -2
      frappe/integrations/doctype/oauth_client/test_records.json
  34. +0
    -0
      frappe/integrations/doctype/oauth_scope/__init__.py
  35. +30
    -0
      frappe/integrations/doctype/oauth_scope/oauth_scope.json
  36. +10
    -0
      frappe/integrations/doctype/oauth_scope/oauth_scope.py
  37. +0
    -0
      frappe/integrations/doctype/query_parameters/__init__.py
  38. +37
    -0
      frappe/integrations/doctype/query_parameters/query_parameters.json
  39. +10
    -0
      frappe/integrations/doctype/query_parameters/query_parameters.py
  40. +14
    -0
      frappe/integrations/doctype/social_login_key/test_social_login_key.py
  41. +0
    -0
      frappe/integrations/doctype/token_cache/__init__.py
  42. +18
    -0
      frappe/integrations/doctype/token_cache/test_records.json
  43. +37
    -0
      frappe/integrations/doctype/token_cache/test_token_cache.py
  44. +8
    -0
      frappe/integrations/doctype/token_cache/token_cache.js
  45. +110
    -0
      frappe/integrations/doctype/token_cache/token_cache.json
  46. +67
    -0
      frappe/integrations/doctype/token_cache/token_cache.py
  47. +1
    -1
      frappe/integrations/doctype/webhook/webhook.py
  48. +1
    -0
      frappe/integrations/oauth2.py
  49. +8
    -5
      frappe/model/base_document.py
  50. +9
    -3
      frappe/model/db_query.py
  51. +4
    -2
      frappe/model/document.py
  52. +3
    -1
      frappe/model/meta.py
  53. +10
    -2
      frappe/model/rename_doc.py
  54. +2
    -3
      frappe/model/workflow.py
  55. +1
    -0
      frappe/patches.txt
  56. +11
    -0
      frappe/patches/v13_0/delete_package_publish_tool.py
  57. +14
    -0
      frappe/patches/v13_0/website_theme_custom_scss.py
  58. +4
    -3
      frappe/permissions.py
  59. +1
    -0
      frappe/public/build.json
  60. +7
    -0
      frappe/public/css/list.css
  61. +22
    -5
      frappe/public/js/frappe/form/controls/base_control.js
  62. +1
    -1
      frappe/public/js/frappe/form/controls/base_input.js
  63. +6
    -4
      frappe/public/js/frappe/form/controls/geolocation.js
  64. +11
    -0
      frappe/public/js/frappe/form/controls/link.js
  65. +1
    -1
      frappe/public/js/frappe/form/controls/rating.js
  66. +2
    -1
      frappe/public/js/frappe/form/controls/table.js
  67. +4
    -1
      frappe/public/js/frappe/form/form.js
  68. +9
    -5
      frappe/public/js/frappe/form/formatters.js
  69. +2
    -0
      frappe/public/js/frappe/form/grid.js
  70. +3
    -0
      frappe/public/js/frappe/form/grid_row_form.js
  71. +61
    -58
      frappe/public/js/frappe/form/layout.js
  72. +17
    -4
      frappe/public/js/frappe/form/toolbar.js
  73. +2
    -11
      frappe/public/js/frappe/form/workflow.js
  74. +122
    -7
      frappe/public/js/frappe/list/list_sidebar.js
  75. +6
    -2
      frappe/public/js/frappe/model/model.js
  76. +1
    -2
      frappe/public/js/frappe/ui/filters/filter.js
  77. +1
    -1
      frappe/public/js/frappe/utils/common.js
  78. +2
    -0
      frappe/public/js/frappe/utils/utils.js
  79. +1
    -2
      frappe/public/js/frappe/views/communication.js
  80. +85
    -0
      frappe/public/js/frappe/views/map/map_view.js
  81. +27
    -0
      frappe/public/js/frappe/views/reports/report_view.js
  82. +7
    -7
      frappe/public/js/frappe/views/treeview.js
  83. +2
    -2
      frappe/public/js/frappe/web_form/webform_script.js
  84. +2
    -2
      frappe/public/scss/website/page_builder.scss
  85. +6
    -2
      frappe/templates/includes/breadcrumbs.html
  86. +7
    -5
      frappe/templates/print_formats/standard_macros.html
  87. +57
    -0
      frappe/tests/test_cors.py
  88. +11
    -11
      frappe/tests/test_hooks.py
  89. +19
    -1
      frappe/tests/test_permissions.py
  90. +42
    -0
      frappe/tests/tests_geo_utils.py
  91. +18
    -0
      frappe/tests/ui_test_helpers.py
  92. +1
    -1
      frappe/translate.py
  93. +1
    -1
      frappe/translations/de.csv
  94. +20
    -16
      frappe/utils/data.py
  95. +6
    -3
      frappe/utils/user.py
  96. +2
    -2
      frappe/website/doctype/blog_post/templates/blog_post_row.html
  97. +5
    -1
      frappe/website/js/bootstrap-4.js
  98. +1
    -3
      frappe/website/web_template/testimonial/testimonial.html
  99. +1
    -1
      package.json
  100. +52
    -78
      yarn.lock

+ 56
- 1
cypress/integration/depends_on.js Целия файл

@@ -3,7 +3,31 @@ context('Depends On', () => {
cy.login();
cy.visit('/app/website');
return cy.window().its('frappe').then(frappe => {
return frappe.call('frappe.tests.ui_test_helpers.create_doctype', {
return frappe.xcall('frappe.tests.ui_test_helpers.create_child_doctype', {
name: 'Child Test Depends On',
fields: [
{
"label": "Child Test Field",
"fieldname": "child_test_field",
"fieldtype": "Data",
"in_list_view": 1,
},
{
"label": "Child Dependant Field",
"fieldname": "child_dependant_field",
"fieldtype": "Data",
"in_list_view": 1,
},
{
"label": "Child Display Dependant Field",
"fieldname": "child_display_dependant_field",
"fieldtype": "Data",
"in_list_view": 1,
},
]
});
}).then(frappe => {
return frappe.xcall('frappe.tests.ui_test_helpers.create_doctype', {
name: 'Test Depends On',
fields: [
{
@@ -24,6 +48,13 @@ context('Depends On', () => {
"fieldtype": "Data",
'depends_on': "eval:doc.test_field=='Value'"
},
{
"label": "Child Test Depends On Field",
"fieldname": "child_test_depends_on_field",
"fieldtype": "Table",
'read_only_depends_on': "eval:doc.test_field=='Some Other Value'",
'options': "Child Test Depends On"
},
]
});
});
@@ -48,6 +79,30 @@ context('Depends On', () => {
cy.get('body').click();
cy.get('.control-input [data-fieldname="dependant_field"]').should('not.be.disabled');
});
it('should set the table and its fields as read only depending on other fields value', () => {
cy.new_form('Test Depends On');
cy.fill_field('dependant_field', 'Some Value');
//cy.fill_field('test_field', 'Some Other Value');
cy.get('.frappe-control[data-fieldname="child_test_depends_on_field"]').as('table');
cy.get('@table').find('button.grid-add-row').click();
cy.get('@table').find('[data-idx="1"]').as('row1');
cy.get('@row1').find('.btn-open-row').click();
cy.get('@row1').find('.form-in-grid').as('row1-form_in_grid');
//cy.get('@row1-form_in_grid').find('')
cy.fill_table_field('child_test_depends_on_field', '1', 'child_test_field', 'Some Value');
cy.fill_table_field('child_test_depends_on_field', '1', 'child_dependant_field', 'Some Other Value');

cy.get('@row1-form_in_grid').find('.octicon-triangle-up').click();

// set the table to read-only
cy.fill_field('test_field', 'Some Other Value');

// grid row form fields should be read-only
cy.get('@row1').find('.btn-open-row').click();

cy.get('@row1-form_in_grid').find('.control-input [data-fieldname="child_test_field"]').should('be.disabled');
cy.get('@row1-form_in_grid').find('.control-input [data-fieldname="child_dependant_field"]').should('be.disabled');
});
it('should display the field depending on other fields value', () => {
cy.new_form('Test Depends On');
cy.get('.control-input [data-fieldname="display_dependant_field"]').should('not.be.visible');


+ 38
- 3
cypress/support/commands.js Целия файл

@@ -160,7 +160,7 @@ Cypress.Commands.add('remove_doc', (doctype, name) => {

Cypress.Commands.add('create_records', doc => {
return cy
.call('frappe.tests.ui_test_helpers.create_if_not_exists', { doc })
.call('frappe.tests.ui_test_helpers.create_if_not_exists', {doc})
.then(r => r.message);
});

@@ -186,7 +186,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
if (fieldtype === 'Select') {
cy.get('@input').select(value);
} else {
cy.get('@input').type(value, { waitForAnimations: false, force: true });
cy.get('@input').type(value, {waitForAnimations: false, force: true});
}
return cy.get('@input');
});
@@ -204,8 +204,43 @@ Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => {
return cy.get(selector);
});

Cypress.Commands.add('fill_table_field', (tablefieldname, row_idx, fieldname, value, fieldtype = 'Data') => {
cy.get_table_field(tablefieldname, row_idx, fieldname, fieldtype).as('input');

if (['Date', 'Time', 'Datetime'].includes(fieldtype)) {
cy.get('@input').click().wait(200);
cy.get('.datepickers-container .datepicker.active').should('exist');
}
if (fieldtype === 'Time') {
cy.get('@input').clear().wait(200);
}

if (fieldtype === 'Select') {
cy.get('@input').select(value);
} else {
cy.get('@input').type(value, {waitForAnimations: false, force: true});
}
return cy.get('@input');
});

Cypress.Commands.add('get_table_field', (tablefieldname, row_idx, fieldname, fieldtype = 'Data') => {
let selector = `.frappe-control[data-fieldname="${tablefieldname}"]`;
selector += ` [data-idx="${row_idx}"]`;
selector += ` .form-in-grid`;

if (fieldtype === 'Text Editor') {
selector += ` [data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`;
} else if (fieldtype === 'Code') {
selector += ` [data-fieldname="${fieldname}"] .ace_text-input`;
} else {
selector += ` .form-control[data-fieldname="${fieldname}"]`;
}

return cy.get(selector);
});

Cypress.Commands.add('awesomebar', text => {
cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, { delay: 100 });
cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, {delay: 100});
});

Cypress.Commands.add('new_form', doctype => {


+ 16
- 0
frappe/__init__.py Целия файл

@@ -27,6 +27,7 @@ __version__ = '13.0.0-dev'
__title__ = "Frappe Framework"

local = Local()
controllers = {}

class _dict(dict):
"""dict like object that exposes keys as attributes"""
@@ -628,6 +629,21 @@ def clear_cache(user=None, doctype=None):

local.role_permissions = {}

def only_has_select_perm(doctype, user=None, ignore_permissions=False):
if ignore_permissions:
return False

if not user:
user = local.session.user

import frappe.permissions
permissions = frappe.permissions.get_role_permissions(doctype, user=user)

if permissions.get('select') and not permissions.get('read'):
return True
else:
return False

def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=False, throw=False):
"""Raises `frappe.PermissionError` if not permitted.



+ 51
- 15
frappe/app.py Целия файл

@@ -7,8 +7,8 @@ import os
from six import iteritems
import logging

from werkzeug.wrappers import Request
from werkzeug.local import LocalManager
from werkzeug.wrappers import Request, Response
from werkzeug.exceptions import HTTPException, NotFound
from werkzeug.middleware.profiler import ProfilerMiddleware
from werkzeug.middleware.shared_data import SharedDataMiddleware
@@ -57,19 +57,22 @@ def application(request):
frappe.monitor.start()
frappe.rate_limiter.apply()

if frappe.local.form_dict.cmd:
if request.method == "OPTIONS":
response = Response()

elif frappe.form_dict.cmd:
response = frappe.handler.handle()

elif frappe.request.path.startswith("/api/"):
elif request.path.startswith("/api/"):
response = frappe.api.handle()

elif frappe.request.path.startswith('/backups'):
elif request.path.startswith('/backups'):
response = frappe.utils.response.download_backup(request.path)

elif frappe.request.path.startswith('/private/files/'):
elif request.path.startswith('/private/files/'):
response = frappe.utils.response.download_private_file(request.path)

elif frappe.local.request.method in ('GET', 'HEAD', 'POST'):
elif request.method in ('GET', 'HEAD', 'POST'):
response = frappe.website.render.render()

else:
@@ -88,13 +91,9 @@ def application(request):
rollback = after_request(rollback)

finally:
if frappe.local.request.method in ("POST", "PUT") and frappe.db and rollback:
if request.method in ("POST", "PUT") and frappe.db and rollback:
frappe.db.rollback()

# set cookies
if response and hasattr(frappe.local, 'cookie_manager'):
frappe.local.cookie_manager.flush_cookies(response=response)

frappe.rate_limiter.update()
frappe.monitor.stop(response)
frappe.recorder.dump()
@@ -110,9 +109,7 @@ def application(request):
"http_status_code": getattr(response, "status_code", "NOTFOUND")
})

if response and hasattr(frappe.local, 'rate_limiter'):
response.headers.extend(frappe.local.rate_limiter.headers())

process_response(response)
frappe.destroy()

return response
@@ -134,7 +131,46 @@ def init_request(request):

make_form_dict(request)

frappe.local.http_request = frappe.auth.HTTPRequest()
if request.method != "OPTIONS":
frappe.local.http_request = frappe.auth.HTTPRequest()

def process_response(response):
if not response:
return

# set cookies
if hasattr(frappe.local, 'cookie_manager'):
frappe.local.cookie_manager.flush_cookies(response=response)

# rate limiter headers
if hasattr(frappe.local, 'rate_limiter'):
response.headers.extend(frappe.local.rate_limiter.headers())

# CORS headers
if hasattr(frappe.local, 'conf') and frappe.conf.allow_cors:
set_cors_headers(response)

def set_cors_headers(response):
origin = frappe.request.headers.get('Origin')
if not origin:
return

allow_cors = frappe.conf.allow_cors
if allow_cors != "*":
if not isinstance(allow_cors, list):
allow_cors = [allow_cors]

if origin not in allow_cors:
return

response.headers.extend({
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': ('Authorization,DNT,X-Mx-ReqToken,'
'Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,'
'Cache-Control,Content-Type')
})

def make_form_dict(request):
import json


+ 6
- 4
frappe/automation/doctype/auto_repeat/auto_repeat.js Целия файл

@@ -54,10 +54,12 @@ frappe.ui.form.on('Auto Repeat', {

toggle_submit_on_creation: function(frm) {
// submit on creation checkbox
frappe.model.with_doctype(frm.doc.reference_doctype, () => {
let meta = frappe.get_meta(frm.doc.reference_doctype);
frm.toggle_display('submit_on_creation', meta.is_submittable);
});
if (frm.doc.reference_doctype) {
frappe.model.with_doctype(frm.doc.reference_doctype, () => {
let meta = frappe.get_meta(frm.doc.reference_doctype);
frm.toggle_display('submit_on_creation', meta.is_submittable);
});
}
},

template: function(frm) {


+ 7
- 7
frappe/automation/doctype/auto_repeat/auto_repeat.json Целия файл

@@ -23,7 +23,7 @@
"repeat_on_last_day",
"column_break_12",
"next_schedule_date",
"section_break_12",
"section_break_16",
"repeat_on_days",
"notification",
"notify_by_email",
@@ -198,20 +198,20 @@
"label": "Repeat on Days",
"options": "Auto Repeat Day"
},
{
"depends_on": "eval:doc.frequency==='Weekly';",
"fieldname": "section_break_12",
"fieldtype": "Section Break"
},
{
"default": "0",
"fieldname": "submit_on_creation",
"fieldtype": "Check",
"label": "Submit on Creation"
},
{
"depends_on": "eval:doc.frequency==='Weekly';",
"fieldname": "section_break_16",
"fieldtype": "Section Break"
}
],
"links": [],
"modified": "2020-12-10 10:43:13.449172",
"modified": "2021-01-12 09:24:49.719611",
"modified_by": "Administrator",
"module": "Automation",
"name": "Auto Repeat",


+ 13
- 0
frappe/cache_manager.py Целия файл

@@ -68,6 +68,7 @@ def clear_defaults_cache(user=None):
frappe.cache().delete_key("defaults")

def clear_doctype_cache(doctype=None):
clear_controller_cache(doctype)
cache = frappe.cache()

if getattr(frappe.local, 'meta_cache') and (doctype in frappe.local.meta_cache):
@@ -99,6 +100,18 @@ def clear_doctype_cache(doctype=None):
for name in doctype_cache_keys:
cache.delete_value(name)

# Clear all document's cache. To clear documents of a specific DocType document_cache should be restructured
clear_document_cache()

def clear_controller_cache(doctype=None):
if not doctype:
del frappe.controllers
frappe.controllers = {}
return

for site_controllers in frappe.controllers.values():
site_controllers.pop(doctype, None)

def get_doctype_map(doctype, name, filters=None, order_by=None):
cache = frappe.cache()
cache_key = frappe.scrub(doctype) + '_map'


+ 1
- 1
frappe/core/doctype/comment/comment.py Целия файл

@@ -164,7 +164,7 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments):
try:
# use sql, so that we do not mess with the timestamp
frappe.db.sql("""update `tab{0}` set `_comments`=%s where name=%s""".format(reference_doctype), # nosec
(json.dumps(_comments[-50:]), reference_name))
(json.dumps(_comments[-100:]), reference_name))

except Exception as e:
if frappe.db.is_column_missing(e) and getattr(frappe.local, 'request', None):


+ 10
- 1
frappe/core/doctype/custom_docperm/custom_docperm.json Целия файл

@@ -1,4 +1,5 @@
{
"actions": [],
"allow_import": 1,
"autoname": "hash",
"creation": "2017-01-11 04:21:35.217943",
@@ -13,6 +14,7 @@
"column_break_2",
"permlevel",
"section_break_4",
"select",
"read",
"write",
"create",
@@ -211,9 +213,16 @@
"fieldtype": "Data",
"label": "Reference Document Type",
"read_only": 1
},
{
"default": "0",
"fieldname": "select",
"fieldtype": "Check",
"label": "Select"
}
],
"modified": "2019-10-31 16:58:16.157079",
"links": [],
"modified": "2020-12-03 15:20:48.296730",
"modified_by": "Administrator",
"module": "Core",
"name": "Custom DocPerm",


+ 1
- 1
frappe/core/doctype/data_import/importer.py Целия файл

@@ -751,7 +751,7 @@ class Row:
self.warnings.append(
{
"row": self.row_number,
"message": _("{0} is a mandatory field asdadsf").format(id_field.label),
"message": _("{0} is a mandatory field").format(id_field.label),
}
)
return


+ 59
- 605
frappe/core/doctype/docperm/docperm.json Целия файл

@@ -1,775 +1,229 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"actions": [],
"autoname": "hash",
"beta": 0,
"creation": "2013-02-22 01:27:33",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"role_and_level",
"role",
"if_owner",
"column_break_2",
"permlevel",
"section_break_4",
"select",
"read",
"write",
"create",
"delete",
"column_break_8",
"submit",
"cancel",
"amend",
"additional_permissions",
"report",
"export",
"import",
"set_user_permissions",
"column_break_19",
"share",
"print",
"email"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "role_and_level",
"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": "Role and Level",
"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,
"translatable": 0,
"unique": 0
"label": "Role and Level"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "role",
"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": 0,
"label": "Role",
"length": 0,
"no_copy": 0,
"oldfieldname": "role",
"oldfieldtype": "Link",
"options": "Role",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "150px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "150px"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"description": "Apply this rule if the User is the Owner",
"fieldname": "if_owner",
"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": "If user is the owner",
"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,
"translatable": 0,
"unique": 0
"label": "If user is the owner"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2",
"fieldtype": "Column 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,
"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,
"translatable": 0,
"unique": 0
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "permlevel",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Level",
"length": 0,
"no_copy": 0,
"oldfieldname": "permlevel",
"oldfieldtype": "Int",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "40px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "40px"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_4",
"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": "Permissions",
"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,
"translatable": 0,
"unique": 0
"label": "Permissions"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "read",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Read",
"length": 0,
"no_copy": 0,
"oldfieldname": "read",
"oldfieldtype": "Check",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "32px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "32px"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "write",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Write",
"length": 0,
"no_copy": 0,
"oldfieldname": "write",
"oldfieldtype": "Check",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "32px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "32px"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "create",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Create",
"length": 0,
"no_copy": 0,
"oldfieldname": "create",
"oldfieldtype": "Check",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "32px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "32px"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "delete",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Delete",
"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,
"translatable": 0,
"unique": 0
"label": "Delete"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_8",
"fieldtype": "Column 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,
"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,
"translatable": 0,
"unique": 0
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "submit",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Submit",
"length": 0,
"no_copy": 0,
"oldfieldname": "submit",
"oldfieldtype": "Check",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "32px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "32px"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "cancel",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Cancel",
"length": 0,
"no_copy": 0,
"oldfieldname": "cancel",
"oldfieldtype": "Check",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "32px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "32px"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "amend",
"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": "Amend",
"length": 0,
"no_copy": 0,
"oldfieldname": "amend",
"oldfieldtype": "Check",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "32px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "32px"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "additional_permissions",
"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": "Additional Permissions",
"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,
"translatable": 0,
"unique": 0
"label": "Additional Permissions"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "report",
"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": "Report",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "32px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "32px"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "export",
"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": "Export",
"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,
"translatable": 0,
"unique": 0
"label": "Export"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "import",
"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": "Import",
"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,
"translatable": 0,
"unique": 0
"label": "Import"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"description": "This role update User Permissions for a user",
"fieldname": "set_user_permissions",
"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": "Set User Permissions",
"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,
"translatable": 0,
"unique": 0
"label": "Set User Permissions"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_19",
"fieldtype": "Column 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,
"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,
"translatable": 0,
"unique": 0
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "share",
"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": "Share",
"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,
"translatable": 0,
"unique": 0
"label": "Share"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "print",
"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": "Print",
"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,
"translatable": 0,
"unique": 0
"label": "Print"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1",
"fieldname": "email",
"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": "Email",
"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,
"translatable": 0,
"unique": 0
"label": "Email"
},
{
"default": "0",
"fieldname": "select",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Select"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-05-29 11:54:38.613936",
"links": [],
"modified": "2020-12-03 15:15:30.488212",
"modified_by": "Administrator",
"module": "Core",
"name": "DocPerm",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_order": "ASC",
"track_changes": 0,
"track_seen": 0
"sort_field": "modified",
"sort_order": "ASC"
}

+ 9
- 10
frappe/core/doctype/doctype/doctype.py Целия файл

@@ -5,7 +5,7 @@
from __future__ import unicode_literals
import re, copy, os, shutil
import json
from frappe.cache_manager import clear_user_cache
from frappe.cache_manager import clear_user_cache, clear_controller_cache

# imports - third party imports
import six
@@ -395,13 +395,11 @@ class DocType(Document):
if not frappe.flags.in_patch:
self.rename_files_and_folders(old, new)

for site in frappe.utils.get_sites():
frappe.cache().delete(f"{site}:doctype_classes", old)
clear_controller_cache(old)

def after_delete(self):
if not self.custom:
for site in frappe.utils.get_sites():
frappe.cache().delete(f"{site}:doctype_classes", self.name)
clear_controller_cache(self.name)

def rename_files_and_folders(self, old, new):
# move files
@@ -1004,10 +1002,10 @@ def validate_fields(meta):
check_sort_field(meta)
check_image_field(meta)

def validate_permissions_for_doctype(doctype, for_remove=False):
def validate_permissions_for_doctype(doctype, for_remove=False, alert=False):
"""Validates if permissions are set correctly."""
doctype = frappe.get_doc("DocType", doctype)
validate_permissions(doctype, for_remove)
validate_permissions(doctype, for_remove, alert=alert)

# save permissions
for perm in doctype.get("permissions"):
@@ -1030,9 +1028,10 @@ def clear_permissions_cache(doctype):
""", doctype):
frappe.clear_cache(user=user)

def validate_permissions(doctype, for_remove=False):
def validate_permissions(doctype, for_remove=False, alert=False):
permissions = doctype.get("permissions")
if not permissions:
# Some DocTypes may not have permissions by default, don't show alert for them
if not permissions and alert:
frappe.msgprint(_('No Permissions Specified'), alert=True, indicator='orange')
issingle = issubmittable = isimportable = False
if doctype:
@@ -1044,7 +1043,7 @@ def validate_permissions(doctype, for_remove=False):
return _("For {0} at level {1} in {2} in row {3}").format(d.role, d.permlevel, d.parent, d.idx)

def check_atleast_one_set(d):
if not d.read and not d.write and not d.submit and not d.cancel and not d.create:
if not d.select and not d.read and not d.write and not d.submit and not d.cancel and not d.create:
frappe.throw(_("{0}: No basic permissions set").format(get_txt(d)))

def check_double(d):


+ 11
- 0
frappe/core/doctype/document_naming_rule/document_naming_rule.py Целия файл

@@ -6,8 +6,19 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils.data import evaluate_filters
from frappe import _

class DocumentNamingRule(Document):
def validate(self):
self.validate_fields_in_conditions()

def validate_fields_in_conditions(self):
if self.has_value_changed("document_type"):
docfields = [x.fieldname for x in frappe.get_meta(self.document_type).fields]
for condition in self.conditions:
if condition.field not in docfields:
frappe.throw(_("{0} is not a field of doctype {1}").format(frappe.bold(condition.field), frappe.bold(self.document_type)))

def apply(self, doc):
'''
Apply naming rules for the given document. Will set `name` if the rule is matched.


+ 2
- 2
frappe/core/doctype/server_script/server_script.json Целия файл

@@ -47,7 +47,7 @@
"fieldname": "doctype_event",
"fieldtype": "Select",
"label": "DocType Event",
"options": "Before Insert\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)"
"options": "Before Insert\nBefore Validate\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)"
},
{
"depends_on": "eval:doc.script_type==='API'",
@@ -88,7 +88,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-12-03 22:42:02.708148",
"modified": "2021-01-03 18:50:14.767595",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",


+ 1
- 0
frappe/core/doctype/server_script/server_script_utils.py Целия файл

@@ -6,6 +6,7 @@ import frappe
EVENT_MAP = {
'before_insert': 'Before Insert',
'after_insert': 'After Insert',
'before_validate': 'Before Validate',
'validate': 'Before Save',
'on_update': 'After Save',
'before_submit': 'Before Submit',


+ 2
- 2
frappe/core/doctype/system_settings/system_settings.json Целия файл

@@ -358,7 +358,7 @@
"collapsible": 1,
"fieldname": "email",
"fieldtype": "Section Break",
"label": "EMail"
"label": "Email"
},
{
"description": "Your organization name and address for the email footer.",
@@ -504,4 +504,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
}
}

+ 5
- 0
frappe/core/doctype/user/user.json Целия файл

@@ -654,6 +654,11 @@
"group": "Activity",
"link_doctype": "ToDo",
"link_fieldname": "owner"
},
{
"group": "Integrations",
"link_doctype": "Token Cache",
"link_fieldname": "user"
}
],
"max_attachments": 5,


+ 3
- 3
frappe/core/doctype/version/version_view.html Целия файл

@@ -21,7 +21,7 @@
<td class="danger">{{ item[1] }}</td>
<td class="success">{{ item[2] }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% endif %}
@@ -58,7 +58,7 @@
</table>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>

@@ -93,4 +93,4 @@
{% endfor %}
</tbody>
{% endif %}
</div>
</div>

+ 1
- 1
frappe/core/page/permission_manager/permission_manager.js Целия файл

@@ -292,7 +292,7 @@ frappe.PermissionEngine = class PermissionEngine {
}

get rights() {
return ["read", "write", "create", "delete", "submit", "cancel", "amend",
return ["select", "read", "write", "create", "delete", "submit", "cancel", "amend",
"print", "email", "report", "import", "export", "set_user_permissions", "share"]
}



+ 13
- 1
frappe/core/page/permission_manager/permission_manager.py Целия файл

@@ -77,6 +77,18 @@ def add(parent, role, permlevel):

@frappe.whitelist()
def update(doctype, role, permlevel, ptype, value=None):
"""Update role permission params
Args:
doctype (str): Name of the DocType to update params for
role (str): Role to be updated for, eg "Website Manager".
permlevel (int): perm level the provided rule applies to
ptype (str): permission type, example "read", "delete", etc.
value (None, optional): value for ptype, None indicates False
Returns:
str: Refresh flag is permission is updated successfully
"""
frappe.only_for("System Manager")
out = update_permission_property(doctype, role, permlevel, ptype, value)
return 'refresh' if out else None
@@ -92,7 +104,7 @@ def remove(doctype, role, permlevel):
if not frappe.get_all('Custom DocPerm', dict(parent=doctype)):
frappe.throw(_('There must be atleast one permission rule.'), title=_('Cannot Remove'))

validate_permissions_for_doctype(doctype, for_remove=True)
validate_permissions_for_doctype(doctype, for_remove=True, alert=True)

@frappe.whitelist()
def reset(doctype):


+ 0
- 1
frappe/desk/form/save.py Целия файл

@@ -42,7 +42,6 @@ def cancel(doctype=None, name=None, workflow_state_fieldname=None, workflow_stat

except Exception:
frappe.errprint(frappe.utils.get_traceback())
frappe.msgprint(frappe._("Did not cancel"))
raise

def send_updated_docs(doc):


+ 2
- 1
frappe/desk/search.py Целия файл

@@ -150,7 +150,8 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
# 2 is the index of _relevance column
order_by = "_relevance, {0}, `tab{1}`.idx desc".format(order_by_based_on_meta, doctype)

ignore_permissions = True if doctype == "DocType" else (cint(ignore_user_permissions) and has_permission(doctype))
ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read'
ignore_permissions = True if doctype == "DocType" else (cint(ignore_user_permissions) and has_permission(doctype, ptype=ptype))

if doctype in UNTRANSLATED_DOCTYPES:
page_length = None


+ 1
- 1
frappe/email/doctype/email_account/email_account.py Целия файл

@@ -210,7 +210,7 @@ class EmailAccount(Document):
elif not in_receive and any(map(lambda t: t in message, auth_error_codes)):
self.throw_invalid_credentials_exception()
else:
frappe.throw(e)
frappe.throw(cstr(e))

except socket.error:
if in_receive:


+ 12
- 2
frappe/event_streaming/doctype/event_producer/event_producer.py Целия файл

@@ -295,7 +295,7 @@ def set_update(update, producer_site):
if data.changed:
local_doc.update(data.changed)
if data.removed:
update_row_removed(local_doc, data.removed)
local_doc = update_row_removed(local_doc, data.removed)
if data.row_changed:
update_row_changed(local_doc, data.row_changed)
if data.added:
@@ -318,7 +318,17 @@ def update_row_removed(local_doc, removed):
for tablename, rownames in iteritems(removed):
table = local_doc.get_table_field_doctype(tablename)
for row in rownames:
frappe.db.delete(table, row)
table_rows = local_doc.get(tablename)
child_table_row = get_child_table_row(table_rows, row)
table_rows.remove(child_table_row)
local_doc.set(tablename, table_rows)
return local_doc


def get_child_table_row(table_rows, row):
for entry in table_rows:
if entry.get('name') == row:
return entry


def update_row_changed(local_doc, changed):


+ 96
- 0
frappe/geo/utils.py Целия файл

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

from __future__ import unicode_literals

import frappe

from pymysql import InternalError


@frappe.whitelist()
def get_coords(doctype, filters, type):
'''Get a geojson dict representing a doctype.'''
filters_sql = get_coords_conditions(doctype, filters)[4:]

coords = None
if type == 'location_field':
coords = return_location(doctype, filters_sql)
elif type == 'coordinates':
coords = return_coordinates(doctype, filters_sql)

out = convert_to_geojson(type, coords)
return out

def convert_to_geojson(type, coords):
'''Converts GPS coordinates to geoJSON string.'''
geojson = {"type": "FeatureCollection", "features": None}

if type == 'location_field':
geojson['features'] = merge_location_features_in_one(coords)
elif type == 'coordinates':
geojson['features'] = create_gps_markers(coords)

return geojson


def merge_location_features_in_one(coords):
'''Merging all features from location field.'''
geojson_dict = []
for element in coords:
geojson_loc = frappe.parse_json(element['location'])
if not geojson_loc:
continue
for coord in geojson_loc['features']:
coord['properties']['name'] = element['name']
geojson_dict.append(coord.copy())

return geojson_dict


def create_gps_markers(coords):
'''Build Marker based on latitude and longitude.'''
geojson_dict = []
for i in coords:
node = {"type": "Feature", "properties": {}, "geometry": {"type": "Point", "coordinates": None}}
node['properties']['name'] = i.name
node['geometry']['coordinates'] = [i.latitude, i.longitude]
geojson_dict.append(node.copy())

return geojson_dict


def return_location(doctype, filters_sql):
'''Get name and location fields for Doctype.'''
if filters_sql:
try:
coords = frappe.db.sql('''SELECT name, location FROM `tab{}` WHERE {}'''.format(doctype, filters_sql), as_dict=True)
except InternalError:
frappe.msgprint(frappe._('This Doctype does not contain location fields'), raise_exception=True)
return
else:
coords = frappe.get_all(doctype, fields=['name', 'location'])
return coords


def return_coordinates(doctype, filters_sql):
'''Get name, latitude and longitude fields for Doctype.'''
if filters_sql:
try:
coords = frappe.db.sql('''SELECT name, latitude, longitude FROM `tab{}` WHERE {}'''.format(doctype, filters_sql), as_dict=True)
except InternalError:
frappe.msgprint(frappe._('This Doctype does not contain latitude and longitude fields'), raise_exception=True)
return
else:
coords = frappe.get_all(doctype, fields=['name', 'latitude', 'longitude'])
return coords


def get_coords_conditions(doctype, filters=None):
'''Returns SQL conditions with user permissions and filters for event queries.'''
from frappe.desk.reportview import get_filters_cond
if not frappe.has_permission(doctype):
frappe.throw(frappe._("Not Permitted"), frappe.PermissionError)

return get_filters_cond(doctype, filters, [], with_match_conditions=True)

+ 1
- 1
frappe/hooks.py Целия файл

@@ -18,7 +18,7 @@ app_email = "info@frappe.io"

docs_app = "frappe_io"

translator_url = "https://translatev2.erpnext.com"
translator_url = "https://translate.erpnext.com"

before_install = "frappe.utils.install.before_install"
after_install = "frappe.utils.install.after_install"


+ 0
- 0
frappe/integrations/doctype/connected_app/__init__.py Целия файл


+ 38
- 0
frappe/integrations/doctype/connected_app/connected_app.js Целия файл

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

frappe.ui.form.on('Connected App', {
refresh: frm => {
frm.add_custom_button(__('Get OpenID Configuration'), async () => {
if (!frm.doc.openid_configuration) {
frappe.msgprint(__('Please enter OpenID Configuration URL'));
} else {
try {
const response = await fetch(frm.doc.openid_configuration);
const oidc = await response.json();
frm.set_value('authorization_uri', oidc.authorization_endpoint);
frm.set_value('token_uri', oidc.token_endpoint);
frm.set_value('userinfo_uri', oidc.userinfo_endpoint);
frm.set_value('introspection_uri', oidc.introspection_endpoint);
frm.set_value('revocation_uri', oidc.revocation_endpoint);
} catch (error) {
frappe.msgprint(__('Please check OpenID Configuration URL'));
}
}
});

if (!frm.is_new()) {
frm.add_custom_button(__('Connect to {}', [frm.doc.provider_name]), async () => {
frappe.call({
method: 'initiate_web_application_flow',
doc: frm.doc,
callback: function(r) {
window.open(r.message, '_blank');
}
});
});
}

frm.toggle_display('sb_client_credentials_section', !frm.is_new());
}
});

+ 166
- 0
frappe/integrations/doctype/connected_app/connected_app.json Целия файл

@@ -0,0 +1,166 @@
{
"actions": [],
"beta": 1,
"creation": "2019-01-24 15:51:06.362222",
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"provider_name",
"cb_00",
"openid_configuration",
"sb_client_credentials_section",
"client_id",
"redirect_uri",
"cb_01",
"client_secret",
"sb_scope_section",
"scopes",
"sb_endpoints_section",
"authorization_uri",
"token_uri",
"revocation_uri",
"cb_02",
"userinfo_uri",
"introspection_uri",
"section_break_18",
"query_parameters"
],
"fields": [
{
"fieldname": "provider_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Provider Name",
"reqd": 1
},
{
"fieldname": "cb_00",
"fieldtype": "Column Break"
},
{
"fieldname": "openid_configuration",
"fieldtype": "Data",
"label": "OpenID Configuration"
},
{
"collapsible": 1,
"fieldname": "sb_client_credentials_section",
"fieldtype": "Section Break",
"label": "Client Credentials"
},
{
"fieldname": "client_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Client Id"
},
{
"fieldname": "redirect_uri",
"fieldtype": "Data",
"label": "Redirect URI",
"read_only": 1
},
{
"fieldname": "cb_01",
"fieldtype": "Column Break"
},
{
"fieldname": "client_secret",
"fieldtype": "Password",
"label": "Client Secret"
},
{
"collapsible": 1,
"fieldname": "sb_scope_section",
"fieldtype": "Section Break",
"label": "Scopes"
},
{
"collapsible": 1,
"fieldname": "sb_endpoints_section",
"fieldtype": "Section Break",
"label": "Endpoints"
},
{
"fieldname": "cb_02",
"fieldtype": "Column Break"
},
{
"fieldname": "scopes",
"fieldtype": "Table",
"label": "Scopes",
"options": "OAuth Scope"
},
{
"fieldname": "authorization_uri",
"fieldtype": "Data",
"label": "Authorization URI"
},
{
"fieldname": "token_uri",
"fieldtype": "Data",
"label": "Token URI"
},
{
"fieldname": "revocation_uri",
"fieldtype": "Data",
"label": "Revocation URI"
},
{
"fieldname": "userinfo_uri",
"fieldtype": "Data",
"label": "Userinfo URI"
},
{
"fieldname": "introspection_uri",
"fieldtype": "Data",
"label": "Introspection URI"
},
{
"fieldname": "section_break_18",
"fieldtype": "Section Break",
"label": "Extra Parameters"
},
{
"fieldname": "query_parameters",
"fieldtype": "Table",
"label": "Query Parameters",
"options": "Query Parameters"
}
],
"links": [
{
"link_doctype": "Token Cache",
"link_fieldname": "connected_app"
}
],
"modified": "2020-11-16 16:29:50.277405",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Connected App",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"read": 1,
"role": "All"
}
],
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "provider_name",
"track_changes": 1
}

+ 133
- 0
frappe/integrations/doctype/connected_app/connected_app.py Целия файл

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

import os
from urllib.parse import urljoin
from urllib.parse import urlencode

import frappe
from frappe import _
from frappe.model.document import Document
from requests_oauthlib import OAuth2Session

if any((os.getenv('CI'), frappe.conf.developer_mode, frappe.conf.allow_tests)):
# Disable mandatory TLS in developer mode and tests
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'

class ConnectedApp(Document):
"""Connect to a remote oAuth Server. Retrieve and store user's access token
in a Token Cache.
"""

def validate(self):
base_url = frappe.utils.get_url()
callback_path = '/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.name
self.redirect_uri = urljoin(base_url, callback_path)

def get_oauth2_session(self, user=None, init=False):
token = None
token_updater = None

if not init:
user = user or frappe.session.user
token_cache = self.get_user_token(user)
token = token_cache.get_json()
token_updater = token_cache.update_data

return OAuth2Session(
client_id=self.client_id,
token=token,
token_updater=token_updater,
auto_refresh_url=self.token_uri,
redirect_uri=self.redirect_uri,
scope=self.get_scopes()
)

def initiate_web_application_flow(self, user=None, success_uri=None):
"""Return an authorization URL for the user. Save state in Token Cache."""
user = user or frappe.session.user
oauth = self.get_oauth2_session(init=True)
query_params = self.get_query_params()
authorization_url, state = oauth.authorization_url(self.authorization_uri, **query_params)
token_cache = self.get_token_cache(user)

if not token_cache:
token_cache = frappe.new_doc('Token Cache')
token_cache.user = user
token_cache.connected_app = self.name

token_cache.success_uri = success_uri
token_cache.state = state
token_cache.save(ignore_permissions=True)
frappe.db.commit()

return authorization_url

def get_user_token(self, user=None, success_uri=None):
"""Return an existing user token or initiate a Web Application Flow."""
user = user or frappe.session.user
token_cache = self.get_token_cache(user)

if token_cache:
return token_cache

redirect = self.initiate_web_application_flow(user, success_uri)
frappe.local.response['type'] = 'redirect'
frappe.local.response['location'] = redirect
return redirect

def get_token_cache(self, user):
token_cache = None
token_cache_name = self.name + '-' + user

if frappe.db.exists('Token Cache', token_cache_name):
token_cache = frappe.get_doc('Token Cache', token_cache_name)

return token_cache

def get_scopes(self):
return [row.scope for row in self.scopes]

def get_query_params(self):
return {param.key: param.value for param in self.query_parameters}


@frappe.whitelist(allow_guest=True)
def callback(code=None, state=None):
"""Handle client's code.

Called during the oauthorization flow by the remote oAuth2 server to
transmit a code that can be used by the local server to obtain an access
token.
"""
if frappe.request.method != 'GET':
frappe.throw(_('Invalid request method: {}').format(frappe.request.method))

if frappe.session.user == 'Guest':
frappe.local.response['type'] = 'redirect'
frappe.local.response['location'] = '/login?' + urlencode({'redirect-to': frappe.request.url})
return

path = frappe.request.path[1:].split('/')
if len(path) != 4 or not path[3]:
frappe.throw(_('Invalid Parameters.'))

connected_app = frappe.get_doc('Connected App', path[3])
token_cache = frappe.get_doc('Token Cache', connected_app.name + '-' + frappe.session.user)

if state != token_cache.state:
frappe.throw(_('Invalid state.'))

oauth_session = connected_app.get_oauth2_session(init=True)
query_params = connected_app.get_query_params()
token = oauth_session.fetch_token(connected_app.token_uri,
code=code,
client_secret=connected_app.get_password('client_secret'),
include_client_id=True,
**query_params
)
token_cache.update_data(token)

frappe.local.response['type'] = 'redirect'
frappe.local.response['location'] = token_cache.get('success_uri') or connected_app.get_url()

+ 162
- 0
frappe/integrations/doctype/connected_app/test_connected_app.py Целия файл

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

import unittest
import requests
from urllib.parse import urljoin

import frappe
from frappe.integrations.doctype.social_login_key.test_social_login_key import create_or_update_social_login_key


def get_user(usr, pwd):
user = frappe.new_doc('User')
user.email = usr
user.enabled = 1
user.first_name = "_Test"
user.new_password = pwd
user.roles = []
user.append('roles', {
'doctype': 'Has Role',
'parentfield': 'roles',
'role': 'System Manager'
})
user.insert()

return user


def get_connected_app():
doctype = 'Connected App'
connected_app = frappe.new_doc(doctype)
connected_app.provider_name = 'frappe'
connected_app.scopes = []
connected_app.append('scopes', {'scope': 'all'})
connected_app.insert()

return connected_app


def get_oauth_client():
oauth_client = frappe.new_doc('OAuth Client')
oauth_client.app_name = '_Test Connected App'
oauth_client.redirect_uris = 'to be replaced'
oauth_client.default_redirect_uri = 'to be replaced'
oauth_client.grant_type = 'Authorization Code'
oauth_client.response_type = 'Code'
oauth_client.skip_authorization = 1
oauth_client.insert()

return oauth_client


class TestConnectedApp(unittest.TestCase):

def setUp(self):
"""Set up a Connected App that connects to our own oAuth provider.

Frappe comes with it's own oAuth2 provider that we can test against. The
client credentials can be obtained from an "OAuth Client". All depends
on "Social Login Key" so we create one as well.

The redirect URIs from "Connected App" and "OAuth Client" have to match.
Frappe's "Authorization URL" and "Access Token URL" (actually they're
just endpoints) are stored in "Social Login Key" so we get them from
there.
"""
self.user_name = 'test-connected-app@example.com'
self.user_password = 'Eastern_43A1W'

self.user = get_user(self.user_name, self.user_password)
self.connected_app = get_connected_app()
self.oauth_client = get_oauth_client()
social_login_key = create_or_update_social_login_key()
self.base_url = social_login_key.get('base_url')

frappe.db.commit()
self.connected_app.reload()
self.oauth_client.reload()

redirect_uri = self.connected_app.get('redirect_uri')
self.oauth_client.update({
'redirect_uris': redirect_uri,
'default_redirect_uri': redirect_uri
})
self.oauth_client.save()

self.connected_app.update({
'authorization_uri': urljoin(self.base_url, social_login_key.get('authorize_url')),
'client_id': self.oauth_client.get('client_id'),
'client_secret': self.oauth_client.get('client_secret'),
'token_uri': urljoin(self.base_url, social_login_key.get('access_token_url'))
})
self.connected_app.save()

frappe.db.commit()
self.connected_app.reload()
self.oauth_client.reload()

def test_web_application_flow(self):
"""Simulate a logged in user who opens the authorization URL."""
def login():
return session.get(urljoin(self.base_url, '/api/method/login'), params={
'usr': self.user_name,
'pwd': self.user_password
})

session = requests.Session()

# first login of a new user on a new site fails with "401 UNAUTHORIZED"
# when anybody fixes that, the two lines below can be removed
first_login = login()
self.assertEqual(first_login.status_code, 401)

second_login = login()
self.assertEqual(second_login.status_code, 200)

authorization_url = self.connected_app.initiate_web_application_flow(user=self.user_name)

auth_response = session.get(authorization_url)
self.assertEqual(auth_response.status_code, 200)

callback_response = session.get(auth_response.url)
self.assertEqual(callback_response.status_code, 200)

self.token_cache = self.connected_app.get_token_cache(self.user_name)
token = self.token_cache.get_password('access_token')
self.assertNotEqual(token, None)

oauth2_session = self.connected_app.get_oauth2_session(self.user_name)
resp = oauth2_session.get(urljoin(self.base_url, '/api/method/frappe.auth.get_logged_user'))
self.assertEqual(resp.json().get('message'), self.user_name)

def tearDown(self):
def delete_if_exists(attribute):
doc = getattr(self, attribute, None)
if doc:
doc.delete()

delete_if_exists('token_cache')
delete_if_exists('connected_app')

if getattr(self, 'oauth_client', None):
tokens = frappe.get_all('OAuth Bearer Token', filters={
'client': self.oauth_client.name
})
for token in tokens:
doc = frappe.get_doc('OAuth Bearer Token', token.name)
doc.delete()

codes = frappe.get_all('OAuth Authorization Code', filters={
'client': self.oauth_client.name
})
for code in codes:
doc = frappe.get_doc('OAuth Authorization Code', code.name)
doc.delete()

delete_if_exists('user')
delete_if_exists('oauth_client')

frappe.db.commit()

+ 13
- 0
frappe/integrations/doctype/connected_app/test_records.json Целия файл

@@ -0,0 +1,13 @@
[
{
"doctype": "Connected App",
"provider_name": "frappe",
"client_id": "test_client_id",
"client_secret": "test_client_secret",
"scopes": [
{
"scope": "all"
}
]
}
]

+ 1
- 2
frappe/integrations/doctype/oauth_client/test_records.json Целия файл

@@ -1,7 +1,6 @@
[
{
"app_name": "_Test OAuth Client",
"client_id": "test_client_id",
"app_name": "_Test OAuth Client",
"client_secret": "test_client_secret",
"default_redirect_uri": "http://localhost",
"docstatus": 0,


+ 0
- 0
frappe/integrations/doctype/oauth_scope/__init__.py Целия файл


+ 30
- 0
frappe/integrations/doctype/oauth_scope/oauth_scope.json Целия файл

@@ -0,0 +1,30 @@
{
"actions": [],
"creation": "2020-07-15 22:08:14.616585",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"scope"
],
"fields": [
{
"fieldname": "scope",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Scope"
}
],
"istable": 1,
"links": [],
"modified": "2020-07-15 22:15:18.930632",
"modified_by": "Administrator",
"module": "Integrations",
"name": "OAuth Scope",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

+ 10
- 0
frappe/integrations/doctype/oauth_scope/oauth_scope.py Целия файл

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

from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document

class OAuthScope(Document):
pass

+ 0
- 0
frappe/integrations/doctype/query_parameters/__init__.py Целия файл


+ 37
- 0
frappe/integrations/doctype/query_parameters/query_parameters.json Целия файл

@@ -0,0 +1,37 @@
{
"actions": [],
"creation": "2020-11-16 14:54:37.226914",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"key",
"value"
],
"fields": [
{
"fieldname": "key",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Key",
"reqd": 1
},
{
"fieldname": "value",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Value",
"reqd": 1
}
],
"istable": 1,
"links": [],
"modified": "2020-11-16 15:18:35.887149",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Query Parameters",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
}

+ 10
- 0
frappe/integrations/doctype/query_parameters/query_parameters.py Целия файл

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

from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document

class QueryParameters(Document):
pass

+ 14
- 0
frappe/integrations/doctype/social_login_key/test_social_login_key.py Целия файл

@@ -22,3 +22,17 @@ def make_social_login_key(**kwargs):
kwargs["provider_name"] = "Test OAuth2 Provider"
doc = frappe.get_doc(kwargs)
return doc

def create_or_update_social_login_key():
# used in other tests (connected app, oauth20)
try:
social_login_key = frappe.get_doc("Social Login Key", "frappe")
except frappe.DoesNotExistError:
social_login_key = frappe.new_doc("Social Login Key")
social_login_key.get_social_login_provider("Frappe", initialize=True)
social_login_key.base_url = frappe.utils.get_url()
social_login_key.enable_social_login = 0
social_login_key.save()
frappe.db.commit()

return social_login_key

+ 0
- 0
frappe/integrations/doctype/token_cache/__init__.py Целия файл


+ 18
- 0
frappe/integrations/doctype/token_cache/test_records.json Целия файл

@@ -0,0 +1,18 @@
[
{
"doctype": "Token Cache",
"user": "test@example.com",
"access_token": "test-access-token",
"refresh_token": "test-refresh-token",
"token_type": "Bearer",
"expires_in": 1000,
"scopes": [
{
"scope": "all"
},
{
"scope": "openid"
}
]
}
]

+ 37
- 0
frappe/integrations/doctype/token_cache/test_token_cache.py Целия файл

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

import unittest
import frappe

test_dependencies = ['User', 'Connected App', 'Token Cache']

class TestTokenCache(unittest.TestCase):

def setUp(self):
self.token_cache = frappe.get_last_doc('Token Cache')
self.token_cache.update({'connected_app': frappe.get_last_doc('Connected App').name})
self.token_cache.save()

def test_get_auth_header(self):
self.token_cache.get_auth_header()

def test_update_data(self):
self.token_cache.update_data({
'access_token': 'new-access-token',
'refresh_token': 'new-refresh-token',
'token_type': 'bearer',
'expires_in': 2000,
'scope': 'new scope'
})

def test_get_expires_in(self):
self.token_cache.get_expires_in()

def test_is_expired(self):
self.token_cache.is_expired()

def get_json(self):
self.token_cache.get_json()

+ 8
- 0
frappe/integrations/doctype/token_cache/token_cache.js Целия файл

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

frappe.ui.form.on('Token Cache', {
// refresh: function(frm) {

// }
});

+ 110
- 0
frappe/integrations/doctype/token_cache/token_cache.json Целия файл

@@ -0,0 +1,110 @@
{
"actions": [],
"autoname": "format:{connected_app}-{user}",
"beta": 1,
"creation": "2019-01-24 16:56:55.631096",
"doctype": "DocType",
"document_type": "System",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"user",
"connected_app",
"provider_name",
"access_token",
"refresh_token",
"expires_in",
"state",
"scopes",
"success_uri",
"token_type"
],
"fields": [
{
"fieldname": "user",
"fieldtype": "Link",
"label": "User",
"options": "User",
"read_only": 1
},
{
"fieldname": "connected_app",
"fieldtype": "Link",
"label": "Connected App",
"options": "Connected App",
"read_only": 1
},
{
"fieldname": "access_token",
"fieldtype": "Password",
"label": "Access Token",
"read_only": 1
},
{
"fieldname": "refresh_token",
"fieldtype": "Password",
"label": "Refresh Token",
"read_only": 1
},
{
"fieldname": "expires_in",
"fieldtype": "Int",
"label": "Expires In",
"read_only": 1
},
{
"fieldname": "state",
"fieldtype": "Data",
"label": "State",
"read_only": 1
},
{
"fieldname": "scopes",
"fieldtype": "Table",
"label": "Scopes",
"options": "OAuth Scope",
"read_only": 1
},
{
"fieldname": "success_uri",
"fieldtype": "Data",
"label": "Success URI",
"read_only": 1
},
{
"fieldname": "token_type",
"fieldtype": "Data",
"label": "Token Type",
"read_only": 1
},
{
"fetch_from": "connected_app.provider_name",
"fieldname": "provider_name",
"fieldtype": "Data",
"label": "Provider Name",
"read_only": 1
}
],
"links": [],
"modified": "2020-11-13 13:35:53.714352",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Token Cache",
"owner": "Administrator",
"permissions": [
{
"delete": 1,
"read": 1,
"role": "System Manager"
},
{
"delete": 1,
"if_owner": 1,
"read": 1,
"role": "All"
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

+ 67
- 0
frappe/integrations/doctype/token_cache/token_cache.py Целия файл

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

from __future__ import unicode_literals
from datetime import datetime, timedelta

import frappe
from frappe import _
from frappe.utils import cstr, cint
from frappe.model.document import Document

class TokenCache(Document):

def get_auth_header(self):
if self.access_token:
headers = {'Authorization': 'Bearer ' + self.get_password('access_token')}
return headers

raise frappe.exceptions.DoesNotExistError

def update_data(self, data):
"""
Store data returned by authorization flow.

Params:
data - Dict with access_token, refresh_token, expires_in and scope.
"""
token_type = cstr(data.get('token_type', '')).lower()
if token_type not in ['bearer', 'mac']:
frappe.throw(_('Received an invalid token type.'))
# 'Bearer' or 'MAC'
token_type = token_type.title() if token_type == 'bearer' else token_type.upper()

self.token_type = token_type
self.access_token = cstr(data.get('access_token', ''))
self.refresh_token = cstr(data.get('refresh_token', ''))
self.expires_in = cint(data.get('expires_in', 0))

new_scopes = data.get('scope')
if new_scopes:
if isinstance(new_scopes, str):
new_scopes = new_scopes.split(' ')
if isinstance(new_scopes, list):
self.scopes = None
for scope in new_scopes:
self.append('scopes', {'scope': scope})

self.state = None
self.save(ignore_permissions=True)
frappe.db.commit()
return self

def get_expires_in(self):
expiry_time = frappe.utils.get_datetime(self.modified) + timedelta(self.expires_in)
return (datetime.now() - expiry_time).total_seconds()

def is_expired(self):
return self.get_expires_in() < 0

def get_json(self):
return {
'access_token': self.get_password('access_token', ''),
'refresh_token': self.get_password('refresh_token', ''),
'expires_in': self.get_expires_in(),
'token_type': self.token_type
}

+ 1
- 1
frappe/integrations/doctype/webhook/webhook.py Целия файл

@@ -85,7 +85,7 @@ def enqueue_webhook(doc, webhook):

for i in range(3):
try:
r = requests.post(webhook.request_url, data=json.dumps(data), headers=headers, timeout=5)
r = requests.post(webhook.request_url, data=json.dumps(data, default=str), headers=headers, timeout=5)
r.raise_for_status()
frappe.logger().debug({"webhook_success": r.text})
break


+ 1
- 0
frappe/integrations/oauth2.py Целия файл

@@ -20,6 +20,7 @@ def get_oauth_server():
return frappe.local.oauth_server

def sanitize_kwargs(param_kwargs):
"""Remove 'data' and 'cmd' keys, if present."""
arguments = param_kwargs
arguments.pop('data', None)
arguments.pop('cmd', None)


+ 8
- 5
frappe/model/base_document.py Целия файл

@@ -48,7 +48,7 @@ def get_controller(doctype):
else:
class_overrides = frappe.get_hooks('override_doctype_class')
if class_overrides and class_overrides.get(doctype):
import_path = frappe.get_hooks('override_doctype_class').get(doctype)[-1]
import_path = class_overrides[doctype][-1]
module_path, classname = import_path.rsplit('.', 1)
module = frappe.get_module(module_path)
if not hasattr(module, classname):
@@ -69,10 +69,13 @@ def get_controller(doctype):

if frappe.local.dev_server:
return _get_controller()

key = '{}:doctype_classes'.format(frappe.local.site)
return frappe.cache().hget(key, doctype, generator=_get_controller, shared=True)

site_controllers = frappe.controllers.setdefault(frappe.local.site, {})
if doctype not in site_controllers:
site_controllers[doctype] = _get_controller()
return site_controllers[doctype]
class BaseDocument(object):
ignore_in_getter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns")



+ 9
- 3
frappe/model/db_query.py Целия файл

@@ -40,7 +40,10 @@ class DatabaseQuery(object):
ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False,
update=None, add_total_row=None, user_settings=None, reference_doctype=None,
return_query=False, strict=True, pluck=None, ignore_ddl=False):
if not ignore_permissions and not frappe.has_permission(self.doctype, "read", user=user):
if not ignore_permissions and \
not frappe.has_permission(self.doctype, "select", user=user) and \
not frappe.has_permission(self.doctype, "read", user=user):

frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(self.doctype))
raise frappe.PermissionError(self.doctype)

@@ -315,7 +318,10 @@ class DatabaseQuery(object):
def append_table(self, table_name):
self.tables.append(table_name)
doctype = table_name[4:-1]
if (not self.flags.ignore_permissions) and (not frappe.has_permission(doctype)):
ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read'

if (not self.flags.ignore_permissions) and\
(not frappe.has_permission(doctype, ptype=ptype)):
frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(doctype))
raise frappe.PermissionError(doctype)

@@ -576,7 +582,7 @@ class DatabaseQuery(object):
self.shared = frappe.share.get_shared(self.doctype, self.user)

if (not meta.istable and
not role_permissions.get("read") and
not (role_permissions.get("select") or role_permissions.get("read")) and
not self.flags.ignore_permissions and
not has_any_user_permission_for_doctype(self.doctype, self.user, self.reference_doctype)):
only_if_shared = True


+ 4
- 2
frappe/model/document.py Целия файл

@@ -939,15 +939,17 @@ class Document(BaseDocument):
self.load_doc_before_save()
self.reset_seen()

# before_validate method should be executed before ignoring validations
if self._action in ("save", "submit"):
self.run_method("before_validate")

if self.flags.ignore_validate:
return

if self._action=="save":
self.run_method("before_validate")
self.run_method("validate")
self.run_method("before_save")
elif self._action=="submit":
self.run_method("before_validate")
self.run_method("validate")
self.run_method("before_submit")
elif self._action=="cancel":


+ 3
- 1
frappe/model/meta.py Целия файл

@@ -68,7 +68,7 @@ def load_doctype_from_file(doctype):
class Meta(Document):
_metaclass = True
default_fields = list(default_fields)[1:]
special_doctypes = ("DocField", "DocPerm", "Role", "DocType", "Module Def", 'DocType Action', 'DocType Link')
special_doctypes = ("DocField", "DocPerm", "DocType", "Module Def", 'DocType Action', 'DocType Link')

def __init__(self, doctype):
self._fields = {}
@@ -484,6 +484,8 @@ class Meta(Document):
if not data.transactions:
# init groups
data.transactions = []

if not data.non_standard_fieldnames:
data.non_standard_fieldnames = {}

for link in dashboard_links:


+ 10
- 2
frappe/model/rename_doc.py Целия файл

@@ -21,8 +21,16 @@ def update_document_title(doctype, docname, title_field=None, old_title=None, ne
docname = rename_doc(doctype=doctype, old=docname, new=new_name, merge=merge)

if old_title and new_title and not old_title == new_title:
frappe.db.set_value(doctype, docname, title_field, new_title)
frappe.msgprint(_('Saved'), alert=True, indicator='green')
try:
frappe.db.set_value(doctype, docname, title_field, new_title)
frappe.msgprint(_('Saved'), alert=True, indicator='green')
except Exception as e:
if frappe.db.is_duplicate_entry(e):
frappe.throw(
_("{0} {1} already exists").format(doctype, frappe.bold(docname)),
title=_("Duplicate Name"),
exc=frappe.DuplicateEntryError
)

return docname



+ 2
- 3
frappe/model/workflow.py Целия файл

@@ -120,9 +120,8 @@ def apply_workflow(doc, action):
return doc

@frappe.whitelist()
def can_cancel_document(doc):
doc = frappe.get_doc(frappe.parse_json(doc))
workflow = get_workflow(doc.doctype)
def can_cancel_document(doctype):
workflow = get_workflow(doctype)
for state_doc in workflow.states:
if state_doc.doc_status == '2':
for transition in workflow.transitions:


+ 1
- 0
frappe/patches.txt Целия файл

@@ -326,3 +326,4 @@ execute:frappe.delete_doc_if_exists('Page', 'dashboard', force=1)
frappe.core.doctype.page.patches.drop_unused_pages
execute:frappe.get_doc('Role', 'Guest').save() # remove desk access
frappe.patches.v13_0.rename_desk_page_to_workspace
frappe.patches.v13_0.delete_package_publish_tool

+ 11
- 0
frappe/patches/v13_0/delete_package_publish_tool.py Целия файл

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

from __future__ import unicode_literals
import frappe


def execute():
frappe.delete_doc("DocType", "Package Publish Tool", ignore_missing=True)
frappe.delete_doc("DocType", "Package Document Type", ignore_missing=True)
frappe.delete_doc("DocType", "Package Publish Target", ignore_missing=True)

+ 14
- 0
frappe/patches/v13_0/website_theme_custom_scss.py Целия файл

@@ -2,9 +2,23 @@ import frappe

def execute():
frappe.reload_doctype('Website Theme')
frappe.reload_doc('website', 'doctype', 'website_theme_ignore_app')
frappe.reload_doc('website', 'doctype', 'color')

for theme in frappe.get_all('Website Theme'):
doc = frappe.get_doc('Website Theme', theme.name)
if not doc.get('custom_scss') and doc.theme_scss:
# move old theme to new theme
doc.custom_scss = doc.theme_scss

if doc.background_color:
setup_color_record(doc.background_color)

doc.save()

def setup_color_record(color):
frappe.get_doc({
"doctype": "Color",
"__newname": color,
"color": color,
}).save()

+ 4
- 3
frappe/permissions.py Целия файл

@@ -7,7 +7,7 @@ import frappe, copy, json
from frappe import _, msgprint
from frappe.utils import cint
import frappe.share
rights = ("read", "write", "create", "delete", "submit", "cancel", "amend",
rights = ("select", "read", "write", "create", "delete", "submit", "cancel", "amend",
"print", "email", "report", "import", "export", "set_user_permissions", "share")

# TODO:
@@ -73,6 +73,7 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None, ra

role_permissions = get_role_permissions(meta, user=user)
perm = role_permissions.get(ptype)

if not perm:
push_perm_check_log(_('User {0} does not have doctype access via role permission for document {1}').format(frappe.bold(user), frappe.bold(doctype)))

@@ -192,9 +193,9 @@ def get_role_permissions(doctype_meta, user=None):
and ptype != 'create'):
perms['if_owner'][ptype] = 1
# has no access if not owner
# only provide read access so that user is able to at-least access list
# only provide select or read access so that user is able to at-least access list
# (and the documents will be filtered based on owner sin further checks)
perms[ptype] = 1 if ptype == 'read' else 0
perms[ptype] = 1 if ptype in ['select', 'read'] else 0

frappe.local.role_permissions[cache_key] = perms



+ 1
- 0
frappe/public/build.json Целия файл

@@ -261,6 +261,7 @@
"public/js/frappe/views/calendar/calendar.js",
"public/js/frappe/views/dashboard/dashboard_view.js",
"public/js/frappe/views/image/image_view.js",
"public/js/frappe/views/map/map_view.js",
"public/js/frappe/views/kanban/kanban_view.js",
"public/js/frappe/views/inbox/inbox_view.js",
"public/js/frappe/views/file/file_view.js",


+ 7
- 0
frappe/public/css/list.css Целия файл

@@ -401,6 +401,13 @@ input.list-row-checkbox {
.pswp__more-item img {
max-height: 100%;
}
.map-view-container {
display: flex;
flex-wrap: wrap;
width: 100%;
height: calc(100vh - 284px);
z-index: 0;
}
.list-paging-area .gantt-view-mode {
margin-left: 15px;
margin-right: 15px;


+ 22
- 5
frappe/public/js/frappe/form/controls/base_control.js Целия файл

@@ -40,23 +40,31 @@ frappe.ui.form.Control = Class.extend({
return this.df.get_status(this);
}

if((!this.doctype && !this.docname) || this.df.parenttype === 'Web Form') {
if ((!this.doctype && !this.docname) || this.df.parenttype === 'Web Form' || this.df.is_web_form) {
// like in case of a dialog box
if (cint(this.df.hidden)) {
// eslint-disable-next-line
if(explain) console.log("By Hidden: None");
if (explain) console.log("By Hidden: None"); // eslint-disable-line no-console
return "None";

} else if (cint(this.df.hidden_due_to_dependency)) {
// eslint-disable-next-line
if(explain) console.log("By Hidden Dependency: None");
if(explain) console.log("By Hidden Dependency: None"); // eslint-disable-line no-console
return "None";

} else if (cint(this.df.read_only)) {
// eslint-disable-next-line
if(explain) console.log("By Read Only: Read");
if (explain) console.log("By Read Only: Read"); // eslint-disable-line no-console
return "Read";

} else if ((this.grid &&
this.grid.display_status == 'Read') ||
(this.layout &&
this.layout.grid &&
this.layout.grid.display_status == 'Read')) {
// parent grid is read
if (explain) console.log("By Parent Grid Read-only: Read"); // eslint-disable-line no-console
return "Read";
}

return "Write";
@@ -65,13 +73,22 @@ frappe.ui.form.Control = Class.extend({
var status = frappe.perm.get_field_display_status(this.df,
frappe.model.get_doc(this.doctype, this.docname), this.perm || (this.frm && this.frm.perm), explain);

// Match parent grid controls read only status
if (status === 'Write' && (this.grid || (this.layout && this.layout.grid))) {
var grid = this.grid || this.layout.grid;
if (grid.display_status == 'Read') {
status = 'Read';
if (explain) console.log("By Parent Grid Read-only: Read"); // eslint-disable-line no-console
}
}

// hide if no value
if (this.doctype && status==="Read" && !this.only_input
&& is_null(frappe.model.get_value(this.doctype, this.docname, this.df.fieldname))
&& !in_list(["HTML", "Image", "Button"], this.df.fieldtype)) {

// eslint-disable-next-line
if(explain) console.log("By Hide Read-only, null fields: None");
if (explain) console.log("By Hide Read-only, null fields: None"); // eslint-disable-line no-console
status = "None";
}



+ 1
- 1
frappe/public/js/frappe/form/controls/base_input.js Целия файл

@@ -20,7 +20,7 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
<div class="control-input-wrapper">\
<div class="control-input"></div>\
<div class="control-value like-disabled-input" style="display: none;"></div>\
<p class="help-box small text-muted hidden-xs"></p>\
<p class="help-box small text-muted"></p>\
</div>\
</div>\
</div>').appendTo(this.parent);


+ 6
- 4
frappe/public/js/frappe/form/controls/geolocation.js Целия файл

@@ -1,3 +1,5 @@
frappe.provide('frappe.utils.utils');

frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({
horizontal: false,

@@ -90,11 +92,11 @@ frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({
});

L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/';
this.map = L.map(this.map_id).setView([19.0800, 72.8961], 13);
this.map = L.map(this.map_id).setView(frappe.utils.map_defaults.center,
frappe.utils.map_defaults.zoom);

L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo(this.map);
L.tileLayer(frappe.utils.map_defaults.tiles,
frappe.utils.map_defaults.options).addTo(this.map);
},

bind_leaflet_locate_control() {


+ 11
- 0
frappe/public/js/frappe/form/controls/link.js Целия файл

@@ -51,6 +51,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
this.translate_values = true;
this.setup_buttons();
this.setup_awesomeplete();
this.bind_change_event();
},
get_options: function() {
return this.df.options;
@@ -217,6 +218,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
}
me.$input.cache[doctype][term] = r.results;
me.awesomplete.list = me.$input.cache[doctype][term];
me.toggle_href(doctype);
}
});
}, 500));
@@ -303,6 +305,15 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
// returns [{value: 'Manufacturer 1', 'description': 'mobile part 1, mobile part 2'}]
},

toggle_href(doctype) {
if (frappe.model.can_select(doctype) && !frappe.model.can_read(doctype)) {
// remove href from link field as user has only select perm
this.$input_area.find(".link-btn").addClass('hide');
} else {
this.$input_area.find(".link-btn").removeClass('hide');
}
},

get_filter_description(filters) {
let doctype = this.get_options();
let filter_array = [];


+ 1
- 1
frappe/public/js/frappe/form/controls/rating.js Целия файл

@@ -47,7 +47,7 @@ frappe.ui.form.ControlRating = frappe.ui.form.ControlInt.extend({
});
},
get_value() {
return cint(this.value);
return cint(this.value, null);
},
set_formatted_input(value) {
let el = $(this.input_area).find('i');


+ 2
- 1
frappe/public/js/frappe/form/controls/table.js Целия файл

@@ -9,7 +9,8 @@ frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({
frm: this.frm,
df: this.df,
perm: this.perm || (this.frm && this.frm.perm) || this.df.perm,
parent: this.wrapper
parent: this.wrapper,
control: this
});
if(this.frm) {
this.frm.grids[this.frm.grids.length] = this;


+ 4
- 1
frappe/public/js/frappe/form/form.js Целия файл

@@ -1264,7 +1264,10 @@ frappe.ui.form.Form = class FrappeForm {
}
if (df && df[property] != value) {
df[property] = value;
this.refresh_field(fieldname);
if (!docname || !table_field) {
// do not refresh childtable fields since `this.fields_dict` doesn't have child table fields
this.refresh_field(fieldname);
}
}
}



+ 9
- 5
frappe/public/js/frappe/form/formatters.js Целия файл

@@ -128,11 +128,15 @@ frappe.form.formatters = {
return repl('<a onclick="%(onclick)s">%(value)s</a>',
{onclick: docfield.link_onclick.replace(/"/g, '&quot;'), value:value});
} else if(docfield && doctype) {
return `<a
href="/app/${encodeURIComponent(frappe.router.slug(doctype))}/${encodeURIComponent(original_value)}"
data-doctype="${doctype}"
data-name="${original_value}">
${__(options && options.label || value)}</a>`
if (!frappe.model.can_select(doctype) && frappe.model.can_read(doctype)) {
return `<a
href="/app/${encodeURIComponent(frappe.router.slug(doctype))}/${encodeURIComponent(original_value)}"
data-doctype="${doctype}"
data-name="${original_value}">
${__(options && options.label || value)}</a>`
} else {
return value;
}
} else {
return value;
}


+ 2
- 0
frappe/public/js/frappe/form/grid.js Целия файл

@@ -280,6 +280,8 @@ export default class Grid {
if (this.frm) {
this.display_status = frappe.perm.get_field_display_status(this.df, this.frm.doc,
this.perm);
} else if (this.df.is_web_form && this.control) {
this.display_status = this.control.get_status();
} else {
// not in form
this.display_status = 'Write';


+ 3
- 0
frappe/public/js/frappe/form/grid_row_form.js Целия файл

@@ -16,6 +16,9 @@ export default class GridRowForm {
body: this.form_area,
no_submit_on_enter: true,
frm: this.row.frm,
grid: this.row.grid,
grid_row: this.row,
grid_row_form: this,
});
this.layout.make();



+ 61
- 58
frappe/public/js/frappe/form/layout.js Целия файл

@@ -1,7 +1,7 @@
import '../class';

frappe.ui.form.Layout = Class.extend({
init: function(opts) {
init: function (opts) {
this.views = {};
this.pages = [];
this.sections = [];
@@ -87,7 +87,7 @@ frappe.ui.form.Layout = Class.extend({
this.message.empty().addClass('hidden');
}
},
render: function(new_fields) {
render: function (new_fields) {
var me = this;
var fields = new_fields || this.fields;

@@ -101,8 +101,8 @@ frappe.ui.form.Layout = Class.extend({
if (this.no_opening_section()) {
this.make_section();
}
$.each(fields, function(i, df) {
switch(df.fieldtype) {
$.each(fields, function (i, df) {
switch (df.fieldtype) {
case "Fold":
me.make_page(df);
break;
@@ -119,17 +119,17 @@ frappe.ui.form.Layout = Class.extend({

},

no_opening_section: function() {
return (this.fields[0] && this.fields[0].fieldtype!="Section Break") || !this.fields.length;
no_opening_section: function () {
return (this.fields[0] && this.fields[0].fieldtype != "Section Break") || !this.fields.length;
},

setup_dashboard_section: function() {
setup_dashboard_section: function () {
if (this.no_opening_section()) {
this.fields.unshift({fieldtype: 'Section Break'});
}
},

replace_field: function(fieldname, df, render) {
replace_field: function (fieldname, df, render) {
df.fieldname = fieldname; // change of fieldname is avoided
if (this.fields_dict[fieldname] && this.fields_dict[fieldname].df) {
const fieldobj = this.init_field(df, render);
@@ -145,7 +145,7 @@ frappe.ui.form.Layout = Class.extend({
}
},

make_field: function(df, colspan, render) {
make_field: function (df, colspan, render) {
!this.section && this.make_section();
!this.column && this.make_column();

@@ -161,29 +161,30 @@ frappe.ui.form.Layout = Class.extend({
fieldobj.section = this.section;
},

init_field: function(df, render = false) {
init_field: function (df, render = false) {
const fieldobj = frappe.ui.form.make_control({
df: df,
doctype: this.doctype,
parent: this.column.wrapper.get(0),
frm: this.frm,
render_input: render,
doc: this.doc
doc: this.doc,
layout: this
});

fieldobj.layout = this;
return fieldobj;
},

make_page: function(df) {
make_page: function (df) { // eslint-disable-line no-unused-vars
var me = this,
head = $('<div class="form-clickable-section text-center">\
<a class="btn-fold h6 text-muted">'+__("Show more details")+'</a>\
<a class="btn-fold h6 text-muted">' + __("Show more details") + '</a>\
</div>').appendTo(this.wrapper);

this.page = $('<div class="form-page second-page hide"></div>').appendTo(this.wrapper);

this.fold_btn = head.find(".btn-fold").on("click", function() {
this.fold_btn = head.find(".btn-fold").on("click", function () {
var page = $(this).parent().next();
if (page.hasClass("hide")) {
$(this).removeClass("btn-fold").html(__("Hide details"));
@@ -201,11 +202,11 @@ frappe.ui.form.Layout = Class.extend({
this.folded = true;
},

unfold: function() {
unfold: function () {
this.fold_btn.trigger('click');
},

make_section: function(df) {
make_section: function (df) {
this.section = new frappe.ui.form.Section(this, df);

// append to layout fields
@@ -217,14 +218,14 @@ frappe.ui.form.Layout = Class.extend({
this.column = null;
},

make_column: function(df) {
make_column: function (df) {
this.column = new frappe.ui.form.Column(this.section, df);
if (df && df.fieldname) {
this.fields_list.push(this.column);
}
},

refresh: function(doc) {
refresh: function (doc) {
var me = this;
if (doc) this.doc = doc;

@@ -267,7 +268,7 @@ frappe.ui.form.Layout = Class.extend({

},

refresh_fields: function(fields) {
refresh_fields: function (fields) {
let fieldnames = fields.map((field) => {
if (field.fieldname) return field.fieldname;
});
@@ -282,15 +283,15 @@ frappe.ui.form.Layout = Class.extend({
});
},

add_fields: function(fields) {
add_fields: function (fields) {
this.render(fields);
this.refresh_fields(fields);
},

refresh_section_collapse: function() {
refresh_section_collapse: function () {
if (!this.doc) return;

for (var i=0; i<this.sections.length; i++) {
for (var i = 0; i < this.sections.length; i++) {
var section = this.sections[i];
var df = section.df;
if (df && df.collapsible) {
@@ -309,9 +310,9 @@ frappe.ui.form.Layout = Class.extend({
}
},

attach_doc_and_docfields: function(refresh) {
attach_doc_and_docfields: function (refresh) {
var me = this;
for (var i=0, l=this.fields_list.length; i<l; i++) {
for (var i = 0, l = this.fields_list.length; i < l; i++) {
var fieldobj = this.fields_list[i];
if (me.doc) {
fieldobj.doc = me.doc;
@@ -329,15 +330,15 @@ frappe.ui.form.Layout = Class.extend({
}
},

refresh_section_count: function() {
this.wrapper.find(".section-count-label:visible").each(function(i) {
$(this).html(i+1);
refresh_section_count: function () {
this.wrapper.find(".section-count-label:visible").each(function (i) {
$(this).html(i + 1);
});
},
setup_tabbing: function() {
setup_tabbing: function () {
var me = this;
this.wrapper.on("keydown", function(ev) {
if (ev.which==9) {
this.wrapper.on("keydown", function (ev) {
if (ev.which == 9) {
var current = $(ev.target),
doctype = current.attr("data-doctype"),
fieldname = current.attr("data-fieldname");
@@ -346,7 +347,7 @@ frappe.ui.form.Layout = Class.extend({
}
});
},
handle_tab: function(doctype, fieldname, shift) {
handle_tab: function (doctype, fieldname, shift) {
var me = this,
grid_row = null,
prev = null,
@@ -363,8 +364,8 @@ frappe.ui.form.Layout = Class.extend({
fields = grid_row.layout.fields_list;
}

for (var i=0, len=fields.length; i < len; i++) {
if (fields[i].df.fieldname==fieldname) {
for (var i = 0, len = fields.length; i < len; i++) {
if (fields[i].df.fieldname == fieldname) {
if (shift) {
if (prev) {
this.set_focus(prev);
@@ -373,7 +374,7 @@ frappe.ui.form.Layout = Class.extend({
}
break;
}
if (i < len-1) {
if (i < len - 1) {
focused = me.focus_on_next_field(i, fields);
}

@@ -389,9 +390,9 @@ frappe.ui.form.Layout = Class.extend({
// last field in this group
if (grid_row) {
// in grid
if (grid_row.doc.idx==grid_row.grid.grid_rows.length) {
if (grid_row.doc.idx == grid_row.grid.grid_rows.length) {
// last row, close it and find next field
grid_row.toggle_view(false, function() {
grid_row.toggle_view(false, function () {
grid_row.grid.frm.layout.handle_tab(grid_row.grid.df.parent, grid_row.grid.df.fieldname);
});
} else {
@@ -405,12 +406,12 @@ frappe.ui.form.Layout = Class.extend({

return false;
},
focus_on_next_field: function(start_idx, fields) {
focus_on_next_field: function (start_idx, fields) {
// loop to find next eligible fields
for (var i= start_idx + 1, len = fields.length; i < len; i++) {
for (var i = start_idx + 1, len = fields.length; i < len; i++) {
var field = fields[i];
if (this.is_visible(field)) {
if (field.df.fieldtype==="Table") {
if (field.df.fieldtype === "Table") {
// open table grid
if (!(field.grid.grid_rows && field.grid.grid_rows.length)) {
// empty grid, add a new row
@@ -427,10 +428,10 @@ frappe.ui.form.Layout = Class.extend({
}
}
},
is_visible: function(field) {
return field.disp_status==="Write" && (field.$wrapper && field.$wrapper.is(":visible"));
is_visible: function (field) {
return field.disp_status === "Write" && (field.$wrapper && field.$wrapper.is(":visible"));
},
set_focus: function(field) {
set_focus: function (field) {
// next is table, show the table
if (field.df.fieldtype=="Table") {
if (!field.grid.grid_rows.length) {
@@ -444,10 +445,10 @@ frappe.ui.form.Layout = Class.extend({
field.$input.focus();
}
},
get_open_grid_row: function() {
get_open_grid_row: function () {
return $(".grid-row-open").data("grid_row");
},
refresh_dependency: function() {
refresh_dependency: function () {
// Resolve "depends_on" and show / hide accordingly
var me = this;

@@ -465,7 +466,7 @@ frappe.ui.form.Layout = Class.extend({
if (!has_dep) return;

// show / hide based on values
for (var i=me.fields_list.length-1;i>=0;i--) {
for (var i = me.fields_list.length - 1; i >= 0; i--) {
var f = me.fields_list[i];
f.guardian_has_value = true;
if (f.df.depends_on) {
@@ -498,14 +499,14 @@ frappe.ui.form.Layout = Class.extend({

this.refresh_section_count();
},
set_dependant_property: function(condition, fieldname, property) {
set_dependant_property: function (condition, fieldname, property) {
let set_property = this.evaluate_depends_on_value(condition);
let value = set_property ? 1 : 0;
let form_obj;

if (this.frm) {
form_obj = this.frm;
} else if (this.is_dialog) {
} else if (this.is_dialog || this.doctype === 'Web Form') {
form_obj = this;
}
if (form_obj) {
@@ -513,12 +514,14 @@ frappe.ui.form.Layout = Class.extend({
form_obj.setting_dependency = true;
form_obj.set_df_property(this.doc.parentfield, property, value, this.doc.parent, fieldname);
form_obj.setting_dependency = false;
// refresh child fields
this.fields_dict[fieldname] && this.fields_dict[fieldname].refresh();
} else {
form_obj.set_df_property(fieldname, property, value);
}
}
},
evaluate_depends_on_value: function(expression) {
evaluate_depends_on_value: function (expression) {
var out = null;
var doc = this.doc;

@@ -544,7 +547,7 @@ frappe.ui.form.Layout = Class.extend({
if (parent && parent.istable && expression.includes('is_submittable')) {
out = true;
}
} catch(e) {
} catch (e) {
frappe.throw(__('Invalid "depends_on" expression'));
}

@@ -640,7 +643,7 @@ frappe.ui.form.Section = Class.extend({

this.wrapper.toggleClass("hide-control", !!hide);
},
collapse: function(hide) {
collapse: function (hide) {
// unknown edge case
if (!(this.head && this.body)) {
return;
@@ -659,7 +662,7 @@ frappe.ui.form.Section = Class.extend({

// refresh signature fields
this.fields_list.forEach((f) => {
if (f.df.fieldtype=='Signature') {
if (f.df.fieldtype == 'Signature') {
f.refresh();
}
});
@@ -669,11 +672,11 @@ frappe.ui.form.Section = Class.extend({
return this.body.hasClass('hide');
},

has_missing_mandatory: function() {
has_missing_mandatory: function () {
var missing_mandatory = false;
for (var j=0, l=this.fields_list.length; j < l; j++) {
for (var j = 0, l = this.fields_list.length; j < l; j++) {
var section_df = this.fields_list[j].df;
if (section_df.reqd && this.layout.doc[section_df.fieldname]==null) {
if (section_df.reqd && this.layout.doc[section_df.fieldname] == null) {
missing_mandatory = true;
break;
}
@@ -691,13 +694,13 @@ frappe.ui.form.Column = Class.extend({
this.make();
this.resize_all_columns();
},
make: function() {
make: function () {
this.wrapper = $('<div class="form-column">\
<form>\
</form>\
</div>').appendTo(this.section.body)
.find("form")
.on("submit", function() {
.on("submit", function () {
return false;
});

@@ -706,7 +709,7 @@ frappe.ui.form.Column = Class.extend({
+ '</label>').appendTo(this.wrapper);
}
},
resize_all_columns: function() {
resize_all_columns: function () {
// distribute all columns equally
var colspan = cint(12 / this.section.wrapper.find(".form-column").length);

@@ -715,7 +718,7 @@ frappe.ui.form.Column = Class.extend({
.addClass("col-sm-" + colspan);

},
refresh: function() {
refresh: function () {
this.section.refresh();
}
});

+ 17
- 4
frappe/public/js/frappe/form/toolbar.js Целия файл

@@ -226,7 +226,7 @@ frappe.ui.form.Toolbar = class Toolbar {
this.page.add_action_icon("right", ()=> {
this.frm.navigate_records(0);
}, 'next-doc', __("Next"));
}
}
}

make_menu_items() {
@@ -470,9 +470,22 @@ frappe.ui.form.Toolbar = class Toolbar {
me.frm.page.set_view('main');
}, 'edit');
} else if(status === "Cancel") {
this.page.set_secondary_action(__(status), function() {
me.frm.savecancel(this);
});
let add_cancel_button = () => {
this.page.set_secondary_action(__(status), function() {
me.frm.savecancel(this);
});
};
if (this.has_workflow()) {
frappe.xcall('frappe.model.workflow.can_cancel_document', {
'doctype': this.frm.doc.doctype,
}).then((can_cancel) => {
if (can_cancel) {
add_cancel_button();
}
});
} else {
add_cancel_button();
}
} else {
var click = {
"Save": function() {


+ 2
- 11
frappe/public/js/frappe/form/workflow.js Целия файл

@@ -85,7 +85,7 @@ frappe.ui.form.States = Class.extend({
frappe.workflow.get_transitions(this.frm.doc).then(transitions => {
this.frm.page.clear_actions_menu();
transitions.forEach(d => {
if(frappe.user_roles.includes(d.allowed) && has_approval_access(d)) {
if (frappe.user_roles.includes(d.allowed) && has_approval_access(d)) {
added = true;
me.frm.page.add_action_item(__(d.action), function() {
// set the workflow_action for use in form scripts
@@ -103,17 +103,8 @@ frappe.ui.form.States = Class.extend({
});
}
});
if (!added) {
//call function and clear cancel button if Cancel doc state is defined in the workfloe
frappe.xcall('frappe.model.workflow.can_cancel_document', {doc: this.frm.doc}).then((can_cancel) => {
if (!can_cancel) {
this.frm.page.clear_secondary_action();
}
});
} else {
this.setup_btn(added);
}

this.setup_btn(added);
});

},


+ 122
- 7
frappe/public/js/frappe/list/list_sidebar.js Целия файл

@@ -39,13 +39,105 @@ frappe.views.ListSidebar = class ListSidebar {

}

setup_list_group_by() {
this.list_group_by = new frappe.views.ListGroupBy({
doctype: this.doctype,
sidebar: this,
list_view: this.list_view,
page: this.page
});
setup_views() {
var show_list_link = false;

if (frappe.views.calendar[this.doctype]) {
this.sidebar.find('.list-link[data-view="Calendar"]').removeClass("hide");
this.sidebar.find('.list-link[data-view="Gantt"]').removeClass('hide');
show_list_link = true;
}
//show link for kanban view
this.sidebar.find('.list-link[data-view="Kanban"]').removeClass('hide');
if (this.doctype === "Communication" && frappe.boot.email_accounts.length) {
this.sidebar.find('.list-link[data-view="Inbox"]').removeClass('hide');
show_list_link = true;
}

if (frappe.treeview_settings[this.doctype] || frappe.get_meta(this.doctype).is_tree) {
this.sidebar.find(".tree-link").removeClass("hide");
}

this.current_view = 'List';
var route = frappe.get_route();
if (route.length > 2 && frappe.views.view_modes.includes(route[2])) {
this.current_view = route[2];

if (this.current_view === 'Kanban') {
this.kanban_board = route[3];
} else if (this.current_view === 'Inbox') {
this.email_account = route[3];
}
}

// disable link for current view
this.sidebar.find('.list-link[data-view="' + this.current_view + '"] a')
.attr('disabled', 'disabled').addClass('disabled');

//enable link for Kanban view
this.sidebar.find('.list-link[data-view="Kanban"] a, .list-link[data-view="Inbox"] a')
.attr('disabled', null).removeClass('disabled');

// show image link if image_view
if (this.list_view.meta.image_field) {
this.sidebar.find('.list-link[data-view="Image"]').removeClass('hide');
show_list_link = true;
}

if (this.list_view.settings.get_coords_method ||
(this.list_view.meta.fields.find(i => i.fieldname === "latitude") &&
this.list_view.meta.fields.find(i => i.fieldname === "longitude")) ||
(this.list_view.meta.fields.find(i => i.fieldname === 'location' && i.fieldtype == 'Geolocation'))) {
this.sidebar.find('.list-link[data-view="Map"]').removeClass('hide');
show_list_link = true;
}

if (show_list_link) {
this.sidebar.find('.list-link[data-view="List"]').removeClass('hide');
}
}

setup_reports() {
// add reports linked to this doctype to the dropdown
var me = this;
var added = [];
var dropdown = this.page.sidebar.find('.reports-dropdown');
var divider = false;

var add_reports = function(reports) {
$.each(reports, function(name, r) {
if (!r.ref_doctype || r.ref_doctype == me.doctype) {
var report_type = r.report_type === 'Report Builder' ?
`List/${r.ref_doctype}/Report` : 'query-report';

var route = r.route || report_type + '/' + (r.title || r.name);

if (added.indexOf(route) === -1) {
// don't repeat
added.push(route);

if (!divider) {
me.get_divider().appendTo(dropdown);
divider = true;
}

$('<li><a href="#' + route + '">' +
__(r.title || r.name) + '</a></li>').appendTo(dropdown);
}
}
});
};

// from reference doctype
if (this.list_view.settings.reports) {
add_reports(this.list_view.settings.reports);
}

// Sort reports alphabetically
var reports = Object.values(frappe.boot.user.all_reports).sort((a,b) => a.title.localeCompare(b.title)) || [];

// from specially tagged reports
add_reports(reports);
}

setup_list_filter() {
@@ -56,6 +148,29 @@ frappe.views.ListSidebar = class ListSidebar {
});
}

setup_kanban_boards() {
const $dropdown = this.page.sidebar.find('.kanban-dropdown');
frappe.views.KanbanView.setup_dropdown_in_sidebar(this.doctype, $dropdown);
}


setup_keyboard_shortcuts() {
this.sidebar.find('.list-link > a, .list-link > .btn-group > a').each((i, el) => {
frappe.ui.keys
.get_shortcut_group(this.page)
.add($(el));
});
}

setup_list_group_by() {
this.list_group_by = new frappe.views.ListGroupBy({
doctype: this.doctype,
sidebar: this,
list_view: this.list_view,
page: this.page
});
}

get_stats() {
var me = this;
frappe.call({


+ 6
- 2
frappe/public/js/frappe/model/model.js Целия файл

@@ -135,8 +135,8 @@ $.extend(frappe.model, {
let cached_timestamp = null;
let cached_doc = null;

let cached_docs = frappe.model.get_from_localstorage(doctype)
let cached_docs = frappe.model.get_from_localstorage(doctype);
if (cached_docs) {
cached_doc = cached_docs.filter(doc => doc.name === doctype)[0];
if(cached_doc) {
@@ -252,6 +252,10 @@ $.extend(frappe.model, {
return frappe.boot.user.can_create.indexOf(doctype)!==-1;
},

can_select: function(doctype) {
return frappe.boot.user.can_select.indexOf(doctype)!==-1;
},

can_read: function(doctype) {
return frappe.boot.user.can_read.indexOf(doctype)!==-1;
},


+ 1
- 2
frappe/public/js/frappe/ui/filters/filter.js Целия файл

@@ -527,7 +527,7 @@ frappe.ui.filter_utils = {
['Date', 'Datetime', 'DateRange', 'Select'].includes(df.fieldtype)
) {
df.fieldtype = 'Select';
df.options = this.get_timespan_options(['Last', 'Today', 'This', 'Next']);
df.options = this.get_timespan_options(['Last', 'Yesterday', 'Today', 'Tomorrow', 'This', 'Next']);
}
if (condition === 'is') {
df.fieldtype = 'Select';
@@ -542,7 +542,6 @@ frappe.ui.filter_utils = {
get_timespan_options(periods) {
const period_map = {
Last: ['Week', 'Month', 'Quarter', '6 months', 'Year'],
Today: null,
This: ['Week', 'Month', 'Quarter', 'Year'],
Next: ['Week', 'Month', 'Quarter', '6 months', 'Year'],
};


+ 1
- 1
frappe/public/js/frappe/utils/common.js Целия файл

@@ -176,7 +176,7 @@ window.replace_all = function (s, t1, t2) {
return s.split(t1).join(t2);
}

window.strip_html = function (txt) {
window.strip_html = function(txt) {
return cstr(txt).replace(/<[^>]*>/g, "");
}



+ 2
- 0
frappe/public/js/frappe/utils/utils.js Целия файл

@@ -1220,6 +1220,7 @@ Object.assign(frappe.utils, {
if (Math.floor(number) === number) return 0;
return number.toString().split(".")[1].length || 0;
},

build_summary_item(summary) {
if (summary.type == "separator") {
return $(`<div class="summary-separator">
@@ -1242,6 +1243,7 @@ Object.assign(frappe.utils, {
<div class="summary-value ${color}">${value}</div>
</div>`);
},

get_names_for_mentions() {
let names_for_mentions = Object.keys(frappe.boot.user_info || [])
.filter(user => {


+ 1
- 2
frappe/public/js/frappe/views/communication.js Целия файл

@@ -112,7 +112,6 @@ frappe.views.CommunicationComposer = Class.extend({
{
label: __("Message"),
fieldtype: "Text Editor",
reqd: 1,
fieldname: "content",
onchange: frappe.utils.debounce(
this.save_as_draft.bind(this),
@@ -124,7 +123,7 @@ frappe.views.CommunicationComposer = Class.extend({
label: __("Send me a copy"),
fieldtype: "Check",
fieldname: "send_me_a_copy",
default: 1
default: 1 // frappe.boot.user.send_me_a_copy
},
{
label: __("Send Read Receipt"),


+ 85
- 0
frappe/public/js/frappe/views/map/map_view.js Целия файл

@@ -0,0 +1,85 @@
/**
* frappe.views.MapView
*/
frappe.provide('frappe.utils.utils');
frappe.provide("frappe.views");

frappe.views.MapView = class MapView extends frappe.views.ListView {
get view_name() {
return 'Map';
}

setup_defaults() {
super.setup_defaults();
this.page_title = __('{0} Map', [this.page_title]);
}

setup_view() {
}

on_filter_change() {
this.get_coords();
}

render() {
this.get_coords()
.then(() => {
this.render_map_view();
});
this.$paging_area.find('.level-left').append('<div></div>');
}

render_map_view() {
this.map_id = frappe.dom.get_unique_id();

this.$result.html(`<div id="${this.map_id}" class="map-view-container"></div>`);

L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/';
this.map = L.map(this.map_id).setView(frappe.utils.map_defaults.center,
frappe.utils.map_defaults.zoom);

L.tileLayer(frappe.utils.map_defaults.tiles,
frappe.utils.map_defaults.options).addTo(this.map);

L.control.scale().addTo(this.map);
if (this.coords.features && this.coords.features.length) {
this.coords.features.forEach(
coords => L.geoJSON(coords).bindPopup(coords.properties.name).addTo(this.map)
);
let lastCoords = this.coords.features[0].geometry.coordinates.reverse();
this.map.panTo(lastCoords, 8);
}
}

get_coords() {
let get_coords_method = this.settings && this.settings.get_coords_method || 'frappe.geo.utils.get_coords';

if (cur_list.meta.fields.find(i => i.fieldname === 'location' && i.fieldtype === 'Geolocation')) {
this.type = 'location_field';
} else if (cur_list.meta.fields.find(i => i.fieldname === "latitude") &&
cur_list.meta.fields.find(i => i.fieldname === "longitude")) {
this.type = 'coordinates';
}
return frappe.call({
method: get_coords_method,
args: {
doctype: this.doctype,
filters: cur_list.filter_area.get(),
type: this.type
}
}).then(r => {
this.coords = r.message;

});
}


get required_libs() {
return [
"assets/frappe/js/lib/leaflet/leaflet.css",
"assets/frappe/js/lib/leaflet/leaflet.js"
];
}


};

+ 27
- 0
frappe/public/js/frappe/views/reports/report_view.js Целия файл

@@ -708,6 +708,32 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
super.build_fields();
}

reorder_fields() {
// generate table fields in the required format ["name", "DocType"]
// these are fields in the column before adding new fields
let table_fields = this.columns.map(df => [df.field, df.docfield.parent]);
// filter fields that are already in table
// iterate over table_fields to preserve the existing order of fields
// The filter will ensure the unchecked fields are removed
let fields_already_in_table = table_fields.filter(df => {
return this.fields.find((field) => {
return df[0] == field[0] && df[1] == field[1]
})
})

// find new fields that didn't already exists
// This will be appended to the end of the table
let fields_to_add = this.fields.filter(df => {
return !table_fields.find((field) => {
return df[0] == field[0] && df[1] == field[1]
})
})
// rebuild fields
this.fields = [...fields_already_in_table, ...fields_to_add];
}

get_fields() {
let fields = this.fields.map(f => {
let column_name = frappe.model.get_full_column_name(f[0], f[1]);
@@ -1329,6 +1355,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {

this.fields.map(f => this.add_currency_column(f[0], f[1]));

this.reorder_fields();
this.build_fields();
this.setup_columns();



+ 7
- 7
frappe/public/js/frappe/views/treeview.js Целия файл

@@ -94,17 +94,17 @@ frappe.views.TreeView = Class.extend({
var me = this;
this.opts.onload && this.opts.onload(me);
},
make_filters: function(){
make_filters: function() {
var me = this;
frappe.treeview_settings.filters = []
$.each(this.opts.filters || [], function(i, filter) {
if(frappe.route_options && frappe.route_options[filter.fieldname]) {
filter.default = frappe.route_options[filter.fieldname]
if (frappe.route_options && frappe.route_options[filter.fieldname]) {
filter.default = frappe.route_options[filter.fieldname];
}

if(!filter.disable_onchange) {
if (!filter.disable_onchange) {
filter.change = function() {
filter.on_change && filter.on_change();
filter.onchange && filter.onchange();
var val = this.get_value();
me.args[filter.fieldname] = val;
if (val) {
@@ -114,7 +114,7 @@ frappe.views.TreeView = Class.extend({
}
me.set_title();
me.make_tree();
}
};
}

me.page.add_field(filter);
@@ -122,7 +122,7 @@ frappe.views.TreeView = Class.extend({
if (filter.default) {
$("[data-fieldname='"+filter.fieldname+"']").trigger("change");
}
})
});
},
get_root: function() {
var me = this;


+ 2
- 2
frappe/public/js/frappe/web_form/webform_script.js Целия файл

@@ -85,6 +85,7 @@ frappe.ready(function() {

function setup_fields(form_data) {
form_data.web_form.web_form_fields.map(df => {
df.is_web_form = true;
if (df.fieldtype === "Table") {
df.get_data = () => {
let data = [];
@@ -99,14 +100,13 @@ frappe.ready(function() {
if (field.fieldtype === "Link") {
field.only_select = true;
}
field.is_web_form = true;
});

if (df.fieldtype === "Attach") {
df.is_private = true;
}

df.is_web_form = true;

delete df.parent;
delete df.parentfield;
delete df.parenttype;


+ 2
- 2
frappe/public/scss/website/page_builder.scss Целия файл

@@ -29,11 +29,11 @@
}

.hero.align-center {
h1, .hero-subtitle, .hero-buttons {
h1, .hero-title, .hero-subtitle, .hero-buttons {
text-align: center;
}

.hero-subtitle {
.hero-title, .hero-subtitle {
margin-left: auto;
margin-right: auto;
}


+ 6
- 2
frappe/templates/includes/breadcrumbs.html Целия файл

@@ -3,6 +3,7 @@
<nav aria-label="breadcrumb">
<ol class="breadcrumb" itemscope itemtype="http://schema.org/BreadcrumbList">
{%- set parents = parents[-3:] %}
{% set count = (parents | length) + 1 %}
{% for parent in parents %}
<li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem" class="breadcrumb-item">
<a itemprop="item" href="{{ url_prefix }}{{ parent.route | abs_url }}" itemprop="url">
@@ -11,8 +12,11 @@
</a>
</li>
{% endfor %}
<li class="breadcrumb-item active" aria-current="page">
{{ title or "" }}
<li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem" class="breadcrumb-item active" aria-current="page">
<span itemprop="item">
<span itemprop="name">{{ title }}</span>
<meta itemprop="position" content="{{ count }}"/>
</span>
</li>
</ol>
</nav>


+ 7
- 5
frappe/templates/print_formats/standard_macros.html Целия файл

@@ -5,8 +5,11 @@
<div>{{ frappe.render_template(df.options, {"doc": doc}) or "" }}</div>
{%- elif df.fieldtype in ("Text", "Text Editor", "Code", "Long Text") -%}
{{ render_text_field(df, doc) }}
{%- elif df.fieldtype in ("Image", "Attach Image", "Attach")
and (guess_mimetype(doc[df.fieldname])[0] or "").startswith("image/") -%}
{%- elif df.fieldtype in ("Image", "Attach Image")
and (
(guess_mimetype(doc[df.fieldname])[0] or "").startswith("image/")
or doc[df.fieldname].startswith("http")
) -%}
{{ render_image(df, doc) }}
{%- elif df.fieldtype=="Geolocation" -%}
{{ render_geolocation(df, doc) }}
@@ -137,15 +140,14 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}"
style="width: 12px; height: 12px; margin-top: 5px;">
<path d="M2 9.66667L5.33333 13L14 3" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
{% elif df.fieldtype=="Image" %}
{% elif df.fieldtype in ("Image", "Attach Image") %}
<img src="{{ doc[doc.meta.get_field(df.fieldname).options] }}"
class="img-responsive"
{%- if df.print_width %} style="width: {{ get_width(df) }};"{% endif %}>
{% elif df.fieldtype=="Signature" %}
<img src="{{ doc[df.fieldname] }}" class="signature-img img-responsive"
{%- if df.print_width %} style="width: {{ get_width(df) }};"{% endif %}>
{% elif df.fieldtype in ("Attach", "Attach Image") and doc[df.fieldname]
and frappe.utils.is_image(doc[df.fieldname]) %}
{% elif df.fieldtype == "Attach" and doc[df.fieldname] and frappe.utils.is_image(doc[df.fieldname]) %}
<img src="{{ doc[df.fieldname] }}" class="img-responsive"
{%- if df.print_width %} style="width: {{ get_width(df) }};"{% endif %}>
{% elif df.fieldtype=="HTML" %}


+ 57
- 0
frappe/tests/test_cors.py Целия файл

@@ -0,0 +1,57 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals

import frappe, unittest
from werkzeug.wrappers import Response
from frappe.app import process_response

HEADERS = ('Access-Control-Allow-Origin', 'Access-Control-Allow-Credentials',
'Access-Control-Allow-Methods', 'Access-Control-Allow-Headers')

class TestCORS(unittest.TestCase):
def make_request_and_test(self, origin='http://example.com', absent=False):
self.origin = origin

headers = {}
if origin:
headers = {'Origin': origin}

frappe.utils.set_request(headers=headers)

self.response = Response()
process_response(self.response)

for header in HEADERS:
if absent:
self.assertNotIn(header, self.response.headers)
else:
if header == 'Access-Control-Allow-Origin':
self.assertEqual(self.response.headers.get(header), self.origin)
else:
self.assertIn(header, self.response.headers)

def test_cors_disabled(self):
frappe.conf.allow_cors = None
self.make_request_and_test('http://example.com', True)

def test_request_without_origin(self):
frappe.conf.allow_cors = 'http://example.com'
self.make_request_and_test(None, True)

def test_valid_origin(self):
frappe.conf.allow_cors = 'http://example.com'
self.make_request_and_test()

frappe.conf.allow_cors = "*"
self.make_request_and_test()

frappe.conf.allow_cors = ['http://example.com', 'https://example.com']
self.make_request_and_test()

def test_invalid_origin(self):
frappe.conf.allow_cors = 'http://example1.com'
self.make_request_and_test(absent=True)

frappe.conf.allow_cors = ['http://example1.com', 'https://example.com']
self.make_request_and_test(absent=True)

+ 11
- 11
frappe/tests/test_hooks.py Целия файл

@@ -5,6 +5,7 @@ from __future__ import unicode_literals
import unittest
import frappe
from frappe.desk.doctype.todo.todo import ToDo
from frappe.cache_manager import clear_controller_cache

class TestHooks(unittest.TestCase):
def test_hooks(self):
@@ -17,21 +18,20 @@ class TestHooks(unittest.TestCase):
hooks.get("doc_events").get("*").get("on_update"))

def test_override_doctype_class(self):
# mock get_hooks
original = frappe.get_hooks
def get_hooks(hook=None, default=None, app_name=None):
if hook == 'override_doctype_class':
return {
'ToDo': ['frappe.tests.test_hooks.CustomToDo']
}
return original(hook, default, app_name)
frappe.get_hooks = get_hooks
from frappe import hooks
# Set hook
hooks.override_doctype_class = {
'ToDo': ['frappe.tests.test_hooks.CustomToDo']
}
# Clear cache
frappe.cache().delete_value('app_hooks')
clear_controller_cache('ToDo')

todo = frappe.get_doc(doctype='ToDo', description='asdf')
self.assertTrue(isinstance(todo, CustomToDo))

# restore
frappe.get_hooks = original

class CustomToDo(ToDo):
pass

+ 19
- 1
frappe/tests/test_permissions.py Целия файл

@@ -9,7 +9,7 @@ import frappe.defaults
import unittest
import frappe.model.meta
from frappe.permissions import (add_user_permission, remove_user_permission,
clear_user_permissions_for_doctype, get_doc_permissions, add_permission)
clear_user_permissions_for_doctype, get_doc_permissions, add_permission, update_permission_property)
from frappe.core.page.permission_manager.permission_manager import update, reset
from frappe.test_runner import make_test_records_for_doctype
from frappe.core.doctype.user_permission.user_permission import clear_user_permissions
@@ -58,6 +58,24 @@ class TestPermissions(unittest.TestCase):
post = frappe.get_doc("Blog Post", "-test-blog-post")
self.assertTrue(post.has_permission("read"))

def test_select_permission(self):
# grant only select perm to blog post
add_permission('Blog Post', 'Sales User', 0)
update_permission_property('Blog Post', 'Sales User', 0, 'select', 1)
update_permission_property('Blog Post', 'Sales User', 0, 'read', 0)
update_permission_property('Blog Post', 'Sales User', 0, 'write', 0)

frappe.clear_cache(doctype="Blog Post")
frappe.set_user("test3@example.com")

# validate select perm
post = frappe.get_doc("Blog Post", "-test-blog-post")
self.assertTrue(post.has_permission("select"))

# validate does not have read and write perm
self.assertFalse(post.has_permission("read"))
self.assertRaises(frappe.PermissionError, post.save)

def test_user_permissions_in_doc(self):
add_user_permission("Blog Category", "-test-blog-category-1",
"test2@example.com")


+ 42
- 0
frappe/tests/tests_geo_utils.py Целия файл

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

from __future__ import unicode_literals

import unittest

import frappe
from frappe.geo.utils import get_coords


class TestGeoUtils(unittest.TestCase):
def setUp(self):
self.todo = frappe.get_doc(
dict(doctype='ToDo', description='Test description', assigned_by='Administrator')).insert()

self.test_location_dict = {'type': 'FeatureCollection', 'features': [
{'type': 'Feature', 'properties': {}, "geometry": {'type': 'Point', 'coordinates': [49.20433, 55.753395]}}]}
self.test_location = frappe.get_doc({'name': 'Test Location', 'doctype': 'Location',
'location': str(self.test_location_dict)})

self.test_filter_exists = [['Location', 'name', 'like', '%Test Location%']]
self.test_filter_not_exists = [['Location', 'name', 'like', '%Test Location Not exists%']]
self.test_filter_todo = [['ToDo', 'description', 'like', '%Test description%']]

def test_get_coords_location_with_filter_exists(self):
coords = get_coords('Location', self.test_filter_exists, 'location_field')
self.assertEqual(self.test_location_dict['features'][0]['geometry'], coords['features'][0]['geometry'])

def test_get_coords_location_with_filter_not_exists(self):
coords = get_coords('Location', self.test_filter_not_exists, 'location_field')
self.assertEqual(coords, {'type': 'FeatureCollection', 'features': []})

def test_get_coords_from_not_existable_location(self):
self.assertRaises(frappe.ValidationError, get_coords, 'ToDo', self.test_filter_todo, 'location_field')

def test_get_coords_from_not_existable_coords(self):
self.assertRaises(frappe.ValidationError, get_coords, 'ToDo', self.test_filter_todo, 'coordinates')

def tearDown(self):
self.todo.delete()

+ 18
- 0
frappe/tests/ui_test_helpers.py Целия файл

@@ -95,6 +95,24 @@ def create_doctype(name, fields):
"name": name
}).insert()

@frappe.whitelist()
def create_child_doctype(name, fields):
fields = frappe.parse_json(fields)
if frappe.db.exists('DocType', name):
return
frappe.get_doc({
"doctype": "DocType",
"module": "Core",
"istable": 1,
"custom": 1,
"fields": fields,
"permissions": [{
"role": "System Manager",
"read": 1
}],
"name": name
}).insert()

@frappe.whitelist()
def create_contact_records():
if frappe.db.get_all('Contact', {'first_name': 'Test Form Contact 1'}):


+ 1
- 1
frappe/translate.py Целия файл

@@ -190,7 +190,7 @@ def get_full_dict(lang):
frappe.local.lang_full_dict = load_lang(lang)

try:
# get user specific transaltion data
# get user specific translation data
user_translations = get_user_translations(lang)
frappe.local.lang_full_dict.update(user_translations)
except Exception:


+ 1
- 1
frappe/translations/de.csv Целия файл

@@ -1577,7 +1577,7 @@ Monospace,Monospace,
More articles on {0},Weitere Artikel zum {0},
More content for the bottom of the page.,Zusätzlicher Inhalt für den unteren Teil der Seite.,
Most Used,Am Meisten verwendet,
Move To,Ziehen nach,
Move To,Bewegen nach,
Move To Trash,In den Papierkorb verschieben,
Move to Row Number,Gehe zu Zeilennummer,
Mr,Hr.,


+ 20
- 16
frappe/utils/data.py Целия файл

@@ -445,25 +445,29 @@ def get_weekday(datetime=None):
return weekdays[datetime.weekday()]

def get_timespan_date_range(timespan):
today = nowdate()
date_range_map = {
"last week": [add_to_date(nowdate(), days=-7), nowdate()],
"last month": [add_to_date(nowdate(), months=-1), nowdate()],
"last quarter": [add_to_date(nowdate(), months=-3), nowdate()],
"last 6 months": [add_to_date(nowdate(), months=-6), nowdate()],
"last year": [add_to_date(nowdate(), years=-1), nowdate()],
"today": [nowdate(), nowdate()],
"this week": [get_first_day_of_week(nowdate(), as_str=True), nowdate()],
"this month": [get_first_day(nowdate(), as_str=True), nowdate()],
"this quarter": [get_quarter_start(nowdate(), as_str=True), nowdate()],
"this year": [get_year_start(nowdate(), as_str=True), nowdate()],
"next week": [nowdate(), add_to_date(nowdate(), days=7)],
"next month": [nowdate(), add_to_date(nowdate(), months=1)],
"next quarter": [nowdate(), add_to_date(nowdate(), months=3)],
"next 6 months": [nowdate(), add_to_date(nowdate(), months=6)],
"next year": [nowdate(), add_to_date(nowdate(), years=1)],
"last week": lambda: (add_to_date(today, days=-7), today),
"last month": lambda: (add_to_date(today, months=-1), today),
"last quarter": lambda: (add_to_date(today, months=-3), today),
"last 6 months": lambda: (add_to_date(today, months=-6), today),
"last year": lambda: (add_to_date(today, years=-1), today),
"yesterday": lambda: (add_to_date(today, days=-1),) * 2,
"today": lambda: (today, today),
"tomorrow": lambda: (add_to_date(today, days=1),) * 2,
"this week": lambda: (get_first_day_of_week(today, as_str=True), today),
"this month": lambda: (get_first_day(today, as_str=True), today),
"this quarter": lambda: (get_quarter_start(today, as_str=True), today),
"this year": lambda: (get_year_start(today, as_str=True), today),
"next week": lambda: (today, add_to_date(today, days=7)),
"next month": lambda: (today, add_to_date(today, months=1)),
"next quarter": lambda: (today, add_to_date(today, months=3)),
"next 6 months": lambda: (today, add_to_date(today, months=6)),
"next year": lambda: (today, add_to_date(today, years=1)),
}

return date_range_map.get(timespan)
if timespan in date_range_map:
return date_range_map[timespan]()

def global_date_format(date, format="long"):
"""returns localized date in the form of January 1, 2012"""


+ 6
- 3
frappe/utils/user.py Целия файл

@@ -22,6 +22,7 @@ class UserPermissions:

self.all_read = []
self.can_create = []
self.can_select = []
self.can_read = []
self.can_write = []
self.can_cancel = []
@@ -104,6 +105,9 @@ class UserPermissions:
if not p.get("read") and (dt in user_shared):
p["read"] = 1

if p.get('select'):
self.can_select.append(dt)

if not dtp.get('istable'):
if p.get('create') and not dtp.get('issingle'):
if dtp.get('in_create'):
@@ -193,9 +197,8 @@ class UserPermissions:
d.name = self.name
d.roles = self.get_roles()
d.defaults = self.get_defaults()

for key in ("can_create", "can_write", "can_read", "can_cancel", "can_delete",
"can_get_report", "allow_modules", "all_read", "can_search",
for key in ("can_select", "can_create", "can_write", "can_read", "can_cancel",
"can_delete", "can_get_report", "allow_modules", "all_read", "can_search",
"in_create", "can_export", "can_import", "can_print", "can_email",
"can_set_user_permissions"):
d[key] = list(set(getattr(self, key)))


+ 2
- 2
frappe/website/doctype/blog_post/templates/blog_post_row.html Целия файл

@@ -21,7 +21,7 @@
{%- if post.featured -%}
<h5 class="mt-1"><span class="text-dark">{{ post.title }}</span></h5>
{%- else -%}
<h5 class="mt-1"><span class="text-dark">{{ post.title }}</span></h3>
<h5 class="mt-1"><span class="text-dark">{{ post.title }}</span></h5>
{%- endif -%}
<p class="post-description text-muted">{{ post.intro }}</p>
</div>
@@ -38,4 +38,4 @@
</div>
<a class="stretched-link" href="/{{ post.route }}"></a>
</div>
</div>
</div>

+ 5
- 1
frappe/website/js/bootstrap-4.js Целия файл

@@ -18,7 +18,7 @@ $('.dropdown-menu a.dropdown-toggle').on('click', function (e) {
return false;
});

frappe.get_modal = function(title, content) {
frappe.get_modal = function (title, content) {
return $(
`<div class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
@@ -33,6 +33,10 @@ frappe.get_modal = function(title, content) {
${content}
</div>
<div class="modal-footer hidden">
<button type="button" class="btn btn-default btn-sm btn-modal-close" data-dismiss="modal">
<i class="octicon octicon-x visible-xs" style="padding: 1px 0px;"></i>
<span class="hidden-xs">${__("Close")}</span>
</button>
<button type="button" class="btn btn-sm btn-primary hidden"></button>
</div>
</div>


+ 1
- 3
frappe/website/web_template/testimonial/testimonial.html Целия файл

@@ -5,9 +5,7 @@
</div>
{% endif %}
<div class="testimonial-content">
<span>“</span>
{{ content }}
<span>”</span>
“{{ content }}”
</div>
<div class="testimonial-by">
{{ name }}


+ 1
- 1
package.json Целия файл

@@ -46,7 +46,7 @@
"redis": "^2.8.0",
"showdown": "^1.9.1",
"snyk": "^1.425.4",
"socket.io": "^2.3.0",
"socket.io": "^2.4.0",
"superagent": "^3.8.2",
"touch": "^3.1.0",
"vue": "^2.6.11",


+ 52
- 78
yarn.lock Целия файл

@@ -693,13 +693,6 @@ bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2:
dependencies:
tweetnacl "^0.14.3"

better-assert@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522"
integrity sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=
dependencies:
callsite "1.0.0"

big.js@^3.1.3:
version "3.2.0"
resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
@@ -940,11 +933,6 @@ caller-path@^2.0.0:
dependencies:
caller-callsite "^2.0.0"

callsite@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20"
integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA=

callsites@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50"
@@ -1252,6 +1240,11 @@ component-emitter@1.2.1, component-emitter@^1.2.0, component-emitter@^1.2.1:
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=

component-emitter@~1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==

component-inherit@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
@@ -1320,16 +1313,16 @@ cookie-signature@1.0.6:
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=

cookie@0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=

cookie@0.4.0, cookie@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==

cookie@~0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==

cookiejar@^2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c"
@@ -1982,20 +1975,20 @@ endian-reader@^0.3.0:
resolved "https://registry.yarnpkg.com/endian-reader/-/endian-reader-0.3.0.tgz#84eca436b80aed0d0639c47291338b932efe50a0"
integrity sha1-hOykNrgK7Q0GOcRykTOLky7+UKA=

engine.io-client@~3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.0.tgz#82a642b42862a9b3f7a188f41776b2deab643700"
integrity sha512-a4J5QO2k99CM2a0b12IznnyQndoEvtA4UAldhGzKqnHf42I3Qs2W5SPnDvatZRcMaNZs4IevVicBPayxYt6FwA==
engine.io-client@~3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.5.0.tgz#fc1b4d9616288ce4f2daf06dcf612413dec941c7"
integrity sha512-12wPRfMrugVw/DNyJk34GQ5vIVArEcVMXWugQGGuw2XxUSztFNmJggZmv8IZlLyEdnpO1QB9LkcjeWewO2vxtA==
dependencies:
component-emitter "1.2.1"
component-emitter "~1.3.0"
component-inherit "0.0.3"
debug "~4.1.0"
debug "~3.1.0"
engine.io-parser "~2.2.0"
has-cors "1.1.0"
indexof "0.0.1"
parseqs "0.0.5"
parseuri "0.0.5"
ws "~6.1.0"
parseqs "0.0.6"
parseuri "0.0.6"
ws "~7.4.2"
xmlhttprequest-ssl "~1.5.4"
yeast "0.1.2"

@@ -2010,17 +2003,17 @@ engine.io-parser@~2.2.0:
blob "0.0.5"
has-binary2 "~1.0.2"

engine.io@~3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.4.0.tgz#3a962cc4535928c252759a00f98519cb46c53ff3"
integrity sha512-XCyYVWzcHnK5cMz7G4VTu2W7zJS7SM1QkcelghyIk/FmobWBtXE7fwhBusEKvCSqc3bMh8fNFMlUkCKTFRxH2w==
engine.io@~3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.5.0.tgz#9d6b985c8a39b1fe87cd91eb014de0552259821b"
integrity sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA==
dependencies:
accepts "~1.3.4"
base64id "2.0.0"
cookie "0.3.1"
cookie "~0.4.1"
debug "~4.1.0"
engine.io-parser "~2.2.0"
ws "^7.1.2"
ws "~7.4.2"

entities@^1.1.1:
version "1.1.2"
@@ -4623,11 +4616,6 @@ object-assign@^4.0.1, object-assign@^4.1.0:
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=

object-component@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291"
integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=

object-copy@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
@@ -4938,19 +4926,15 @@ parse-passwd@^1.0.0:
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=

parseqs@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d"
integrity sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=
dependencies:
better-assert "~1.0.0"
parseqs@0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5"
integrity sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==

parseuri@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a"
integrity sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=
dependencies:
better-assert "~1.0.0"
parseuri@0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a"
integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==

parseurl@~1.3.3:
version "1.3.3"
@@ -6808,23 +6792,20 @@ socket.io-adapter@~1.1.0:
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz#2a805e8a14d6372124dd9159ad4502f8cb07f06b"
integrity sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=

socket.io-client@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.0.tgz#14d5ba2e00b9bcd145ae443ab96b3f86cbcc1bb4"
integrity sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==
socket.io-client@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.4.0.tgz#aafb5d594a3c55a34355562fc8aea22ed9119a35"
integrity sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==
dependencies:
backo2 "1.0.2"
base64-arraybuffer "0.1.5"
component-bind "1.0.0"
component-emitter "1.2.1"
debug "~4.1.0"
engine.io-client "~3.4.0"
component-emitter "~1.3.0"
debug "~3.1.0"
engine.io-client "~3.5.0"
has-binary2 "~1.0.2"
has-cors "1.1.0"
indexof "0.0.1"
object-component "0.0.3"
parseqs "0.0.5"
parseuri "0.0.5"
parseqs "0.0.6"
parseuri "0.0.6"
socket.io-parser "~3.3.0"
to-array "0.1.4"

@@ -6846,16 +6827,16 @@ socket.io-parser@~3.4.0:
debug "~4.1.0"
isarray "2.0.1"

socket.io@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.3.0.tgz#cd762ed6a4faeca59bc1f3e243c0969311eb73fb"
integrity sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==
socket.io@^2.4.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.4.1.tgz#95ad861c9a52369d7f1a68acf0d4a1b16da451d2"
integrity sha512-Si18v0mMXGAqLqCVpTxBa8MGqriHGQh8ccEOhmsmNS3thNCGBwO8WGrwMibANsWtQQ5NStdZwHqZR3naJVFc3w==
dependencies:
debug "~4.1.0"
engine.io "~3.4.0"
engine.io "~3.5.0"
has-binary2 "~1.0.2"
socket.io-adapter "~1.1.0"
socket.io-client "2.3.0"
socket.io-client "2.4.0"
socket.io-parser "~3.4.0"

socks-proxy-agent@^4.0.1:
@@ -7970,17 +7951,10 @@ write-file-atomic@^3.0.0:
signal-exit "^3.0.2"
typedarray-to-buffer "^3.1.5"

ws@^7.1.2:
version "7.2.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.1.tgz#03ed52423cd744084b2cf42ed197c8b65a936b8e"
integrity sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==

ws@~6.1.0:
version "6.1.4"
resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9"
integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==
dependencies:
async-limiter "~1.0.0"
ws@~7.4.2:
version "7.4.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.2.tgz#782100048e54eb36fe9843363ab1c68672b261dd"
integrity sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==

xdg-basedir@^4.0.0:
version "4.0.0"


Зареждане…
Отказ
Запис