Explorar el Código

Merge branch 'develop' into tree-view-print-fix

version-14
Shariq Ansari hace 3 años
committed by GitHub
padre
commit
457817ea1e
No se encontró ninguna clave conocida en la base de datos para esta firma ID de clave GPG: 4AEE18F83AFDEB23
Se han modificado 84 ficheros con 927 adiciones y 570 borrados
  1. +3
    -0
      .git-blame-ignore-revs
  2. +11
    -2
      .github/workflows/linters.yml
  3. +23
    -0
      .pre-commit-config.yaml
  4. +0
    -1
      codecov.yml
  5. +1
    -1
      frappe/core/doctype/data_import/importer.py
  6. +3
    -2
      frappe/core/doctype/doctype/boilerplate/test_controller._py
  7. +3
    -3
      frappe/core/doctype/report/test_report.py
  8. +7
    -1
      frappe/database/query.py
  9. +1
    -1
      frappe/desk/doctype/dashboard_chart/dashboard_chart.js
  10. +1
    -1
      frappe/desk/doctype/form_tour/form_tour.js
  11. +0
    -20
      frappe/desk/doctype/kanban_board/kanban_board.py
  12. +1
    -1
      frappe/desk/doctype/todo/todo_calendar.js
  13. +1
    -1
      frappe/desk/doctype/workspace/workspace.js
  14. +4
    -4
      frappe/desk/doctype/workspace/workspace.py
  15. +1
    -1
      frappe/desk/query_report.py
  16. +1
    -1
      frappe/email/doctype/notification/notification.py
  17. +1
    -1
      frappe/email/receive.py
  18. +3
    -3
      frappe/hooks.py
  19. +1
    -1
      frappe/integrations/doctype/razorpay_settings/razorpay_settings.js
  20. +8
    -10
      frappe/model/document.py
  21. +9
    -6
      frappe/model/rename_doc.py
  22. +23
    -7
      frappe/modules/patch_handler.py
  23. +2
    -0
      frappe/patches.txt
  24. +1
    -1
      frappe/patches/v12_0/set_correct_url_in_files.py
  25. +41
    -0
      frappe/patches/v14_0/reset_creation_datetime.py
  26. +5
    -0
      frappe/patches/v14_0/update_auto_account_deletion_duration.py
  27. +1
    -1
      frappe/public/js/frappe/form/controls/date.js
  28. +1
    -1
      frappe/public/js/frappe/form/controls/table.js
  29. +3
    -0
      frappe/public/js/frappe/form/controls/text_editor.js
  30. +1
    -1
      frappe/public/js/frappe/form/grid_pagination.js
  31. +1
    -0
      frappe/public/js/frappe/form/grid_row.js
  32. +1
    -0
      frappe/public/js/frappe/form/save.js
  33. +4
    -1
      frappe/public/js/frappe/form/toolbar.js
  34. +1
    -1
      frappe/public/js/frappe/list/list_view_select.js
  35. +3
    -0
      frappe/public/js/frappe/model/model.js
  36. +5
    -0
      frappe/public/js/frappe/request.js
  37. +4
    -4
      frappe/public/js/frappe/ui/filters/field_select.js
  38. +3
    -3
      frappe/public/js/frappe/utils/utils.js
  39. +0
    -12
      frappe/public/js/frappe/views/kanban/kanban_board.js
  40. +1
    -1
      frappe/public/js/frappe/views/reports/query_report.js
  41. +3
    -3
      frappe/public/js/frappe/views/workspace/blocks/block.js
  42. +1
    -1
      frappe/public/js/frappe/views/workspace/blocks/header.js
  43. +3
    -3
      frappe/public/js/frappe/views/workspace/blocks/header_size.js
  44. +1
    -1
      frappe/public/js/frappe/views/workspace/blocks/paragraph.js
  45. +11
    -11
      frappe/public/js/frappe/views/workspace/workspace.js
  46. +2
    -2
      frappe/public/js/frappe/widgets/chart_widget.js
  47. +1
    -1
      frappe/public/js/lib/jSignature.min.js
  48. +5
    -5
      frappe/public/js/lib/photoswipe/default-skin.css
  49. +81
    -81
      frappe/public/js/lib/photoswipe/photoswipe-ui-default.js
  50. +263
    -263
      frappe/public/js/lib/photoswipe/photoswipe.js
  51. +2
    -2
      frappe/public/js/lib/prettydate.js
  52. +7
    -0
      frappe/public/scss/common/grid.scss
  53. +10
    -10
      frappe/public/scss/desk/desktop.scss
  54. +1
    -1
      frappe/public/scss/desk/list.scss
  55. +1
    -1
      frappe/public/scss/login.bundle.scss
  56. +6
    -6
      frappe/public/scss/website/blog.scss
  57. +1
    -1
      frappe/public/scss/website/error-state.scss
  58. +3
    -1
      frappe/public/scss/website/index.scss
  59. +1
    -1
      frappe/public/scss/website/navbar.scss
  60. +1
    -1
      frappe/public/scss/website/portal.scss
  61. +1
    -1
      frappe/public/scss/website/web_form.scss
  62. +12
    -6
      frappe/social/doctype/energy_point_log/test_energy_point_log.py
  63. +1
    -1
      frappe/templates/includes/feedback/feedback.html
  64. +5
    -1
      frappe/templates/includes/footer/footer_info.html
  65. +0
    -2
      frappe/templates/includes/website_theme/navbar.css
  66. +1
    -1
      frappe/templates/pages/integrations/payment-success.html
  67. +6
    -2
      frappe/tests/test_db_query.py
  68. +18
    -0
      frappe/tests/test_document.py
  69. +1
    -1
      frappe/utils/make_random.py
  70. +3
    -3
      frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py
  71. +14
    -2
      frappe/website/doctype/personal_data_deletion_request/test_personal_data_deletion_request.py
  72. +14
    -6
      frappe/website/doctype/website_settings/website_settings.json
  73. +2
    -1
      frappe/website/doctype/website_settings/website_settings.py
  74. +1
    -1
      frappe/website/web_form/request_to_delete_data/request_to_delete_data.js
  75. +50
    -1
      frappe/workflow/doctype/workflow/test_workflow.py
  76. +33
    -11
      frappe/workflow/doctype/workflow_action/workflow_action.json
  77. +131
    -36
      frappe/workflow/doctype/workflow_action/workflow_action.py
  78. +0
    -0
      frappe/workflow/doctype/workflow_action_permitted_role/__init__.py
  79. +33
    -0
      frappe/workflow/doctype/workflow_action_permitted_role/workflow_action_permitted_role.json
  80. +8
    -0
      frappe/workflow/doctype/workflow_action_permitted_role/workflow_action_permitted_role.py
  81. +1
    -1
      frappe/www/about.html
  82. +1
    -1
      frappe/www/me.html
  83. +1
    -1
      frappe/www/me.py
  84. +2
    -2
      frappe/www/third_party_apps.html

+ 3
- 0
.git-blame-ignore-revs Ver fichero

@@ -16,3 +16,6 @@ fe20515c23a3ac41f1092bf0eaf0a0a452ec2e85


# Refactor "not a in b" -> "a not in b" # Refactor "not a in b" -> "a not in b"
745297a49d516e5e3c4bb3e1b0c4235e7d31165d 745297a49d516e5e3c4bb3e1b0c4235e7d31165d

# Clean up whitespace
b2fc959307c7c79f5584625569d5aed04133ba13

.github/workflows/semgrep.yml → .github/workflows/linters.yml Ver fichero

@@ -1,15 +1,24 @@
name: Semgrep
name: Linters


on: on:
pull_request: { } pull_request: { }


jobs: jobs:
semgrep:

linters:
name: Frappe Linter name: Frappe Linter
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2


- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8

- name: Install and Run Pre-commit
uses: pre-commit/action@v2.0.3

- name: Download Semgrep rules - name: Download Semgrep rules
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules



+ 23
- 0
.pre-commit-config.yaml Ver fichero

@@ -0,0 +1,23 @@
exclude: 'node_modules|.git'
default_stages: [commit]
fail_fast: false


repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
hooks:
- id: trailing-whitespace
files: "frappe.*"
exclude: ".*json$|.*txt$|.*csv|.*md|.*svg"
- id: check-yaml
- id: no-commit-to-branch
args: ['--branch', 'develop']
- id: check-merge-conflict
- id: check-ast


ci:
autoupdate_schedule: weekly
skip: []
submodules: false

+ 0
- 1
codecov.yml Ver fichero

@@ -3,7 +3,6 @@ codecov:


coverage: coverage:
status: status:
patch: off
project: project:
default: false default: false
server: server:


+ 1
- 1
frappe/core/doctype/data_import/importer.py Ver fichero

@@ -166,7 +166,7 @@ class Importer:


if not self.data_import.status == "Partial Success": if not self.data_import.status == "Partial Success":
self.data_import.db_set("status", "Partial Success") self.data_import.db_set("status", "Partial Success")
# commit after every successful import # commit after every successful import
frappe.db.commit() frappe.db.commit()




+ 3
- 2
frappe/core/doctype/doctype/boilerplate/test_controller._py Ver fichero

@@ -2,7 +2,8 @@
# See license.txt # See license.txt


# import frappe # import frappe
import unittest
from frappe.tests.utils import FrappeTestCase


class Test{classname}(unittest.TestCase):

class Test{classname}(FrappeTestCase):
pass pass

+ 3
- 3
frappe/core/doctype/report/test_report.py Ver fichero

@@ -314,19 +314,19 @@ result = [
{ {
"parent_column": "Parent 1", "parent_column": "Parent 1",
"column_1": 200, "column_1": 200,
"column_2": 150.50
"column_2": 150.50
}, },
{ {
"parent_column": "Child 1", "parent_column": "Child 1",
"column_1": 100, "column_1": 100,
"column_2": 75.25, "column_2": 75.25,
"parent_value": "Parent 1"
"parent_value": "Parent 1"
}, },
{ {
"parent_column": "Child 2", "parent_column": "Child 2",
"column_1": 100, "column_1": 100,
"column_2": 75.25, "column_2": 75.25,
"parent_value": "Parent 1"
"parent_value": "Parent 1"
} }
] ]




+ 7
- 1
frappe/database/query.py Ver fichero

@@ -244,7 +244,13 @@ class Query:
_operator = OPERATOR_MAP[value[0]] _operator = OPERATOR_MAP[value[0]]
conditions = conditions.where(_operator(Field(key), value[1])) conditions = conditions.where(_operator(Field(key), value[1]))
else: else:
conditions = conditions.where(_operator(Field(key), value))
if value is not None:
conditions = conditions.where(_operator(Field(key), value))
else:
_table = conditions._from[0]
field = getattr(_table, key)
conditions = conditions.where(field.isnull())

conditions = self.add_conditions(conditions, **kwargs) conditions = self.add_conditions(conditions, **kwargs)
return conditions return conditions




+ 1
- 1
frappe/desk/doctype/dashboard_chart/dashboard_chart.js Ver fichero

@@ -495,7 +495,7 @@ frappe.ui.form.on('Dashboard Chart', {


set_parent_document_type: async function(frm) { set_parent_document_type: async function(frm) {
let document_type = frm.doc.document_type; let document_type = frm.doc.document_type;
let doc_is_table = document_type &&
let doc_is_table = document_type &&
(await frappe.db.get_value('DocType', document_type, 'istable')).message.istable; (await frappe.db.get_value('DocType', document_type, 'istable')).message.istable;


frm.set_df_property('parent_document_type', 'hidden', !doc_is_table); frm.set_df_property('parent_document_type', 'hidden', !doc_is_table);


+ 1
- 1
frappe/desk/doctype/form_tour/form_tour.js Ver fichero

@@ -16,7 +16,7 @@ frappe.ui.form.on('Form Tour', {
frm.add_custom_button(__('Show Tour'), async () => { frm.add_custom_button(__('Show Tour'), async () => {
const issingle = await check_if_single(frm.doc.reference_doctype); const issingle = await check_if_single(frm.doc.reference_doctype);
let route_changed = null; let route_changed = null;
if (issingle) { if (issingle) {
route_changed = frappe.set_route('Form', frm.doc.reference_doctype); route_changed = frappe.set_route('Form', frm.doc.reference_doctype);
} else if (frm.doc.first_document) { } else if (frm.doc.first_document) {


+ 0
- 20
frappe/desk/doctype/kanban_board/kanban_board.py Ver fichero

@@ -76,26 +76,6 @@ def archive_restore_column(board_name, column_title, status):
return doc.columns return doc.columns




@frappe.whitelist()
def update_doc(doc):
'''Updates the doc when card is edited'''
doc = json.loads(doc)

try:
to_update = doc
doctype = doc['doctype']
docname = doc['name']
doc = frappe.get_doc(doctype, docname)
doc.update(to_update)
doc.save()
except:
return {
'doc': doc,
'exc': frappe.utils.get_traceback()
}
return doc


@frappe.whitelist() @frappe.whitelist()
def update_order(board_name, order): def update_order(board_name, order):
'''Save the order of cards in columns''' '''Save the order of cards in columns'''


+ 1
- 1
frappe/desk/doctype/todo/todo_calendar.js Ver fichero

@@ -24,7 +24,7 @@ frappe.views.calendar["ToDo"] = {
"options": "reference_type", "options": "reference_type",
"label": __("Task") "label": __("Task")
} }
], ],
get_events_method: "frappe.desk.calendar.get_events" get_events_method: "frappe.desk.calendar.get_events"
}; };


+ 1
- 1
frappe/desk/doctype/workspace/workspace.js Ver fichero

@@ -9,7 +9,7 @@ frappe.ui.form.on('Workspace', {
refresh: function(frm) { refresh: function(frm) {
frm.enable_save(); frm.enable_save();


if (frm.doc.for_user || (frm.doc.public && !frm.has_perm('write') &&
if (frm.doc.for_user || (frm.doc.public && !frm.has_perm('write') &&
!frappe.user.has_role('Workspace Manager'))) { !frappe.user.has_role('Workspace Manager'))) {
frm.trigger('disable_form'); frm.trigger('disable_form');
} }


+ 4
- 4
frappe/desk/doctype/workspace/workspace.py Ver fichero

@@ -176,9 +176,9 @@ def update_page(name, title, icon, parent, public):


doc = frappe.get_doc("Workspace", name) doc = frappe.get_doc("Workspace", name)


filters = {
filters = {
'parent_page': doc.title, 'parent_page': doc.title,
'public': doc.public
'public': doc.public
} }
child_docs = frappe.get_list("Workspace", filters=filters) child_docs = frappe.get_list("Workspace", filters=filters)


@@ -255,7 +255,7 @@ def delete_page(page):
def sort_pages(sb_public_items, sb_private_items): def sort_pages(sb_public_items, sb_private_items):
if not loads(sb_public_items) and not loads(sb_private_items): if not loads(sb_public_items) and not loads(sb_private_items):
return return
sb_public_items = loads(sb_public_items) sb_public_items = loads(sb_public_items)
sb_private_items = loads(sb_private_items) sb_private_items = loads(sb_private_items)


@@ -292,7 +292,7 @@ def last_sequence_id(doc):
if not doc_exists: if not doc_exists:
return 0 return 0


return frappe.db.get_list('Workspace',
return frappe.db.get_list('Workspace',
fields=['sequence_id'], fields=['sequence_id'],
filters={ filters={
'public': doc.public, 'public': doc.public,


+ 1
- 1
frappe/desk/query_report.py Ver fichero

@@ -406,7 +406,7 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visi
for column in data.columns: for column in data.columns:
if column.get("hidden"): if column.get("hidden"):
continue continue
result[0].append(column.get("label"))
result[0].append(_(column.get("label")))
column_width = cint(column.get('width', 0)) column_width = cint(column.get('width', 0))
# to convert into scale accepted by openpyxl # to convert into scale accepted by openpyxl
column_width /= 10 column_width /= 10


+ 1
- 1
frappe/email/doctype/notification/notification.py Ver fichero

@@ -61,7 +61,7 @@ def get_context(context):
""") """)


def validate_standard(self): def validate_standard(self):
if self.is_standard and not frappe.conf.developer_mode:
if self.is_standard and self.enabled and not frappe.conf.developer_mode:
frappe.throw(_('Cannot edit Standard Notification. To edit, please disable this and duplicate it')) frappe.throw(_('Cannot edit Standard Notification. To edit, please disable this and duplicate it'))


def validate_condition(self): def validate_condition(self):


+ 1
- 1
frappe/email/receive.py Ver fichero

@@ -630,7 +630,7 @@ class InboundMail(Email):
if self.reference_document(): if self.reference_document():
data['reference_doctype'] = self.reference_document().doctype data['reference_doctype'] = self.reference_document().doctype
data['reference_name'] = self.reference_document().name data['reference_name'] = self.reference_document().name
else:
else:
if append_to and append_to != 'Communication': if append_to and append_to != 'Communication':
reference_doc = self._create_reference_document(append_to) reference_doc = self._create_reference_document(append_to)
if reference_doc: if reference_doc:


+ 3
- 3
frappe/hooks.py Ver fichero

@@ -221,7 +221,8 @@ scheduler_events = {
"frappe.deferred_insert.save_to_db", "frappe.deferred_insert.save_to_db",
"frappe.desk.form.document_follow.send_hourly_updates", "frappe.desk.form.document_follow.send_hourly_updates",
"frappe.integrations.doctype.google_calendar.google_calendar.sync", "frappe.integrations.doctype.google_calendar.google_calendar.sync",
"frappe.email.doctype.newsletter.newsletter.send_scheduled_email"
"frappe.email.doctype.newsletter.newsletter.send_scheduled_email",
"frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.process_data_deletion_request"
], ],
"daily": [ "daily": [
"frappe.email.queue.set_expiry_for_email_queue", "frappe.email.queue.set_expiry_for_email_queue",
@@ -240,8 +241,7 @@ scheduler_events = {
"frappe.automation.doctype.auto_repeat.auto_repeat.set_auto_repeat_as_completed", "frappe.automation.doctype.auto_repeat.auto_repeat.set_auto_repeat_as_completed",
"frappe.email.doctype.unhandled_email.unhandled_email.remove_old_unhandled_emails", "frappe.email.doctype.unhandled_email.unhandled_email.remove_old_unhandled_emails",
"frappe.core.doctype.prepared_report.prepared_report.delete_expired_prepared_reports", "frappe.core.doctype.prepared_report.prepared_report.delete_expired_prepared_reports",
"frappe.core.doctype.log_settings.log_settings.run_log_clean_up",
"frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.process_data_deletion_request"
"frappe.core.doctype.log_settings.log_settings.run_log_clean_up"
], ],
"daily_long": [ "daily_long": [
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily", "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily",


+ 1
- 1
frappe/integrations/doctype/razorpay_settings/razorpay_settings.js Ver fichero

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


frappe.ui.form.on('Razorpay Settings', { frappe.ui.form.on('Razorpay Settings', {
refresh: function(frm) { refresh: function(frm) {
} }
}); });

+ 8
- 10
frappe/model/document.py Ver fichero

@@ -471,7 +471,7 @@ class Document(BaseDocument):


# We'd probably want the creation and owner to be set via API # We'd probably want the creation and owner to be set via API
# or Data import at some point, that'd have to be handled here # or Data import at some point, that'd have to be handled here
if self.is_new() and not (frappe.flags.in_patch or frappe.flags.in_migrate):
if self.is_new() and not (frappe.flags.in_install or frappe.flags.in_patch or frappe.flags.in_migrate):
self.creation = self.modified self.creation = self.modified
self.owner = self.modified_by self.owner = self.modified_by


@@ -860,14 +860,14 @@ class Document(BaseDocument):


def run_method(self, method, *args, **kwargs): def run_method(self, method, *args, **kwargs):
"""run standard triggers, plus those in hooks""" """run standard triggers, plus those in hooks"""
if "flags" in kwargs:
del kwargs["flags"]


if hasattr(self, method) and hasattr(getattr(self, method), "__call__"):
fn = lambda self, *args, **kwargs: getattr(self, method)(*args, **kwargs)
else:
# hack! to run hooks even if method does not exist
fn = lambda self, *args, **kwargs: None
def fn(self, *args, **kwargs):
method_object = getattr(self, method, None)

# Cannot have a field with same name as method
# If method found in __dict__, expect it to be callable
if method in self.__dict__ or callable(method_object):
return method_object(*args, **kwargs)


fn.__name__ = str(method) fn.__name__ = str(method)
out = Document.hook(fn)(self, *args, **kwargs) out = Document.hook(fn)(self, *args, **kwargs)
@@ -1003,8 +1003,6 @@ class Document(BaseDocument):
- `on_cancel` for **Cancel** - `on_cancel` for **Cancel**
- `update_after_submit` for **Update after Submit**""" - `update_after_submit` for **Update after Submit**"""


doc_before_save = self.get_doc_before_save()

if self._action=="save": if self._action=="save":
self.run_method("on_update") self.run_method("on_update")
elif self._action=="submit": elif self._action=="submit":


+ 9
- 6
frappe/model/rename_doc.py Ver fichero

@@ -77,13 +77,15 @@ def rename_doc(
) -> str: ) -> str:
"""Rename a doc(dt, old) to doc(dt, new) and update all linked fields of type "Link".""" """Rename a doc(dt, old) to doc(dt, new) and update all linked fields of type "Link"."""
if not frappe.db.exists(doctype, old): if not frappe.db.exists(doctype, old):
frappe.errprint(_("Failed: {0} to {1} because {0} doesn't exist.").format(old, new))
return return


if ignore_if_exists and frappe.db.exists(doctype, new): if ignore_if_exists and frappe.db.exists(doctype, new):
frappe.errprint(_("Failed: {0} to {1} because {1} already exists.").format(old, new))
return return


if old==new: if old==new:
frappe.msgprint(_('Please select a new name to rename'))
frappe.errprint(_("Ignored: {0} to {1} no changes made because old and new name are the same.").format(old, new))
return return


force = cint(force) force = cint(force)
@@ -540,15 +542,16 @@ def bulk_rename(doctype: str, rows: Optional[List[List]] = None, via_console: bo
msg = _("Successful: {0} to {1}").format(row[0], row[1]) msg = _("Successful: {0} to {1}").format(row[0], row[1])
frappe.db.commit() frappe.db.commit()
else: else:
msg = _("Ignored: {0} to {1}").format(row[0], row[1])
msg = None
except Exception as e: except Exception as e:
msg = _("** Failed: {0} to {1}: {2}").format(row[0], row[1], repr(e)) msg = _("** Failed: {0} to {1}: {2}").format(row[0], row[1], repr(e))
frappe.db.rollback() frappe.db.rollback()


if via_console:
print(msg)
else:
rename_log.append(msg)
if msg:
if via_console:
print(msg)
else:
rename_log.append(msg)


frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype', doctype=doctype) frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype', doctype=doctype)




+ 23
- 7
frappe/modules/patch_handler.py Ver fichero

@@ -37,6 +37,7 @@ patches by using INI like file format:
import configparser import configparser
import time import time
from enum import Enum from enum import Enum
from textwrap import dedent, indent
from typing import List, Optional from typing import List, Optional


import frappe import frappe
@@ -148,21 +149,36 @@ def run_single(patchmodule=None, method=None, methodargs=None, force=False):
def execute_patch(patchmodule, method=None, methodargs=None): def execute_patch(patchmodule, method=None, methodargs=None):
"""execute the patch""" """execute the patch"""
block_user(True) block_user(True)
frappe.db.begin()

if patchmodule.startswith("execute:"):
has_patch_file = False
patch = patchmodule.split("execute:")[1]
docstring = ""
else:
has_patch_file = True
patch = f"{patchmodule.split()[0]}.execute"
_patch = frappe.get_attr(patch)
docstring = _patch.__doc__ or ""

if docstring:
docstring = "\n" + indent(dedent(docstring), "\t")

print(f"Executing {patchmodule or methodargs} in {frappe.local.site} ({frappe.db.cur_db_name}){docstring}")

start_time = time.time() start_time = time.time()
frappe.db.begin()
try: try:
print('Executing {patch} in {site} ({db})'.format(patch=patchmodule or str(methodargs),
site=frappe.local.site, db=frappe.db.cur_db_name))
if patchmodule: if patchmodule:
if patchmodule.startswith("finally:"): if patchmodule.startswith("finally:"):
# run run patch at the end # run run patch at the end
frappe.flags.final_patches.append(patchmodule) frappe.flags.final_patches.append(patchmodule)
else: else:
if patchmodule.startswith("execute:"):
exec(patchmodule.split("execute:")[1],globals())
if has_patch_file:
_patch()
else: else:
frappe.get_attr(patchmodule.split()[0] + ".execute")()
exec(patch, globals())
update_patch_log(patchmodule) update_patch_log(patchmodule)

elif method: elif method:
method(**methodargs) method(**methodargs)


@@ -174,7 +190,7 @@ def execute_patch(patchmodule, method=None, methodargs=None):
frappe.db.commit() frappe.db.commit()
end_time = time.time() end_time = time.time()
block_user(False) block_user(False)
print('Success: Done in {time}s'.format(time = round(end_time - start_time, 3)))
print(f"Success: Done in {round(end_time - start_time, 3)}s")


return True return True




+ 2
- 0
frappe/patches.txt Ver fichero

@@ -189,6 +189,7 @@ frappe.patches.v14_0.update_workspace2 # 20.09.2021
frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021 frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021
frappe.patches.v14_0.transform_todo_schema frappe.patches.v14_0.transform_todo_schema
frappe.patches.v14_0.remove_post_and_post_comment frappe.patches.v14_0.remove_post_and_post_comment
frappe.patches.v14_0.reset_creation_datetime


[post_model_sync] [post_model_sync]
frappe.patches.v14_0.drop_data_import_legacy frappe.patches.v14_0.drop_data_import_legacy
@@ -196,3 +197,4 @@ frappe.patches.v14_0.copy_mail_data #08.03.21
frappe.patches.v14_0.update_github_endpoints #08-11-2021 frappe.patches.v14_0.update_github_endpoints #08-11-2021
frappe.patches.v14_0.remove_db_aggregation frappe.patches.v14_0.remove_db_aggregation
frappe.patches.v14_0.update_color_names_in_kanban_board_column frappe.patches.v14_0.update_color_names_in_kanban_board_column
frappe.patches.v14_0.update_auto_account_deletion_duration

+ 1
- 1
frappe/patches/v12_0/set_correct_url_in_files.py Ver fichero

@@ -15,7 +15,7 @@ def execute():
for file in files: for file in files:
file_path = file.file_url file_path = file.file_url
file_name = file_path.split('/')[-1] file_name = file_path.split('/')[-1]
if not file_path.startswith(('/private/', '/files/')): if not file_path.startswith(('/private/', '/files/')):
continue continue




+ 41
- 0
frappe/patches/v14_0/reset_creation_datetime.py Ver fichero

@@ -0,0 +1,41 @@
import glob
import json
import frappe
import os
from frappe.query_builder import DocType as _DocType


def execute():
"""Resetting creation datetimes for DocTypes"""
DocType = _DocType("DocType")
doctype_jsons = glob.glob(
os.path.join("..", "apps", "frappe", "frappe", "**", "doctype", "**", "*.json")
)

frappe_modules = frappe.get_all(
"Module Def", filters={"app_name": "frappe"}, pluck="name"
)
site_doctypes = frappe.get_all(
"DocType",
filters={"module": ("in", frappe_modules), "custom": False},
fields=["name", "creation"],
)

for dt_path in doctype_jsons:
with open(dt_path) as f:
try:
file_schema = frappe._dict(json.load(f))
except Exception:
continue

if not file_schema.name:
continue

_site_schema = [x for x in site_doctypes if x.name == file_schema.name]
if not _site_schema:
continue

if file_schema.creation != _site_schema[0].creation:
frappe.qb.update(DocType).set(
DocType.creation, file_schema.creation
).where(DocType.name == file_schema.name).run()

+ 5
- 0
frappe/patches/v14_0/update_auto_account_deletion_duration.py Ver fichero

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

def execute():
days = frappe.db.get_single_value("Website Settings", "auto_account_deletion")
frappe.db.set_value("Website Settings", None, "auto_account_deletion", days * 24)

+ 1
- 1
frappe/public/js/frappe/form/controls/date.js Ver fichero

@@ -160,7 +160,7 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat
get_df_options() { get_df_options() {
let df_options = this.df.options; let df_options = this.df.options;
if (!df_options) return {}; if (!df_options) return {};
let options = {}; let options = {};
if (typeof df_options === 'string') { if (typeof df_options === 'string') {
try { try {


+ 1
- 1
frappe/public/js/frappe/form/controls/table.js Ver fichero

@@ -92,7 +92,7 @@ frappe.ui.form.ControlTable = class ControlTable extends frappe.ui.form.Control
if (frappe.model.no_value_type.includes(field.fieldtype)) { if (frappe.model.no_value_type.includes(field.fieldtype)) {
return false; return false;
} }
const is_field_matching = () => { const is_field_matching = () => {
return ( return (
field.fieldname.toLowerCase() === field_name || field.fieldname.toLowerCase() === field_name ||


+ 3
- 0
frappe/public/js/frappe/form/controls/text_editor.js Ver fichero

@@ -88,6 +88,9 @@ frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.for
make_quill_editor() { make_quill_editor() {
if (this.quill) return; if (this.quill) return;
this.quill_container = $('<div>').appendTo(this.input_area); this.quill_container = $('<div>').appendTo(this.input_area);
if (this.df.max_height) {
$(this.quill_container).css({'max-height': this.df.max_height, 'overflow': 'auto'});
}
this.quill = new Quill(this.quill_container[0], this.get_quill_options()); this.quill = new Quill(this.quill_container[0], this.get_quill_options());
this.bind_events(); this.bind_events();
} }


+ 1
- 1
frappe/public/js/frappe/form/grid_pagination.js Ver fichero

@@ -66,7 +66,7 @@ export default class GridPagination {
} }


// only allow numbers from 0-9 and up, down, left, right arrow keys // only allow numbers from 0-9 and up, down, left, right arrow keys
if (charCode > 31 && (charCode < 48 || charCode > 57) &&
if (charCode > 31 && (charCode < 48 || charCode > 57) &&
![37, 38, 39, 40].includes(charCode)) { ![37, 38, 39, 40].includes(charCode)) {
return false; return false;
} }


+ 1
- 0
frappe/public/js/frappe/form/grid_row.js Ver fichero

@@ -615,6 +615,7 @@ export default class GridRow {
if (!this.doc) { if (!this.doc) {
$col.attr("title", txt); $col.attr("title", txt);
} }
df.fieldname && $col.static_area.toggleClass('reqd', Boolean(df.reqd));


$col.df = df; $col.df = df;
$col.column_index = ci; $col.column_index = ci;


+ 1
- 0
frappe/public/js/frappe/form/save.js Ver fichero

@@ -148,6 +148,7 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
}); });


if (frm.is_new() && frm.meta.autoname === 'Prompt' && !frm.doc.__newname) { if (frm.is_new() && frm.meta.autoname === 'Prompt' && !frm.doc.__newname) {
has_errors = true;
error_fields = [__('Name'), ...error_fields]; error_fields = [__('Name'), ...error_fields];
} }




+ 4
- 1
frappe/public/js/frappe/form/toolbar.js Ver fichero

@@ -103,7 +103,9 @@ frappe.ui.form.Toolbar = class Toolbar {
docname, docname,
name: new_name, name: new_name,
title: new_title, title: new_title,
merge
merge,
freeze: true,
freeze_message: __("Updating related fields...")
}).then(new_docname => { }).then(new_docname => {
if (new_name != docname) { if (new_name != docname) {
$(document).trigger("rename", [doctype, docname, new_docname || new_name]); $(document).trigger("rename", [doctype, docname, new_docname || new_name]);
@@ -172,6 +174,7 @@ frappe.ui.form.Toolbar = class Toolbar {
d.show(); d.show();
d.set_primary_action(__("Rename"), (values) => { d.set_primary_action(__("Rename"), (values) => {
d.disable_primary_action(); d.disable_primary_action();
d.hide();
this.rename_document_title(values.name, values.title, values.merge) this.rename_document_title(values.name, values.title, values.merge)
.then(() => { .then(() => {
d.hide(); d.hide();


+ 1
- 1
frappe/public/js/frappe/list/list_view_select.js Ver fichero

@@ -150,7 +150,7 @@ frappe.views.ListViewSelect = class ListViewSelect {
const views_wrapper = this.sidebar.sidebar.find(".views-section"); const views_wrapper = this.sidebar.sidebar.find(".views-section");
views_wrapper.find(".sidebar-label").html(`${__(view)}`); views_wrapper.find(".sidebar-label").html(`${__(view)}`);
const $dropdown = views_wrapper.find(".views-dropdown"); const $dropdown = views_wrapper.find(".views-dropdown");
let placeholder = `${__("Select {0}", [__(view)])}`; let placeholder = `${__("Select {0}", [__(view)])}`;
let html = ``; let html = ``;




+ 3
- 0
frappe/public/js/frappe/model/model.js Ver fichero

@@ -615,10 +615,13 @@ $.extend(frappe.model, {
}); });


d.set_primary_action(__("Rename"), function() { d.set_primary_action(__("Rename"), function() {
d.hide();
var args = d.get_values(); var args = d.get_values();
if(!args) return; if(!args) return;
return frappe.call({ return frappe.call({
method:"frappe.rename_doc", method:"frappe.rename_doc",
freeze: true,
freeze_message: "Updating related fields...",
args: { args: {
doctype: doctype, doctype: doctype,
old: docname, old: docname,


+ 5
- 0
frappe/public/js/frappe/request.js Ver fichero

@@ -50,6 +50,11 @@ frappe.call = function(opts) {
} }
var args = $.extend({}, opts.args); var args = $.extend({}, opts.args);


if (args.freeze) {
opts.freeze = opts.freeze || args.freeze;
opts.freeze_message = opts.freeze_message || args.freeze_message;
}

// cmd // cmd
if(opts.module && opts.page) { if(opts.module && opts.page) {
args.cmd = opts.module+'.page.'+opts.page+'.'+opts.page+'.'+opts.method; args.cmd = opts.module+'.page.'+opts.page+'.'+opts.page+'.'+opts.method;


+ 4
- 4
frappe/public/js/frappe/ui/filters/field_select.js Ver fichero

@@ -112,9 +112,9 @@ frappe.ui.FieldSelect = class FieldSelect {
// main table // main table
var main_table_fields = std_filters.concat(frappe.meta.docfield_list[me.doctype]); var main_table_fields = std_filters.concat(frappe.meta.docfield_list[me.doctype]);
$.each(frappe.utils.sort(main_table_fields, "label", "string"), function(i, df) { $.each(frappe.utils.sort(main_table_fields, "label", "string"), function(i, df) {
let doctype = frappe.get_meta(me.doctype).istable && me.parent_doctype ?
let doctype = frappe.get_meta(me.doctype).istable && me.parent_doctype ?
me.parent_doctype : me.doctype; me.parent_doctype : me.doctype;
// show fields where user has read access and if report hide flag is not set // show fields where user has read access and if report hide flag is not set
if (frappe.perm.has_perm(doctype, df.permlevel, "read")) if (frappe.perm.has_perm(doctype, df.permlevel, "read"))
me.add_field_option(df); me.add_field_option(df);
@@ -132,9 +132,9 @@ frappe.ui.FieldSelect = class FieldSelect {
} }


$.each(frappe.utils.sort(child_table_fields, "label", "string"), function(i, df) { $.each(frappe.utils.sort(child_table_fields, "label", "string"), function(i, df) {
let doctype = frappe.get_meta(me.doctype).istable && me.parent_doctype ?
let doctype = frappe.get_meta(me.doctype).istable && me.parent_doctype ?
me.parent_doctype : me.doctype; me.parent_doctype : me.doctype;
// show fields where user has read access and if report hide flag is not set // show fields where user has read access and if report hide flag is not set
if (frappe.perm.has_perm(doctype, df.permlevel, "read")) if (frappe.perm.has_perm(doctype, df.permlevel, "read"))
me.add_field_option(df); me.add_field_option(df);


+ 3
- 3
frappe/public/js/frappe/utils/utils.js Ver fichero

@@ -244,7 +244,7 @@ Object.assign(frappe.utils, {
}; };


return String(txt).replace( return String(txt).replace(
/[&<>"'`=/]/g,
/[&<>"'`=/]/g,
char => escape_html_mapping[char] || char char => escape_html_mapping[char] || char
); );
}, },
@@ -262,7 +262,7 @@ Object.assign(frappe.utils, {
}; };


return String(txt).replace( return String(txt).replace(
/&amp;|&lt;|&gt;|&quot;|&#39;|&#x2F;|&#x60;|&#x3D;/g,
/&amp;|&lt;|&gt;|&quot;|&#39;|&#x2F;|&#x60;|&#x3D;/g,
char => unescape_html_mapping[char] || char char => unescape_html_mapping[char] || char
); );
}, },
@@ -1435,7 +1435,7 @@ Object.assign(frappe.utils, {
// for link titles // for link titles
frappe._link_titles = {}; frappe._link_titles = {};
} }
frappe._link_titles[doctype + "::" + name] = value; frappe._link_titles[doctype + "::" + name] = value;
}, },




+ 0
- 12
frappe/public/js/frappe/views/kanban/kanban_board.js Ver fichero

@@ -150,18 +150,6 @@ frappe.provide("frappe.views");
} }
updater.set({ cards: cards }); updater.set({ cards: cards });
}, },
update_doc: function(updater, doc, card) {
var state = this;
return frappe.call({
method: method_prefix + "update_doc",
args: { doc: doc },
freeze: true
}).then(function(r) {
var updated_doc = r.message;
var updated_card = prepare_card(card, state, updated_doc);
fluxify.doAction('update_card', updated_card);
});
},
update_order_for_single_card: function(updater, card) { update_order_for_single_card: function(updater, card) {
// cache original order // cache original order
const _cards = this.cards.slice(); const _cards = this.cards.slice();


+ 1
- 1
frappe/public/js/frappe/views/reports/query_report.js Ver fichero

@@ -1343,7 +1343,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
if (file_format === 'CSV') { if (file_format === 'CSV') {
const column_row = this.columns.reduce((acc, col) => { const column_row = this.columns.reduce((acc, col) => {
if (!col.hidden) { if (!col.hidden) {
acc.push(col.label);
acc.push(__(col.label));
} }
return acc; return acc;
}, []); }, []);


+ 3
- 3
frappe/public/js/frappe/views/workspace/blocks/block.js Ver fichero

@@ -50,7 +50,7 @@ export default class Block {
document.documentElement.addEventListener('mousemove', do_drag, false); document.documentElement.addEventListener('mousemove', do_drag, false);
document.documentElement.addEventListener('mouseup', stop_drag, false); document.documentElement.addEventListener('mouseup', stop_drag, false);
} }
function do_drag(e) { function do_drag(e) {
$(this).css("cursor", "col-resize"); $(this).css("cursor", "col-resize");
$('.widget').css("pointer-events", "none"); $('.widget').css("pointer-events", "none");
@@ -72,7 +72,7 @@ export default class Block {
} else { } else {
window.getSelection().removeAllRanges(); window.getSelection().removeAllRanges();
} }
}
}


function stop_drag() { function stop_drag() {
$(this).css("cursor", "default"); $(this).css("cursor", "default");
@@ -221,7 +221,7 @@ export default class Block {
$widget_control.prepend($button); $widget_control.prepend($button);


this.dropdown_list.forEach((item) => { this.dropdown_list.forEach((item) => {
if ((item.label == 'Expand' || item.label == 'Shrink') &&
if ((item.label == 'Expand' || item.label == 'Shrink') &&
me.options && !me.options.allow_resize) { me.options && !me.options.allow_resize) {
return; return;
} }


+ 1
- 1
frappe/public/js/frappe/views/workspace/blocks/header.js Ver fichero

@@ -107,7 +107,7 @@ export default class Header extends Block {
if (data.text !== undefined) { if (data.text !== undefined) {
let text = this._data.text || ''; let text = this._data.text || '';
const contains_html_tag = /<[a-z][\s\S]*>/i.test(text); const contains_html_tag = /<[a-z][\s\S]*>/i.test(text);
this._element.innerHTML = contains_html_tag ?
this._element.innerHTML = contains_html_tag ?
text : `<span class="h${this._settings.default_size}">${text}</span>`; text : `<span class="h${this._settings.default_size}">${text}</span>`;
} }




+ 3
- 3
frappe/public/js/frappe/views/workspace/blocks/header_size.js Ver fichero

@@ -36,7 +36,7 @@ export default class HeaderSize {


checkState(selection) { checkState(selection) {
let termWrapper = this.api.selection.findParentTag('SPAN'); let termWrapper = this.api.selection.findParentTag('SPAN');
for (const h of ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']) { for (const h of ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']) {
if (termWrapper && termWrapper.classList.contains(h)) { if (termWrapper && termWrapper.classList.contains(h)) {
let num = h.match(/\d+/)[0]; let num = h.match(/\d+/)[0];
@@ -57,7 +57,7 @@ export default class HeaderSize {
span.innerText = range.toString(); span.innerText = range.toString();


this.remove_parent_tag(range, range.commonAncestorContainer, span); this.remove_parent_tag(range, range.commonAncestorContainer, span);
range.extractContents(); range.extractContents();
range.insertNode(span); range.insertNode(span);
this.api.inlineToolbar.close(); this.api.inlineToolbar.close();
@@ -90,7 +90,7 @@ export default class HeaderSize {
renderActions() { renderActions() {
this.actions = document.createElement('div'); this.actions = document.createElement('div');
this.actions.classList = 'header-level-select'; this.actions.classList = 'header-level-select';
this.headerLevels = new Array(6).fill().map((_, idx) => { this.headerLevels = new Array(6).fill().map((_, idx) => {
const $header_level = document.createElement('div'); const $header_level = document.createElement('div');
$header_level.classList.add(`h${idx+1}`, 'header-level'); $header_level.classList.add(`h${idx+1}`, 'header-level');


+ 1
- 1
frappe/public/js/frappe/views/workspace/blocks/paragraph.js Ver fichero

@@ -116,7 +116,7 @@ export default class Paragraph extends Block {
this.wrapper.appendChild(this._element); this.wrapper.appendChild(this._element);
this._element.classList.remove('widget'); this._element.classList.remove('widget');
$para_control.appendTo(this.wrapper); $para_control.appendTo(this.wrapper);
this.wrapper.classList.add('widget', 'paragraph', 'edit-mode'); this.wrapper.classList.add('widget', 'paragraph', 'edit-mode');


this.open_block_list(); this.open_block_list();


+ 11
- 11
frappe/public/js/frappe/views/workspace/workspace.js Ver fichero

@@ -219,7 +219,7 @@ frappe.views.Workspace = class Workspace {
$sidebar[0].firstElementChild.classList.add("selected"); $sidebar[0].firstElementChild.classList.add("selected");
if (sidebar_page) sidebar_page.selected = true; if (sidebar_page) sidebar_page.selected = true;


// open child sidebar section if closed
// open child sidebar section if closed
$sidebar.parent().hasClass('hidden') && $sidebar.parent().hasClass('hidden') &&
$sidebar.parent().removeClass('hidden'); $sidebar.parent().removeClass('hidden');


@@ -244,7 +244,7 @@ frappe.views.Workspace = class Workspace {
this.pages[page.name] = data.message; this.pages[page.name] = data.message;


if (!this.page_data || Object.keys(this.page_data).length === 0) return; if (!this.page_data || Object.keys(this.page_data).length === 0) return;
if (this.page_data.charts && this.page_data.charts.items.length === 0) return;
if (this.page_data.charts && this.page_data.charts.items.length === 0) return;


return frappe.dashboard_utils.get_dashboard_settings().then(settings => { return frappe.dashboard_utils.get_dashboard_settings().then(settings => {
if (settings) { if (settings) {
@@ -596,9 +596,9 @@ frappe.views.Workspace = class Workspace {
} }


update_cached_values(old_item, new_item, duplicate, new_page) { update_cached_values(old_item, new_item, duplicate, new_page) {
let [from_pages, to_pages] = old_item.public ?
let [from_pages, to_pages] = old_item.public ?
[this.public_pages, this.private_pages] : [this.private_pages, this.public_pages]; [this.public_pages, this.private_pages] : [this.private_pages, this.public_pages];
let old_item_index = from_pages.findIndex(page => page.title == old_item.title); let old_item_index = from_pages.findIndex(page => page.title == old_item.title);
duplicate && old_item_index++; duplicate && old_item_index++;


@@ -859,7 +859,7 @@ frappe.views.Workspace = class Workspace {
public: page.attributes['item-public'].value public: page.attributes['item-public'].value
}); });


let $drop_icon = $(page).find('.sidebar-item-control .drop-icon').first();
let $drop_icon = $(page).find('.sidebar-item-control .drop-icon').first();
if ($(page).find('.sidebar-child-item > *').length != 0) { if ($(page).find('.sidebar-child-item > *').length != 0) {
$drop_icon.removeClass('hidden'); $drop_icon.removeClass('hidden');
} else { } else {
@@ -993,13 +993,13 @@ frappe.views.Workspace = class Workspace {
} }
} }
}); });
this.update_cached_values(new_page, new_page, true, true); this.update_cached_values(new_page, new_page, true, true);
let pre_url = new_page.public ? '' : 'private/'; let pre_url = new_page.public ? '' : 'private/';
let route = pre_url + frappe.router.slug(new_page.title); let route = pre_url + frappe.router.slug(new_page.title);
frappe.set_route(route); frappe.set_route(route);
this.make_sidebar(); this.make_sidebar();
this.show_sidebar_actions(); this.show_sidebar_actions();
}); });
@@ -1010,15 +1010,15 @@ frappe.views.Workspace = class Workspace {


validate_page(new_page, old_page) { validate_page(new_page, old_page) {
let message = ""; let message = "";
let [from_pages, to_pages] = new_page.is_public ?
let [from_pages, to_pages] = new_page.is_public ?
[this.private_pages, this.public_pages] : [this.public_pages, this.private_pages]; [this.private_pages, this.public_pages] : [this.public_pages, this.private_pages];


let section = this.sidebar_categories[new_page.is_public]; let section = this.sidebar_categories[new_page.is_public];


if (to_pages && to_pages.filter(p => p.title == new_page.title)[0]) { if (to_pages && to_pages.filter(p => p.title == new_page.title)[0]) {
message = `Page with title ${new_page.title} already exist.`; message = `Page with title ${new_page.title} already exist.`;
}
}
if (frappe.router.doctype_route_exist(frappe.router.slug(new_page.title))) { if (frappe.router.doctype_route_exist(frappe.router.slug(new_page.title))) {
message = "Doctype with same route already exist. Please choose different title."; message = "Doctype with same route already exist. Please choose different title.";
} }


+ 2
- 2
frappe/public/js/frappe/widgets/chart_widget.js Ver fichero

@@ -698,12 +698,12 @@ export default class ChartWidget extends Widget {
.get_filters_for_chart_type(this.chart_doc).then(filters => { .get_filters_for_chart_type(this.chart_doc).then(filters => {
chart_saved_filters = this.update_default_date_filters(filters, chart_saved_filters); chart_saved_filters = this.update_default_date_filters(filters, chart_saved_filters);
this.filters = this.filters =
frappe.utils.parse_array(user_saved_filters) || frappe.utils.parse_array(this.filters)
frappe.utils.parse_array(user_saved_filters) || frappe.utils.parse_array(this.filters)
|| frappe.utils.parse_array(chart_saved_filters); || frappe.utils.parse_array(chart_saved_filters);
}); });
} else { } else {
this.filters = this.filters =
frappe.utils.parse_array(user_saved_filters) || frappe.utils.parse_array(this.filters)
frappe.utils.parse_array(user_saved_filters) || frappe.utils.parse_array(this.filters)
|| frappe.utils.parse_array(chart_saved_filters); || frappe.utils.parse_array(chart_saved_filters);
return Promise.resolve(); return Promise.resolve();
} }


+ 1
- 1
frappe/public/js/lib/jSignature.min.js Ver fichero

@@ -993,7 +993,7 @@ jSignatureClass.prototype.resetCanvas = function(data, dontClear){
ctx.shadowBlur = 0; ctx.shadowBlur = 0;
} }
} }
ctx.strokeStyle = settings.color; ctx.strokeStyle = settings.color;


// setting up new dataEngine // setting up new dataEngine


+ 5
- 5
frappe/public/js/lib/photoswipe/default-skin.css Ver fichero

@@ -12,7 +12,7 @@


*/ */
/* /*
1. Buttons 1. Buttons


*/ */
@@ -257,7 +257,7 @@ a.pswp__share--download:hover {
padding: 0 10px; } padding: 0 10px; }


/* /*
4. Caption 4. Caption


*/ */
@@ -338,8 +338,8 @@ a.pswp__share--download:hover {
margin: 0; } margin: 0; }


.pswp--css_animation .pswp__preloader__cut { .pswp--css_animation .pswp__preloader__cut {
/*
The idea of animating inner circle is based on Polymer ("material") loading indicator
/*
The idea of animating inner circle is based on Polymer ("material") loading indicator
by Keanu Lee https://blog.keanulee.com/2014/10/20/the-tale-of-three-spinners.html by Keanu Lee https://blog.keanulee.com/2014/10/20/the-tale-of-three-spinners.html
*/ */
position: relative; position: relative;
@@ -409,7 +409,7 @@ a.pswp__share--download:hover {
transform: rotate(0); } } transform: rotate(0); } }


/* /*
6. Additional styles 6. Additional styles


*/ */


+ 81
- 81
frappe/public/js/lib/photoswipe/photoswipe-ui-default.js Ver fichero

@@ -5,9 +5,9 @@
* *
* UI on top of main sliding area (caption, arrows, close button, etc.). * UI on top of main sliding area (caption, arrows, close button, etc.).
* Built just using public methods/properties of PhotoSwipe. * Built just using public methods/properties of PhotoSwipe.
*
*
*/ */
(function (root, factory) {
(function (root, factory) {
if (typeof define === 'function' && define.amd) { if (typeof define === 'function' && define.amd) {
define(factory); define(factory);
} else if (typeof exports === 'object') { } else if (typeof exports === 'object') {
@@ -48,11 +48,11 @@ var PhotoSwipeUI_Default =
_options, _options,
_defaultUIOptions = { _defaultUIOptions = {
barsSize: {top:44, bottom:'auto'}, barsSize: {top:44, bottom:'auto'},
closeElClasses: ['item', 'caption', 'zoom-wrap', 'ui', 'top-bar'],
timeToIdle: 4000,
closeElClasses: ['item', 'caption', 'zoom-wrap', 'ui', 'top-bar'],
timeToIdle: 4000,
timeToIdleOutside: 1000, timeToIdleOutside: 1000,
loadingIndicatorDelay: 1000, // 2s loadingIndicatorDelay: 1000, // 2s
addCaptionHTMLFn: function(item, captionEl /*, isFake */) { addCaptionHTMLFn: function(item, captionEl /*, isFake */) {
if(!item.title) { if(!item.title) {
captionEl.children[0].innerHTML = ''; captionEl.children[0].innerHTML = '';
@@ -92,7 +92,7 @@ var PhotoSwipeUI_Default =
getTextForShare: function( /* shareButtonData */ ) { getTextForShare: function( /* shareButtonData */ ) {
return pswp.currItem.title || ''; return pswp.currItem.title || '';
}, },
indexIndicatorSep: ' / ', indexIndicatorSep: ' / ',
fitControlsWidth: 1200 fitControlsWidth: 1200


@@ -136,12 +136,12 @@ var PhotoSwipeUI_Default =
} }
_blockControlsTap = true; _blockControlsTap = true;


// Some versions of Android don't prevent ghost click event
// Some versions of Android don't prevent ghost click event
// when preventDefault() was called on touchstart and/or touchend. // when preventDefault() was called on touchstart and/or touchend.
//
// This happens on v4.3, 4.2, 4.1,
// older versions strangely work correctly,
// but just in case we add delay on all of them)
//
// This happens on v4.3, 4.2, 4.1,
// older versions strangely work correctly,
// but just in case we add delay on all of them)
var tapDelay = framework.features.isOldAndroid ? 600 : 30; var tapDelay = framework.features.isOldAndroid ? 600 : 30;
_blockControlsTapTimeout = setTimeout(function() { _blockControlsTapTimeout = setTimeout(function() {
_blockControlsTap = false; _blockControlsTap = false;
@@ -172,8 +172,8 @@ var PhotoSwipeUI_Default =
_toggleShareModal = function() { _toggleShareModal = function() {


_shareModalHidden = !_shareModalHidden; _shareModalHidden = !_shareModalHidden;
if(!_shareModalHidden) { if(!_shareModalHidden) {
_toggleShareModalClass(); _toggleShareModalClass();
setTimeout(function() { setTimeout(function() {
@@ -189,7 +189,7 @@ var PhotoSwipeUI_Default =
} }
}, 300); }, 300);
} }
if(!_shareModalHidden) { if(!_shareModalHidden) {
_updateShareURLs(); _updateShareURLs();
} }
@@ -211,13 +211,13 @@ var PhotoSwipeUI_Default =
} }


window.open(target.href, 'pswp_share', 'scrollbars=yes,resizable=yes,toolbar=no,'+ window.open(target.href, 'pswp_share', 'scrollbars=yes,resizable=yes,toolbar=no,'+
'location=yes,width=550,height=420,top=100,left=' +
'location=yes,width=550,height=420,top=100,left=' +
(window.screen ? Math.round(screen.width / 2 - 275) : 100) ); (window.screen ? Math.round(screen.width / 2 - 275) : 100) );


if(!_shareModalHidden) { if(!_shareModalHidden) {
_toggleShareModal(); _toggleShareModal();
} }
return false; return false;
}, },
_updateShareURLs = function() { _updateShareURLs = function() {
@@ -242,7 +242,7 @@ var PhotoSwipeUI_Default =


shareButtonOut += '<a href="' + shareURL + '" target="_blank" '+ shareButtonOut += '<a href="' + shareURL + '" target="_blank" '+
'class="pswp__share--' + shareButtonData.id + '"' + 'class="pswp__share--' + shareButtonData.id + '"' +
(shareButtonData.download ? 'download' : '') + '>' +
(shareButtonData.download ? 'download' : '') + '>' +
shareButtonData.label + '</a>'; shareButtonData.label + '</a>';


if(_options.parseShareButtonOut) { if(_options.parseShareButtonOut) {
@@ -297,7 +297,7 @@ var PhotoSwipeUI_Default =
_setupLoadingIndicator = function() { _setupLoadingIndicator = function() {
// Setup loading indicator // Setup loading indicator
if(_options.preloaderEl) { if(_options.preloaderEl) {
_toggleLoadingIndicator(true); _toggleLoadingIndicator(true);


_listen('beforeChange', function() { _listen('beforeChange', function() {
@@ -310,18 +310,18 @@ var PhotoSwipeUI_Default =
if(pswp.currItem && pswp.currItem.loading) { if(pswp.currItem && pswp.currItem.loading) {


if( !pswp.allowProgressiveImg() || (pswp.currItem.img && !pswp.currItem.img.naturalWidth) ) { if( !pswp.allowProgressiveImg() || (pswp.currItem.img && !pswp.currItem.img.naturalWidth) ) {
// show preloader if progressive loading is not enabled,
// show preloader if progressive loading is not enabled,
// or image width is not defined yet (because of slow connection) // or image width is not defined yet (because of slow connection)
_toggleLoadingIndicator(false);
_toggleLoadingIndicator(false);
// items-controller.js function allowProgressiveImg // items-controller.js function allowProgressiveImg
} }
} else { } else {
_toggleLoadingIndicator(true); // hide preloader _toggleLoadingIndicator(true); // hide preloader
} }


}, _options.loadingIndicatorDelay); }, _options.loadingIndicatorDelay);
}); });
_listen('imageLoadComplete', function(index, item) { _listen('imageLoadComplete', function(index, item) {
if(pswp.currItem === item) { if(pswp.currItem === item) {
@@ -341,8 +341,8 @@ var PhotoSwipeUI_Default =
var gap = item.vGap; var gap = item.vGap;


if( _fitControlsInViewport() ) { if( _fitControlsInViewport() ) {
var bars = _options.barsSize;
var bars = _options.barsSize;
if(_options.captionEl && bars.bottom === 'auto') { if(_options.captionEl && bars.bottom === 'auto') {
if(!_fakeCaptionContainer) { if(!_fakeCaptionContainer) {
_fakeCaptionContainer = framework.createEl('pswp__caption pswp__caption--fake'); _fakeCaptionContainer = framework.createEl('pswp__caption pswp__caption--fake');
@@ -360,7 +360,7 @@ var PhotoSwipeUI_Default =
} else { } else {
gap.bottom = bars.bottom === 'auto' ? 0 : bars.bottom; gap.bottom = bars.bottom === 'auto' ? 0 : bars.bottom;
} }
// height of top bar is static, no need to calculate it // height of top bar is static, no need to calculate it
gap.top = bars.top; gap.top = bars.top;
} else { } else {
@@ -371,7 +371,7 @@ var PhotoSwipeUI_Default =
// Hide controls when mouse is used // Hide controls when mouse is used
if(_options.timeToIdle) { if(_options.timeToIdle) {
_listen('mouseUsed', function() { _listen('mouseUsed', function() {
framework.bind(document, 'mousemove', _onIdleMouseMove); framework.bind(document, 'mousemove', _onIdleMouseMove);
framework.bind(document, 'mouseout', _onMouseLeaveWindow); framework.bind(document, 'mouseout', _onMouseLeaveWindow);


@@ -418,77 +418,77 @@ var PhotoSwipeUI_Default =




var _uiElements = [ var _uiElements = [
{
name: 'caption',
{
name: 'caption',
option: 'captionEl', option: 'captionEl',
onInit: function(el) {
_captionContainer = el;
}
onInit: function(el) {
_captionContainer = el;
}
}, },
{
name: 'share-modal',
{
name: 'share-modal',
option: 'shareEl', option: 'shareEl',
onInit: function(el) {
onInit: function(el) {
_shareModal = el; _shareModal = el;
}, },
onTap: function() { onTap: function() {
_toggleShareModal(); _toggleShareModal();
}
}
}, },
{
name: 'button--share',
{
name: 'button--share',
option: 'shareEl', option: 'shareEl',
onInit: function(el) {
onInit: function(el) {
_shareButton = el; _shareButton = el;
}, },
onTap: function() { onTap: function() {
_toggleShareModal(); _toggleShareModal();
}
}
}, },
{
name: 'button--zoom',
{
name: 'button--zoom',
option: 'zoomEl', option: 'zoomEl',
onTap: pswp.toggleDesktopZoom onTap: pswp.toggleDesktopZoom
}, },
{
name: 'counter',
{
name: 'counter',
option: 'counterEl', option: 'counterEl',
onInit: function(el) {
onInit: function(el) {
_indexIndicator = el; _indexIndicator = el;
}
}
}, },
{
name: 'button--close',
{
name: 'button--close',
option: 'closeEl', option: 'closeEl',
onTap: pswp.close onTap: pswp.close
}, },
{
name: 'button--arrow--left',
{
name: 'button--arrow--left',
option: 'arrowEl', option: 'arrowEl',
onTap: pswp.prev onTap: pswp.prev
}, },
{
name: 'button--arrow--right',
{
name: 'button--arrow--right',
option: 'arrowEl', option: 'arrowEl',
onTap: pswp.next onTap: pswp.next
}, },
{
name: 'button--fs',
{
name: 'button--fs',
option: 'fullscreenEl', option: 'fullscreenEl',
onTap: function() {
onTap: function() {
if(_fullscrenAPI.isFullscreen()) { if(_fullscrenAPI.isFullscreen()) {
_fullscrenAPI.exit(); _fullscrenAPI.exit();
} else { } else {
_fullscrenAPI.enter(); _fullscrenAPI.enter();
} }
}
}
}, },
{
name: 'preloader',
{
name: 'preloader',
option: 'preloaderEl', option: 'preloaderEl',
onInit: function(el) {
onInit: function(el) {
_loadingIndicator = el; _loadingIndicator = el;
}
}
} }


]; ];
@@ -514,12 +514,12 @@ var PhotoSwipeUI_Default =
if(classAttr.indexOf('pswp__' + uiElement.name) > -1 ) { if(classAttr.indexOf('pswp__' + uiElement.name) > -1 ) {


if( _options[uiElement.option] ) { // if element is not disabled from options if( _options[uiElement.option] ) { // if element is not disabled from options
framework.removeClass(item, 'pswp__element--disabled'); framework.removeClass(item, 'pswp__element--disabled');
if(uiElement.onInit) { if(uiElement.onInit) {
uiElement.onInit(item); uiElement.onInit(item);
} }
//item.style.display = 'block'; //item.style.display = 'block';
} else { } else {
framework.addClass(item, 'pswp__element--disabled'); framework.addClass(item, 'pswp__element--disabled');
@@ -538,7 +538,7 @@ var PhotoSwipeUI_Default =
}; };






ui.init = function() { ui.init = function() {


@@ -574,9 +574,9 @@ var PhotoSwipeUI_Default =
_listen('preventDragEvent', function(e, isDown, preventObj) { _listen('preventDragEvent', function(e, isDown, preventObj) {
var t = e.target || e.srcElement; var t = e.target || e.srcElement;
if( if(
t &&
t.getAttribute('class') && e.type.indexOf('mouse') > -1 &&
( t.getAttribute('class').indexOf('__caption') > 0 || (/(SMALL|STRONG|EM)/i).test(t.tagName) )
t &&
t.getAttribute('class') && e.type.indexOf('mouse') > -1 &&
( t.getAttribute('class').indexOf('__caption') > 0 || (/(SMALL|STRONG|EM)/i).test(t.tagName) )
) { ) {
preventObj.prevent = false; preventObj.prevent = false;
} }
@@ -634,7 +634,7 @@ var PhotoSwipeUI_Default =
framework.addClass( _controls, 'pswp__ui--hidden'); framework.addClass( _controls, 'pswp__ui--hidden');
ui.setIdle(false); ui.setIdle(false);
}); });


if(!_options.showAnimationDuration) { if(!_options.showAnimationDuration) {
framework.removeClass( _controls, 'pswp__ui--hidden'); framework.removeClass( _controls, 'pswp__ui--hidden');
@@ -649,7 +649,7 @@ var PhotoSwipeUI_Default =
}); });


_listen('parseVerticalMargin', _applyNavBarGaps); _listen('parseVerticalMargin', _applyNavBarGaps);
_setupUIElements(); _setupUIElements();


if(_options.shareEl && _shareButton && _shareModal) { if(_options.shareEl && _shareButton && _shareModal) {
@@ -673,7 +673,7 @@ var PhotoSwipeUI_Default =
ui.update = function() { ui.update = function() {
// Don't update UI if it's hidden // Don't update UI if it's hidden
if(_controlsVisible && pswp.currItem) { if(_controlsVisible && pswp.currItem) {
ui.updateIndexIndicator(); ui.updateIndexIndicator();


if(_options.captionEl) { if(_options.captionEl) {
@@ -704,19 +704,19 @@ var PhotoSwipeUI_Default =
pswp.setScrollOffset( 0, framework.getScrollY() ); pswp.setScrollOffset( 0, framework.getScrollY() );
}, 50); }, 50);
} }
// toogle pswp--fs class on root element // toogle pswp--fs class on root element
framework[ (_fullscrenAPI.isFullscreen() ? 'add' : 'remove') + 'Class' ](pswp.template, 'pswp--fs'); framework[ (_fullscrenAPI.isFullscreen() ? 'add' : 'remove') + 'Class' ](pswp.template, 'pswp--fs');
}; };


ui.updateIndexIndicator = function() { ui.updateIndexIndicator = function() {
if(_options.counterEl) { if(_options.counterEl) {
_indexIndicator.innerHTML = (pswp.getCurrentIndex()+1) +
_options.indexIndicatorSep +
_indexIndicator.innerHTML = (pswp.getCurrentIndex()+1) +
_options.indexIndicatorSep +
_options.getNumItemsFn(); _options.getNumItemsFn();
} }
}; };
ui.onGlobalTap = function(e) { ui.onGlobalTap = function(e) {
e = e || window.event; e = e || window.event;
var target = e.target || e.srcElement; var target = e.target || e.srcElement;
@@ -742,7 +742,7 @@ var PhotoSwipeUI_Default =
pswp.toggleDesktopZoom(e.detail.releasePoint); pswp.toggleDesktopZoom(e.detail.releasePoint);
} }
} }
} else { } else {


// tap anywhere (except buttons) to toggle visibility of controls // tap anywhere (except buttons) to toggle visibility of controls
@@ -759,7 +759,7 @@ var PhotoSwipeUI_Default =
pswp.close(); pswp.close();
return; return;
} }
} }
}; };
ui.onMouseOver = function(e) { ui.onMouseOver = function(e) {
@@ -809,7 +809,7 @@ var PhotoSwipeUI_Default =
eventK: 'moz' + tF eventK: 'moz' + tF
}; };




} else if(dE.webkitRequestFullscreen) { } else if(dE.webkitRequestFullscreen) {
api = { api = {
@@ -829,21 +829,21 @@ var PhotoSwipeUI_Default =
} }


if(api) { if(api) {
api.enter = function() {
api.enter = function() {
// disable close-on-scroll in fullscreen // disable close-on-scroll in fullscreen
_initalCloseOnScrollValue = _options.closeOnScroll;
_options.closeOnScroll = false;
_initalCloseOnScrollValue = _options.closeOnScroll;
_options.closeOnScroll = false;


if(this.enterK === 'webkitRequestFullscreen') { if(this.enterK === 'webkitRequestFullscreen') {
pswp.template[this.enterK]( Element.ALLOW_KEYBOARD_INPUT ); pswp.template[this.enterK]( Element.ALLOW_KEYBOARD_INPUT );
} else { } else {
return pswp.template[this.enterK]();
return pswp.template[this.enterK]();
} }
}; };
api.exit = function() {
api.exit = function() {
_options.closeOnScroll = _initalCloseOnScrollValue; _options.closeOnScroll = _initalCloseOnScrollValue;


return document[this.exitK]();
return document[this.exitK]();


}; };
api.isFullscreen = function() { return document[this.elementK]; }; api.isFullscreen = function() { return document[this.elementK]; };


+ 263
- 263
frappe/public/js/lib/photoswipe/photoswipe.js
La diferencia del archivo ha sido suprimido porque es demasiado grande
Ver fichero


+ 2
- 2
frappe/public/js/lib/prettydate.js Ver fichero

@@ -11,10 +11,10 @@ function prettyDate(time){
var date = new Date((time || "").replace(/-/g,"/").replace(/[TZ]/g," ").replace(/\.[0-9]*/, "")), var date = new Date((time || "").replace(/-/g,"/").replace(/[TZ]/g," ").replace(/\.[0-9]*/, "")),
diff = (((new Date()).getTime() - date.getTime()) / 1000), diff = (((new Date()).getTime() - date.getTime()) / 1000),
day_diff = Math.floor(diff / 86400); day_diff = Math.floor(diff / 86400);
if ( isNaN(day_diff) || day_diff < 0 ) if ( isNaN(day_diff) || day_diff < 0 )
return ''; return '';
return day_diff == 0 && ( return day_diff == 0 && (
diff < 60 && "just now" || diff < 60 && "just now" ||
diff < 120 && "1 minute ago" || diff < 120 && "1 minute ago" ||


+ 7
- 0
frappe/public/scss/common/grid.scss Ver fichero

@@ -12,6 +12,13 @@
border-bottom: 1px solid var(--table-border-color); border-bottom: 1px solid var(--table-border-color);
color: var(--text-muted); color: var(--text-muted);
font-size: var(--text-md); font-size: var(--text-md);

.grid-static-col {
.static-area.reqd:after {
content: ' *';
color: var(--red-400);
}
}
} }


.rows .grid-row .data-row, .rows .grid-row .data-row,


+ 10
- 10
frappe/public/scss/desk/desktop.scss Ver fichero

@@ -1070,11 +1070,11 @@ body {
} }


.resizer { .resizer {
width: 10px;
width: 10px;
height: 100%; height: 100%;
position:absolute;
right: 0;
bottom: 0;
position:absolute;
right: 0;
bottom: 0;
cursor: col-resize; cursor: col-resize;
border-color: transparent; border-color: transparent;
transition: border-color 0.3s ease-in-out; transition: border-color 0.3s ease-in-out;
@@ -1089,8 +1089,8 @@ body {
margin-bottom: 0 !important; margin-bottom: 0 !important;
flex: 1; flex: 1;


&:focus {
outline: none;
&:focus {
outline: none;
} }
} }


@@ -1124,11 +1124,11 @@ body {
color: var(--text-muted); color: var(--text-muted);
border: 1px dashed var(--gray-400); border: 1px dashed var(--gray-400);
cursor: pointer; cursor: pointer;
.widget-control > * { .widget-control > * {
width: auto; width: auto;
} }
.spacer-left { .spacer-left {
min-width: 74px; min-width: 74px;
} }
@@ -1158,7 +1158,7 @@ body {
gap: 5px; gap: 5px;
background-color: var(--card-bg); background-color: var(--card-bg);
padding-left: 5px; padding-left: 5px;
.drag-handle { .drag-handle {
cursor: all-scroll; cursor: all-scroll;
cursor: grabbing; cursor: grabbing;
@@ -1325,7 +1325,7 @@ body {
padding: 6px 10px; padding: 6px 10px;
font-size: small; font-size: small;
border-radius: var(--border-radius-sm); border-radius: var(--border-radius-sm);
margin: 1px 0px;
margin: 1px 0px;
} }


.dropdown-item-icon { .dropdown-item-icon {


+ 1
- 1
frappe/public/scss/desk/list.scss Ver fichero

@@ -202,7 +202,7 @@ $level-margin-right: 8px;
box-shadow: none; box-shadow: none;
margin-left: 0px !important; margin-left: 0px !important;
border: 1px solid var(--dark-border-color); border: 1px solid var(--dark-border-color);
&.btn-info { &.btn-info {
background-color: var(--gray-400); background-color: var(--gray-400);
border-color: var(--gray-400); border-color: var(--gray-400);


+ 1
- 1
frappe/public/scss/login.bundle.scss Ver fichero

@@ -150,7 +150,7 @@ body {
min-width: 50%; min-width: 50%;
padding: 0 4px; padding: 0 4px;
margin-bottom: var(--margin-md); margin-bottom: var(--margin-md);
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }


+ 6
- 6
frappe/public/scss/website/blog.scss Ver fichero

@@ -163,18 +163,18 @@
padding: var(--padding-lg); padding: var(--padding-lg);
box-shadow: var(--card-shadow); box-shadow: var(--card-shadow);
border-radius: var(--border-radius-md); border-radius: var(--border-radius-md);
.new-comment-fields { .new-comment-fields {
flex: 1; flex: 1;
.form-label { .form-label {
font-weight: var(--text-bold); font-weight: var(--text-bold);
} }
.comment-text-area textarea { .comment-text-area textarea {
resize: none; resize: none;
} }
@media (min-width: 576px) { @media (min-width: 576px) {
.comment-by { .comment-by {
padding-right: 0px !important; padding-right: 0px !important;
@@ -184,7 +184,7 @@
} }
} }
} }


#comment-list { #comment-list {
position: relative; position: relative;
@@ -206,7 +206,7 @@
top: 10px; top: 10px;
left: -17px; left: -17px;
} }
.comment-content { .comment-content {
box-shadow: var(--card-shadow); box-shadow: var(--card-shadow);
border-radius: var(--border-radius-md); border-radius: var(--border-radius-md);


+ 1
- 1
frappe/public/scss/website/error-state.scss Ver fichero

@@ -9,7 +9,7 @@
width: 80% width: 80%
} }
} }
.back-to-home { .back-to-home {
font-size: var(--text-base); font-size: var(--text-base);
} }


+ 3
- 1
frappe/public/scss/website/index.scss Ver fichero

@@ -80,6 +80,8 @@


.dropdown-menu { .dropdown-menu {
padding: 0.25rem; padding: 0.25rem;
box-shadow: var(--shadow-lg);
border-color: var(--gray-200);
} }


.dropdown-item { .dropdown-item {
@@ -308,4 +310,4 @@ h5.modal-title {


.empty-list-icon { .empty-list-icon {
height: 70px; height: 70px;
}
}

+ 1
- 1
frappe/public/scss/website/navbar.scss Ver fichero

@@ -46,7 +46,7 @@


.navbar-toggler { .navbar-toggler {
border-color: rgba(255,255,255, 0.1); border-color: rgba(255,255,255, 0.1);
.icon { .icon {
stroke: none; stroke: none;
} }


+ 1
- 1
frappe/public/scss/website/portal.scss Ver fichero

@@ -1,6 +1,6 @@
.portal-row { .portal-row {
padding: 1rem 0; padding: 1rem 0;
a { a {
color: $body-color; color: $body-color;
} }

+ 1
- 1
frappe/public/scss/website/web_form.scss Ver fichero

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


[data-doctype="Web Form"] { [data-doctype="Web Form"] {
.page-content-wrapper { .page-content-wrapper {
.breadcrumb-container.container { .breadcrumb-container.container {
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
padding-left: 0; padding-left: 0;


+ 12
- 6
frappe/social/doctype/energy_point_log/test_energy_point_log.py Ver fichero

@@ -2,12 +2,14 @@
# Copyright (c) 2019, Frappe Technologies and Contributors # Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE # License: MIT. See LICENSE
import frappe import frappe
import unittest

from frappe.tests.utils import FrappeTestCase
from .energy_point_log import get_energy_points as _get_energy_points, create_review_points_log, review from .energy_point_log import get_energy_points as _get_energy_points, create_review_points_log, review
from frappe.utils.testutils import add_custom_field, clear_custom_fields from frappe.utils.testutils import add_custom_field, clear_custom_fields
from frappe.desk.form.assign_to import add as assign_to from frappe.desk.form.assign_to import add as assign_to


class TestEnergyPointLog(unittest.TestCase):

class TestEnergyPointLog(FrappeTestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
settings = frappe.get_single('Energy Point Settings') settings = frappe.get_single('Energy Point Settings')
@@ -140,9 +142,10 @@ class TestEnergyPointLog(unittest.TestCase):


# for criticism # for criticism
criticism_points = 2 criticism_points = 2
todo = create_a_todo(description='Bad patch')
energy_points_before_review = energy_points_after_review energy_points_before_review = energy_points_after_review
review_points_before_review = review_points_after_review review_points_before_review = review_points_after_review
review(created_todo, criticism_points, 'test@example.com', 'You could have done better.', 'Criticism')
review(todo, criticism_points, 'test@example.com', 'You could have done better.', 'Criticism')
energy_points_after_review = get_points('test@example.com') energy_points_after_review = get_points('test@example.com')
review_points_after_review = get_points('test2@example.com', 'review_points') review_points_after_review = get_points('test2@example.com', 'review_points')
self.assertEqual(energy_points_after_review, energy_points_before_review - criticism_points) self.assertEqual(energy_points_after_review, energy_points_before_review - criticism_points)
@@ -332,11 +335,14 @@ def create_energy_point_rule_for_todo(multiplier_field=None, for_doc_event='Cust
'apply_only_once': apply_once 'apply_only_once': apply_once
}).insert(ignore_permissions=1) }).insert(ignore_permissions=1)


def create_a_todo():

def create_a_todo(description=None):
if not description:
description = 'Fix a bug'
return frappe.get_doc({ return frappe.get_doc({
'doctype': 'ToDo', 'doctype': 'ToDo',
'description': 'Fix a bug',
}).insert()
'description': description,
}).insert(ignore_permissions=True)




def get_points(user, point_type='energy_points'): def get_points(user, point_type='energy_points'):


+ 1
- 1
frappe/templates/includes/feedback/feedback.html Ver fichero

@@ -38,6 +38,6 @@
like like
} }
}); });
}
}
}); });
</script> </script>

+ 5
- 1
frappe/templates/includes/footer/footer_info.html Ver fichero

@@ -12,7 +12,11 @@
{# powered #} {# powered #}
<div class="footer-col-right col-sm-6 col-12 footer-powered"> <div class="footer-col-right col-sm-6 col-12 footer-powered">
{% block powered %} {% block powered %}
{% include "templates/includes/footer/footer_powered.html" %}
{%- if footer_powered -%}
{{ footer_powered }}
{%- else -%}
{% include "templates/includes/footer/footer_powered.html" %}
{%- endif -%}
{% endblock %} {% endblock %}
</div> </div>
</div> </div>


+ 0
- 2
frappe/templates/includes/website_theme/navbar.css Ver fichero

@@ -95,8 +95,6 @@
min-width: 200px; min-width: 200px;
padding: 0px; padding: 0px;
font-size: 85%; font-size: 85%;

// only rounded bottoms
border-radius: 0px 0px 4px 4px; border-radius: 0px 0px 4px 4px;
} }




+ 1
- 1
frappe/templates/pages/integrations/payment-success.html Ver fichero

@@ -11,7 +11,7 @@
<p>{{ payment_message or _("Your payment was successfully accepted") }}</p> <p>{{ payment_message or _("Your payment was successfully accepted") }}</p>
{% if not payment_message %} {% if not payment_message %}
<div> <div>
<a
<a
href='{{ frappe.form_dict.redirect_to or "/" }}' href='{{ frappe.form_dict.redirect_to or "/" }}'
class='btn btn-primary btn-sm'> class='btn btn-primary btn-sm'>
{{ _("Continue") }} {{ _("Continue") }}


+ 6
- 2
frappe/tests/test_db_query.py Ver fichero

@@ -97,6 +97,12 @@ class TestReportview(unittest.TestCase):
self.assertFalse(result self.assertFalse(result
in DatabaseQuery("DocType").execute(filters={"name": ["not in", 'DocType,DocField']})) in DatabaseQuery("DocType").execute(filters={"name": ["not in", 'DocType,DocField']}))


def test_none_filter(self):
query = frappe.db.query.get_sql("DocType", fields="name", filters={"restrict_to_domain": None})
sql = str(query).replace('`', '').replace('"', '')
condition = 'restrict_to_domain IS NULL'
self.assertIn(condition, sql)

def test_or_filters(self): def test_or_filters(self):
data = DatabaseQuery("DocField").execute( data = DatabaseQuery("DocField").execute(
filters={"parent": "DocType"}, fields=["fieldname", "fieldtype"], filters={"parent": "DocType"}, fields=["fieldname", "fieldtype"],
@@ -149,7 +155,6 @@ class TestReportview(unittest.TestCase):
filters={"creation": ["between", ["2016-07-06", "2016-07-07"]]}, filters={"creation": ["between", ["2016-07-06", "2016-07-07"]]},
fields=["name"]) fields=["name"])



def test_ignore_permissions_for_get_filters_cond(self): def test_ignore_permissions_for_get_filters_cond(self):
frappe.set_user('test2@example.com') frappe.set_user('test2@example.com')
self.assertRaises(frappe.PermissionError, get_filters_cond, 'DocType', dict(istable=1), []) self.assertRaises(frappe.PermissionError, get_filters_cond, 'DocType', dict(istable=1), [])
@@ -351,7 +356,6 @@ class TestReportview(unittest.TestCase):
self.assertTrue(len(data) == 0) self.assertTrue(len(data) == 0)
self.assertTrue(len(frappe.get_all('Nested DocType', {'name': ('not ancestors of', 'Root')})) == len(frappe.get_all('Nested DocType'))) self.assertTrue(len(frappe.get_all('Nested DocType', {'name': ('not ancestors of', 'Root')})) == len(frappe.get_all('Nested DocType')))



def test_is_set_is_not_set(self): def test_is_set_is_not_set(self):
res = DatabaseQuery('DocType').execute(filters={'autoname': ['is', 'not set']}) res = DatabaseQuery('DocType').execute(filters={'autoname': ['is', 'not set']})
self.assertTrue({'name': 'Integration Request'} in res) self.assertTrue({'name': 'Integration Request'} in res)


+ 18
- 0
frappe/tests/test_document.py Ver fichero

@@ -319,3 +319,21 @@ class TestDocument(unittest.TestCase):
self.assertIsInstance(doc, Note) self.assertIsInstance(doc, Note)
self.assertIsInstance(doc.as_dict().get("age"), timedelta) self.assertIsInstance(doc.as_dict().get("age"), timedelta)
self.assertIsInstance(doc.get_valid_dict().get("age"), timedelta) self.assertIsInstance(doc.get_valid_dict().get("age"), timedelta)

def test_run_method(self):
doc = frappe.get_last_doc("User")

# Case 1: Override with a string
doc.as_dict = ""

# run_method should throw TypeError
self.assertRaisesRegex(TypeError, "not callable", doc.run_method, "as_dict")

# Case 2: Override with a function
def my_as_dict(*args, **kwargs):
return "success"

doc.as_dict = my_as_dict

# run_method should get overridden
self.assertEqual(doc.run_method("as_dict"), "success")

+ 1
- 1
frappe/utils/make_random.py Ver fichero

@@ -35,7 +35,7 @@ def get_random(doctype, filters=None, doc=False):
condition = " where " + " and ".join(condition) condition = " where " + " and ".join(condition)
else: else:
condition = "" condition = ""
out = frappe.db.multisql({ out = frappe.db.multisql({
'mariadb': """select name from `tab%s` %s 'mariadb': """select name from `tab%s` %s
order by RAND() limit 1 offset 0""" % (doctype, condition), order by RAND() limit 1 offset 0""" % (doctype, condition),


+ 3
- 3
frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py Ver fichero

@@ -7,7 +7,7 @@ import re
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import get_fullname, date_diff, get_datetime
from frappe.utils import get_fullname, time_diff_in_hours, get_datetime
from frappe.utils.user import get_system_managers from frappe.utils.user import get_system_managers
from frappe.utils.verified_command import get_signed_params, verify_request from frappe.utils.verified_command import get_signed_params, verify_request
import json import json
@@ -353,8 +353,8 @@ def process_data_deletion_request():


for request in requests: for request in requests:
doc = frappe.get_doc("Personal Data Deletion Request", request) doc = frappe.get_doc("Personal Data Deletion Request", request)
if date_diff(get_datetime(), doc.creation) >= auto_account_deletion:
doc.add_comment("Comment", _("The User record for this request has been auto-deleted due to inactivity."))
if time_diff_in_hours(get_datetime(), doc.creation) >= auto_account_deletion:
doc.add_comment("Comment", _("The User record for this request has been auto-deleted due to inactivity by system admins."))
doc.trigger_data_deletion() doc.trigger_data_deletion()


def remove_unverified_record(): def remove_unverified_record():


+ 14
- 2
frappe/website/doctype/personal_data_deletion_request/test_personal_data_deletion_request.py Ver fichero

@@ -4,10 +4,10 @@
import frappe import frappe
import unittest import unittest
from frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request import ( from frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request import (
remove_unverified_record,
remove_unverified_record, process_data_deletion_request
) )
from frappe.website.doctype.personal_data_download_request.test_personal_data_download_request import ( from frappe.website.doctype.personal_data_download_request.test_personal_data_download_request import (
create_user_if_not_exists,
create_user_if_not_exists
) )
from datetime import datetime, timedelta from datetime import datetime, timedelta


@@ -58,3 +58,15 @@ class TestPersonalDataDeletionRequest(unittest.TestCase):
self.assertFalse( self.assertFalse(
frappe.db.exists("Personal Data Deletion Request", self.delete_request.name) frappe.db.exists("Personal Data Deletion Request", self.delete_request.name)
) )

def test_process_auto_request(self):
frappe.db.set_value("Website Settings", None, "auto_account_deletion", "1")
date_time_obj = datetime.strptime(
self.delete_request.creation, "%Y-%m-%d %H:%M:%S.%f"
) + timedelta(hours=-2)
self.delete_request.db_set("creation", date_time_obj)
self.delete_request.db_set("status", "Pending Approval")

process_data_deletion_request()
self.delete_request.reload()
self.assertEqual(self.delete_request.status, "Deleted")

+ 14
- 6
frappe/website/doctype/website_settings/website_settings.json Ver fichero

@@ -42,6 +42,7 @@
"copyright", "copyright",
"address", "address",
"footer_items", "footer_items",
"footer_powered",
"footer_template", "footer_template",
"footer_template_values", "footer_template_values",
"edit_footer_template_values", "edit_footer_template_values",
@@ -142,7 +143,6 @@
}, },
{ {
"collapsible": 1, "collapsible": 1,
"collapsible_depends_on": "top_bar_items",
"fieldname": "top_bar", "fieldname": "top_bar",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Navbar" "label": "Navbar"
@@ -189,7 +189,8 @@
"description": "Address and other legal information you may want to put in the footer.", "description": "Address and other legal information you may want to put in the footer.",
"fieldname": "address", "fieldname": "address",
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
"label": "Address"
"label": "Address",
"max_height": "8rem"
}, },
{ {
"fieldname": "footer_items", "fieldname": "footer_items",
@@ -391,6 +392,7 @@
"label": "App Logo" "label": "App Logo"
}, },
{ {
"collapsible": 1,
"fieldname": "account_deletion_settings_section", "fieldname": "account_deletion_settings_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Account Deletion Settings" "label": "Account Deletion Settings"
@@ -402,10 +404,15 @@
"label": "Show Account Deletion Link in My Account Page" "label": "Show Account Deletion Link in My Account Page"
}, },
{ {
"default": "3",
"default": "72",
"fieldname": "auto_account_deletion", "fieldname": "auto_account_deletion",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Auto Account Deletion within (Days)"
"label": "Auto Account Deletion within (Hours)"
},
{
"fieldname": "footer_powered",
"fieldtype": "Small Text",
"label": "Footer \"Powered By\""
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
@@ -414,7 +421,7 @@
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"max_attachments": 10, "max_attachments": 10,
"modified": "2021-12-15 17:28:59.255184",
"modified": "2022-02-24 15:37:22.360138",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Website", "module": "Website",
"name": "Website Settings", "name": "Website Settings",
@@ -437,5 +444,6 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"states": [],
"track_changes": 1 "track_changes": 1
}
}

+ 2
- 1
frappe/website/doctype/website_settings/website_settings.py Ver fichero

@@ -120,7 +120,8 @@ def get_website_settings(context=None):
"facebook_share", "google_plus_one", "twitter_share", "linked_in_share", "facebook_share", "google_plus_one", "twitter_share", "linked_in_share",
"disable_signup", "hide_footer_signup", "head_html", "title_prefix", "disable_signup", "hide_footer_signup", "head_html", "title_prefix",
"navbar_template", "footer_template", "navbar_search", "enable_view_tracking", "navbar_template", "footer_template", "navbar_search", "enable_view_tracking",
"footer_logo", "call_to_action", "call_to_action_url", "show_language_picker"]:
"footer_logo", "call_to_action", "call_to_action_url", "show_language_picker",
"footer_powered"]:
if hasattr(settings, k): if hasattr(settings, k):
context[k] = settings.get(k) context[k] = settings.get(k)




+ 1
- 1
frappe/website/web_form/request_to_delete_data/request_to_delete_data.js Ver fichero

@@ -5,7 +5,7 @@ frappe.ready(function() {
callback: (data) => { callback: (data) => {
if (data.message) { if (data.message) {
const intro_wrapper = $('#introduction .ql-editor.read-mode'); const intro_wrapper = $('#introduction .ql-editor.read-mode');
const sla_description = __("Note: Your request for account deletion will be fulfilled within {0} days.", [data.message]);
const sla_description = __("Note: Your request for account deletion will be fulfilled within {0} hours.", [data.message]);
const sla_description_wrapper = `<br><b>${sla_description}</b>`; const sla_description_wrapper = `<br><b>${sla_description}</b>`;
intro_wrapper.html(intro_wrapper.html() + sla_description_wrapper); intro_wrapper.html(intro_wrapper.html() + sla_description_wrapper);
} }


+ 50
- 1
frappe/workflow/doctype/workflow/test_workflow.py Ver fichero

@@ -5,6 +5,7 @@ import unittest
from frappe.utils import random_string from frappe.utils import random_string
from frappe.model.workflow import apply_workflow, WorkflowTransitionError, WorkflowPermissionError, get_common_transition_actions from frappe.model.workflow import apply_workflow, WorkflowTransitionError, WorkflowPermissionError, get_common_transition_actions
from frappe.test_runner import make_test_records from frappe.test_runner import make_test_records
from frappe.query_builder import DocType




class TestWorkflow(unittest.TestCase): class TestWorkflow(unittest.TestCase):
@@ -15,9 +16,31 @@ class TestWorkflow(unittest.TestCase):
def setUp(self): def setUp(self):
self.workflow = create_todo_workflow() self.workflow = create_todo_workflow()
frappe.set_user('Administrator') frappe.set_user('Administrator')
if self._testMethodName == "test_if_workflow_actions_were_processed_using_user":
if not frappe.db.has_column('Workflow Action', 'user'):
# mariadb would raise this statement would create an implicit commit
# if we do not commit before alter statement
# nosemgrep
frappe.db.commit()
frappe.db.multisql({
'mariadb': 'ALTER TABLE `tabWorkflow Action` ADD COLUMN user varchar(140)',
'postgres': 'ALTER TABLE "tabWorkflow Action" ADD COLUMN "user" varchar(140)'
})
frappe.cache().delete_value('table_columns')


def tearDown(self): def tearDown(self):
frappe.delete_doc('Workflow', 'Test ToDo') frappe.delete_doc('Workflow', 'Test ToDo')
if self._testMethodName == "test_if_workflow_actions_were_processed_using_user":
if frappe.db.has_column('Workflow Action', 'user'):
# mariadb would raise this statement would create an implicit commit
# if we do not commit before alter statement
# nosemgrep
frappe.db.commit()
frappe.db.multisql({
'mariadb': 'ALTER TABLE `tabWorkflow Action` DROP COLUMN user',
'postgres': 'ALTER TABLE "tabWorkflow Action" DROP COLUMN "user"'
})
frappe.cache().delete_value('table_columns')


def test_default_condition(self): def test_default_condition(self):
'''test default condition is set''' '''test default condition is set'''
@@ -75,7 +98,7 @@ class TestWorkflow(unittest.TestCase):
actions = get_common_transition_actions([todo1, todo2], 'ToDo') actions = get_common_transition_actions([todo1, todo2], 'ToDo')
self.assertListEqual(actions, ['Review']) self.assertListEqual(actions, ['Review'])


def test_if_workflow_actions_were_processed(self):
def test_if_workflow_actions_were_processed_using_role(self):
frappe.db.delete("Workflow Action") frappe.db.delete("Workflow Action")
user = frappe.get_doc('User', 'test2@example.com') user = frappe.get_doc('User', 'test2@example.com')
user.add_roles('Test Approver', 'System Manager') user.add_roles('Test Approver', 'System Manager')
@@ -93,6 +116,32 @@ class TestWorkflow(unittest.TestCase):
self.assertEqual(workflow_actions[0].status, 'Completed') self.assertEqual(workflow_actions[0].status, 'Completed')
frappe.set_user('Administrator') frappe.set_user('Administrator')


def test_if_workflow_actions_were_processed_using_user(self):
frappe.db.delete("Workflow Action")

user = frappe.get_doc('User', 'test2@example.com')
user.add_roles('Test Approver', 'System Manager')
frappe.set_user('test2@example.com')

doc = self.test_default_condition()
workflow_actions = frappe.get_all('Workflow Action', fields=['*'])
self.assertEqual(len(workflow_actions), 1)

# test if status of workflow actions are updated on approval
WorkflowAction = DocType("Workflow Action")
WorkflowActionPermittedRole = DocType("Workflow Action Permitted Role")
frappe.qb.update(WorkflowAction).set(WorkflowAction.user, 'test2@example.com').run()
frappe.qb.update(WorkflowActionPermittedRole).set(WorkflowActionPermittedRole.role, '').run()

self.test_approve(doc)

user.remove_roles('Test Approver', 'System Manager')
workflow_actions = frappe.get_all('Workflow Action', fields=['status'])
self.assertEqual(len(workflow_actions), 1)
self.assertEqual(workflow_actions[0].status, 'Completed')
frappe.set_user('Administrator')


def test_update_docstatus(self): def test_update_docstatus(self):
todo = create_new_todo() todo = create_new_todo()
apply_workflow(todo, 'Approve') apply_workflow(todo, 'Approve')


+ 33
- 11
frappe/workflow/doctype/workflow_action/workflow_action.json Ver fichero

@@ -8,9 +8,12 @@
"status", "status",
"reference_name", "reference_name",
"reference_doctype", "reference_doctype",
"user",
"workflow_state", "workflow_state",
"completed_by"
"column_break_5",
"completed_by_role",
"completed_by",
"permitted_roles",
"user"
], ],
"fields": [ "fields": [
{ {
@@ -24,16 +27,14 @@
"fieldname": "reference_name", "fieldname": "reference_name",
"fieldtype": "Dynamic Link", "fieldtype": "Dynamic Link",
"label": "Reference Name", "label": "Reference Name",
"options": "reference_doctype",
"search_index": 1
"options": "reference_doctype"
}, },
{ {
"fieldname": "reference_doctype", "fieldname": "reference_doctype",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Reference Document Type", "label": "Reference Document Type",
"options": "DocType",
"search_index": 1
"options": "DocType"
}, },
{ {
"fieldname": "user", "fieldname": "user",
@@ -47,18 +48,38 @@
"fieldname": "workflow_state", "fieldname": "workflow_state",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Workflow State",
"search_index": 1
"label": "Workflow State"
}, },
{ {
"depends_on": "eval: doc.completed_by",
"fieldname": "completed_by", "fieldname": "completed_by",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Completed By",
"options": "User"
"label": "Completed By User",
"options": "User",
"read_only": 1
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"depends_on": "eval: doc.completed_by_role",
"fieldname": "completed_by_role",
"fieldtype": "Link",
"label": "Completed By Role",
"options": "Role",
"read_only": 1
},
{
"fieldname": "permitted_roles",
"fieldtype": "Table MultiSelect",
"label": "Permitted Roles",
"options": "Workflow Action Permitted Role",
"read_only": 1
} }
], ],
"links": [], "links": [],
"modified": "2021-07-01 09:07:52.848618",
"modified": "2022-02-23 21:06:45.122258",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Workflow", "module": "Workflow",
"name": "Workflow Action", "name": "Workflow Action",
@@ -72,6 +93,7 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"title_field": "reference_name", "title_field": "reference_name",
"track_changes": 1 "track_changes": 1
} }

+ 131
- 36
frappe/workflow/doctype/workflow_action/workflow_action.py Ver fichero

@@ -13,24 +13,46 @@ from frappe.model.workflow import apply_workflow, get_workflow_name, has_approva
from frappe.desk.notifications import clear_doctype_notifications from frappe.desk.notifications import clear_doctype_notifications
from frappe.utils.user import get_users_with_role from frappe.utils.user import get_users_with_role
from frappe.utils.data import get_link_to_form from frappe.utils.data import get_link_to_form
from frappe.query_builder import DocType


class WorkflowAction(Document): class WorkflowAction(Document):
pass pass




def on_doctype_update(): def on_doctype_update():
frappe.db.add_index("Workflow Action", ["status", "user"])
# The search order in any use case is no ["reference_name", "reference_doctype", "status"]
# The index scan would happen from left to right
# so even if status is not in the where clause the index will be used
frappe.db.add_index("Workflow Action", ["reference_name", "reference_doctype", "status"])


def get_permission_query_conditions(user): def get_permission_query_conditions(user):
if not user: user = frappe.session.user if not user: user = frappe.session.user


if user == "Administrator": return "" if user == "Administrator": return ""


return "(`tabWorkflow Action`.`user`='{user}')".format(user=user)
roles = frappe.get_roles(user)

WorkflowAction = DocType("Workflow Action")
WorkflowActionPermittedRole = DocType("Workflow Action Permitted Role")

permitted_workflow_actions = (frappe.qb.from_(WorkflowAction)
.join(WorkflowActionPermittedRole)
.on(WorkflowAction.name == WorkflowActionPermittedRole.parent)
.select(WorkflowAction.name)
.where(WorkflowActionPermittedRole.role.isin(roles))
).get_sql()

return f"""(`tabWorkflow Action`.`name` in ({permitted_workflow_actions})
or `tabWorkflow Action`.`user`='{user}')
and `tabWorkflow Action`.`status`='Open'"""


def has_permission(doc, user): def has_permission(doc, user):
if user not in ['Administrator', doc.user]:
return False

user_roles = set(frappe.get_roles(user))

permitted_roles = {permitted_role.role for permitted_role in doc.permitted_roles}

return user == "Administrator" or user_roles.intersection(permitted_roles)


def process_workflow_actions(doc, state): def process_workflow_actions(doc, state):
workflow = get_workflow_name(doc.get('doctype')) workflow = get_workflow_name(doc.get('doctype'))
@@ -42,19 +64,18 @@ def process_workflow_actions(doc, state):


if is_workflow_action_already_created(doc): return if is_workflow_action_already_created(doc): return


clear_old_workflow_actions(doc)
update_completed_workflow_actions(doc)
update_completed_workflow_actions(doc, workflow=workflow, workflow_state=get_doc_workflow_state(doc))
clear_doctype_notifications('Workflow Action') clear_doctype_notifications('Workflow Action')


next_possible_transitions = get_next_possible_transitions(workflow, get_doc_workflow_state(doc), doc) next_possible_transitions = get_next_possible_transitions(workflow, get_doc_workflow_state(doc), doc)


if not next_possible_transitions: return if not next_possible_transitions: return


user_data_map = get_users_next_action_data(next_possible_transitions, doc)
user_data_map, roles = get_users_next_action_data(next_possible_transitions, doc)


if not user_data_map: return if not user_data_map: return


create_workflow_actions_for_users(user_data_map.keys(), doc)
create_workflow_actions_for_roles(roles, doc)


if send_email_alert(workflow): if send_email_alert(workflow):
enqueue(send_workflow_action_email, queue='short', users_data=list(user_data_map.values()), doc=doc) enqueue(send_workflow_action_email, queue='short', users_data=list(user_data_map.values()), doc=doc)
@@ -132,20 +153,85 @@ def return_link_expired_page(doc, doc_workflow_state):
frappe.bold(frappe.get_value('User', doc.get("modified_by"), 'full_name')) frappe.bold(frappe.get_value('User', doc.get("modified_by"), 'full_name'))
), indicator_color='blue') ), indicator_color='blue')


def clear_old_workflow_actions(doc, user=None):

def update_completed_workflow_actions(doc, user=None, workflow=None, workflow_state=None):
allowed_roles = get_allowed_roles(user, workflow, workflow_state)
# There is no transaction leading upto this state
# so no older actions to complete
if not allowed_roles:
return
if workflow_action := get_workflow_action_by_role(doc, allowed_roles):
update_completed_workflow_actions_using_role(doc, user, allowed_roles, workflow_action)
else:
# backwards compatibility
# for workflow actions saved using user
clear_old_workflow_actions_using_user(doc, user)
update_completed_workflow_actions_using_user(doc, user)

def get_allowed_roles(user, workflow, workflow_state):
user = user if user else frappe.session.user user = user if user else frappe.session.user
frappe.db.delete("Workflow Action", {
"reference_doctype": doc.get("doctype"),
"reference_name": doc.get("name"),
"user": ("!=", user),
"status": "Open"
})


def update_completed_workflow_actions(doc, user=None):
allowed_roles = frappe.get_all('Workflow Transition',
fields='allowed',
filters=[
['parent', '=', workflow],
['next_state', '=', workflow_state]
],
pluck = 'allowed')

user_roles = set(frappe.get_roles(user))
return set(allowed_roles).intersection(user_roles)

def get_workflow_action_by_role(doc, allowed_roles):
WorkflowAction = DocType("Workflow Action")
WorkflowActionPermittedRole = DocType("Workflow Action Permitted Role")
return (frappe.qb.from_(WorkflowAction).join(WorkflowActionPermittedRole)
.on(WorkflowAction.name == WorkflowActionPermittedRole.parent)
.select(WorkflowAction.name, WorkflowActionPermittedRole.role)
.where((WorkflowAction.reference_name == doc.get('name'))
& (WorkflowAction.reference_doctype == doc.get('doctype'))
& (WorkflowAction.status == 'Open')
& (WorkflowActionPermittedRole.role.isin(list(allowed_roles))))
.orderby(WorkflowActionPermittedRole.role).limit(1)).run(as_dict=True)

def update_completed_workflow_actions_using_role(doc, user=None, allowed_roles = set(), workflow_action=None):
user = user if user else frappe.session.user user = user if user else frappe.session.user
frappe.db.sql("""UPDATE `tabWorkflow Action` SET `status`='Completed', `completed_by`=%s
WHERE `reference_doctype`=%s AND `reference_name`=%s AND `user`=%s AND `status`='Open'""",
(user, doc.get('doctype'), doc.get('name'), user))
WorkflowAction = DocType("Workflow Action")

if not workflow_action:
return

(frappe.qb.update(WorkflowAction)
.set(WorkflowAction.status, 'Completed')
.set(WorkflowAction.completed_by, user)
.set(WorkflowAction.completed_by_role, workflow_action[0].role)
.where(WorkflowAction.name == workflow_action[0].name)
).run()

def clear_old_workflow_actions_using_user(doc, user=None):
user = user if user else frappe.session.user

if frappe.db.has_column('Workflow Action', 'user'):
frappe.db.delete("Workflow Action", {
"reference_name": doc.get("name"),
"reference_doctype": doc.get("doctype"),
"status": "Open",
"user": ("!=", user)
})

def update_completed_workflow_actions_using_user(doc, user=None):
user = user or frappe.session.user

if frappe.db.has_column('Workflow Action', 'user'):
WorkflowAction = DocType("Workflow Action")
(frappe.qb.update(WorkflowAction)
.set(WorkflowAction.status, 'Completed')
.set(WorkflowAction.completed_by, user)
.where((WorkflowAction.reference_name == doc.get('name'))
& (WorkflowAction.reference_doctype == doc.get('doctype'))
& (WorkflowAction.status == 'Open')
& (WorkflowAction.user == user))
).run()


def get_next_possible_transitions(workflow_name, state, doc=None): def get_next_possible_transitions(workflow_name, state, doc=None):
transitions = frappe.get_all('Workflow Transition', transitions = frappe.get_all('Workflow Transition',
@@ -167,8 +253,10 @@ def get_next_possible_transitions(workflow_name, state, doc=None):
return transitions_to_return return transitions_to_return


def get_users_next_action_data(transitions, doc): def get_users_next_action_data(transitions, doc):
roles = set()
user_data_map = {} user_data_map = {}
for transition in transitions: for transition in transitions:
roles.add(transition.allowed)
users = get_users_with_role(transition.allowed) users = get_users_with_role(transition.allowed)
filtered_users = filter_allowed_users(users, doc, transition) filtered_users = filter_allowed_users(users, doc, transition)
for user in filtered_users: for user in filtered_users:
@@ -182,19 +270,24 @@ def get_users_next_action_data(transitions, doc):
'action_name': transition.action, 'action_name': transition.action,
'action_link': get_workflow_action_url(transition.action, doc, user) 'action_link': get_workflow_action_url(transition.action, doc, user)
})) }))
return user_data_map
return user_data_map, roles




def create_workflow_actions_for_users(users, doc):
for user in users:
frappe.get_doc({
'doctype': 'Workflow Action',
'reference_doctype': doc.get('doctype'),
'reference_name': doc.get('name'),
'workflow_state': get_doc_workflow_state(doc),
'status': 'Open',
'user': user
}).insert(ignore_permissions=True)
def create_workflow_actions_for_roles(roles, doc):
workflow_action = frappe.get_doc({
'doctype': 'Workflow Action',
'reference_doctype': doc.get('doctype'),
'reference_name': doc.get('name'),
'workflow_state': get_doc_workflow_state(doc),
'status': 'Open',
})

for role in roles:
workflow_action.append('permitted_roles', {
'role': role
})

workflow_action.insert(ignore_permissions=True)


def send_workflow_action_email(users_data, doc): def send_workflow_action_email(users_data, doc):
common_args = get_common_email_args(doc) common_args = get_common_email_args(doc)
@@ -249,18 +342,20 @@ def get_confirm_workflow_action_url(doc, action, user):
def is_workflow_action_already_created(doc): def is_workflow_action_already_created(doc):
return frappe.db.exists({ return frappe.db.exists({
'doctype': 'Workflow Action', 'doctype': 'Workflow Action',
'reference_doctype': doc.get('doctype'),
'reference_name': doc.get('name'), 'reference_name': doc.get('name'),
'workflow_state': get_doc_workflow_state(doc)
'reference_doctype': doc.get('doctype'),
'workflow_state': get_doc_workflow_state(doc),
}) })


def clear_workflow_actions(doctype, name): def clear_workflow_actions(doctype, name):
if not (doctype and name): if not (doctype and name):
return return
frappe.db.delete("Workflow Action", {
"reference_doctype": doctype,
"reference_name": name
})
frappe.db.delete("Workflow Action", filters = {
"reference_name": name,
"reference_doctype": doctype,
}
)

def get_doc_workflow_state(doc): def get_doc_workflow_state(doc):
workflow_name = get_workflow_name(doc.get('doctype')) workflow_name = get_workflow_name(doc.get('doctype'))
workflow_state_field = get_workflow_state_field(workflow_name) workflow_state_field = get_workflow_state_field(workflow_name)


+ 0
- 0
frappe/workflow/doctype/workflow_action_permitted_role/__init__.py Ver fichero


+ 33
- 0
frappe/workflow/doctype/workflow_action_permitted_role/workflow_action_permitted_role.json Ver fichero

@@ -0,0 +1,33 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "hash",
"creation": "2022-02-21 20:28:05.662187",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"role"
],
"fields": [
{
"fieldname": "role",
"fieldtype": "Link",
"label": "Role",
"options": "Role"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-02-21 20:28:05.662187",
"modified_by": "Administrator",
"module": "Workflow",
"name": "Workflow Action Permitted Role",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

+ 8
- 0
frappe/workflow/doctype/workflow_action_permitted_role/workflow_action_permitted_role.py Ver fichero

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

# import frappe
from frappe.model.document import Document

class WorkflowActionPermittedRole(Document):
pass

+ 1
- 1
frappe/www/about.html Ver fichero

@@ -22,7 +22,7 @@
add_top_padding=1, add_top_padding=1,
add_bottom_padding=1, add_bottom_padding=1,
) }} ) }}


{% if doc.get({"doctype":"Company History"}) %} {% if doc.get({"doctype":"Company History"}) %}
<section class="section section-padding-bottom"> <section class="section section-padding-bottom">


+ 1
- 1
frappe/www/me.html Ver fichero

@@ -87,7 +87,7 @@
{% if item.target %}target="{{ item.target }}"{% endif %}> {% if item.target %}target="{{ item.target }}"{% endif %}>
{{ _(item.title or item.label) }} {{ _(item.title or item.label) }}
</a> </a>
{%- endfor %}
{%- endfor %}
</ul> </ul>
</div> </div>
</div> </div>

+ 1
- 1
frappe/www/me.py Ver fichero

@@ -10,6 +10,6 @@ no_cache = 1
def get_context(context): def get_context(context):
if frappe.session.user=='Guest': if frappe.session.user=='Guest':
frappe.throw(_("You need to be logged in to access this page"), frappe.PermissionError) frappe.throw(_("You need to be logged in to access this page"), frappe.PermissionError)
context.current_user = frappe.get_doc("User", frappe.session.user) context.current_user = frappe.get_doc("User", frappe.session.user)
context.show_sidebar=True context.show_sidebar=True

+ 2
- 2
frappe/www/third_party_apps.html Ver fichero

@@ -53,8 +53,8 @@
{% endfor %} {% endfor %}
{% else %} {% else %}
<div class="empty-apps-state"> <div class="empty-apps-state">
<img src="/assets/frappe/images/ui-states/empty-app-state.svg"/>
<div class="font-weight-bold mt-4">
<img src="/assets/frappe/images/ui-states/empty-app-state.svg"/>
<div class="font-weight-bold mt-4">
{{ _("No Active Sessions")}} {{ _("No Active Sessions")}}
</div> </div>
<div class="text-muted mt-2"> <div class="text-muted mt-2">


Cargando…
Cancelar
Guardar