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

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

version-14
Shariq Ansari преди 3 години
committed by GitHub
родител
ревизия
457817ea1e
No known key found for this signature in database GPG ключ ID: 4AEE18F83AFDEB23
променени са 84 файла, в които са добавени 927 реда и са изтрити 570 реда
  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 Целия файл

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

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

# Clean up whitespace
b2fc959307c7c79f5584625569d5aed04133ba13

.github/workflows/semgrep.yml → .github/workflows/linters.yml Целия файл

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

on:
pull_request: { }

jobs:
semgrep:

linters:
name: Frappe Linter
runs-on: ubuntu-latest
steps:
- 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
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules


+ 23
- 0
.pre-commit-config.yaml Целия файл

@@ -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 Целия файл

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

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


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

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

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



+ 3
- 2
frappe/core/doctype/doctype/boilerplate/test_controller._py Целия файл

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

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

class Test{classname}(unittest.TestCase):

class Test{classname}(FrappeTestCase):
pass

+ 3
- 3
frappe/core/doctype/report/test_report.py Целия файл

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



+ 7
- 1
frappe/database/query.py Целия файл

@@ -244,7 +244,13 @@ class Query:
_operator = OPERATOR_MAP[value[0]]
conditions = conditions.where(_operator(Field(key), value[1]))
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)
return conditions



+ 1
- 1
frappe/desk/doctype/dashboard_chart/dashboard_chart.js Целия файл

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

set_parent_document_type: async function(frm) {
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;

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


+ 1
- 1
frappe/desk/doctype/form_tour/form_tour.js Целия файл

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


+ 0
- 20
frappe/desk/doctype/kanban_board/kanban_board.py Целия файл

@@ -76,26 +76,6 @@ def archive_restore_column(board_name, column_title, status):
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()
def update_order(board_name, order):
'''Save the order of cards in columns'''


+ 1
- 1
frappe/desk/doctype/todo/todo_calendar.js Целия файл

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


+ 1
- 1
frappe/desk/doctype/workspace/workspace.js Целия файл

@@ -9,7 +9,7 @@ frappe.ui.form.on('Workspace', {
refresh: function(frm) {
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'))) {
frm.trigger('disable_form');
}


+ 4
- 4
frappe/desk/doctype/workspace/workspace.py Целия файл

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

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

filters = {
filters = {
'parent_page': doc.title,
'public': doc.public
'public': doc.public
}
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):
if not loads(sb_public_items) and not loads(sb_private_items):
return
sb_public_items = loads(sb_public_items)
sb_private_items = loads(sb_private_items)

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

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


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

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


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

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

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'))

def validate_condition(self):


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

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


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

@@ -221,7 +221,8 @@ scheduler_events = {
"frappe.deferred_insert.save_to_db",
"frappe.desk.form.document_follow.send_hourly_updates",
"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": [
"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.email.doctype.unhandled_email.unhandled_email.remove_old_unhandled_emails",
"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": [
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily",


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

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

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

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

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

# 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
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.owner = self.modified_by

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

def run_method(self, method, *args, **kwargs):
"""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)
out = Document.hook(fn)(self, *args, **kwargs)
@@ -1003,8 +1003,6 @@ class Document(BaseDocument):
- `on_cancel` for **Cancel**
- `update_after_submit` for **Update after Submit**"""

doc_before_save = self.get_doc_before_save()

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


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

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

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

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

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])
frappe.db.commit()
else:
msg = _("Ignored: {0} to {1}").format(row[0], row[1])
msg = None
except Exception as e:
msg = _("** Failed: {0} to {1}: {2}").format(row[0], row[1], repr(e))
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)



+ 23
- 7
frappe/modules/patch_handler.py Целия файл

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

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):
"""execute the patch"""
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()
frappe.db.begin()
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.startswith("finally:"):
# run run patch at the end
frappe.flags.final_patches.append(patchmodule)
else:
if patchmodule.startswith("execute:"):
exec(patchmodule.split("execute:")[1],globals())
if has_patch_file:
_patch()
else:
frappe.get_attr(patchmodule.split()[0] + ".execute")()
exec(patch, globals())
update_patch_log(patchmodule)

elif method:
method(**methodargs)

@@ -174,7 +190,7 @@ def execute_patch(patchmodule, method=None, methodargs=None):
frappe.db.commit()
end_time = time.time()
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



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

@@ -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.transform_todo_schema
frappe.patches.v14_0.remove_post_and_post_comment
frappe.patches.v14_0.reset_creation_datetime

[post_model_sync]
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.remove_db_aggregation
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 Целия файл

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



+ 41
- 0
frappe/patches/v14_0/reset_creation_datetime.py Целия файл

@@ -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 Целия файл

@@ -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 Целия файл

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


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

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


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

@@ -88,6 +88,9 @@ frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.for
make_quill_editor() {
if (this.quill) return;
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.bind_events();
}


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

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

// 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)) {
return false;
}


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

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

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


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

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

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



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

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


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

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



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

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

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


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

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

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

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


+ 4
- 4
frappe/public/js/frappe/ui/filters/field_select.js Целия файл

@@ -112,9 +112,9 @@ frappe.ui.FieldSelect = class FieldSelect {
// main table
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) {
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;
// show fields where user has read access and if report hide flag is not set
if (frappe.perm.has_perm(doctype, df.permlevel, "read"))
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) {
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;
// show fields where user has read access and if report hide flag is not set
if (frappe.perm.has_perm(doctype, df.permlevel, "read"))
me.add_field_option(df);


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

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

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

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
);
},
@@ -1435,7 +1435,7 @@ Object.assign(frappe.utils, {
// for link titles
frappe._link_titles = {};
}
frappe._link_titles[doctype + "::" + name] = value;
},



+ 0
- 12
frappe/public/js/frappe/views/kanban/kanban_board.js Целия файл

@@ -150,18 +150,6 @@ frappe.provide("frappe.views");
}
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) {
// cache original order
const _cards = this.cards.slice();


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

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


+ 3
- 3
frappe/public/js/frappe/views/workspace/blocks/block.js Целия файл

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

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

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) {
return;
}


+ 1
- 1
frappe/public/js/frappe/views/workspace/blocks/header.js Целия файл

@@ -107,7 +107,7 @@ export default class Header extends Block {
if (data.text !== undefined) {
let text = this._data.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>`;
}



+ 3
- 3
frappe/public/js/frappe/views/workspace/blocks/header_size.js Целия файл

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

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

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


+ 1
- 1
frappe/public/js/frappe/views/workspace/blocks/paragraph.js Целия файл

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

this.open_block_list();


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

@@ -219,7 +219,7 @@ frappe.views.Workspace = class Workspace {
$sidebar[0].firstElementChild.classList.add("selected");
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().removeClass('hidden');

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

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 => {
if (settings) {
@@ -596,9 +596,9 @@ frappe.views.Workspace = class Workspace {
}

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];
let old_item_index = from_pages.findIndex(page => page.title == old_item.title);
duplicate && old_item_index++;

@@ -859,7 +859,7 @@ frappe.views.Workspace = class Workspace {
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) {
$drop_icon.removeClass('hidden');
} else {
@@ -993,13 +993,13 @@ frappe.views.Workspace = class Workspace {
}
}
});
this.update_cached_values(new_page, new_page, true, true);
let pre_url = new_page.public ? '' : 'private/';
let route = pre_url + frappe.router.slug(new_page.title);
frappe.set_route(route);
this.make_sidebar();
this.show_sidebar_actions();
});
@@ -1010,15 +1010,15 @@ frappe.views.Workspace = class Workspace {

validate_page(new_page, old_page) {
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];

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

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


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

@@ -698,12 +698,12 @@ export default class ChartWidget extends Widget {
.get_filters_for_chart_type(this.chart_doc).then(filters => {
chart_saved_filters = this.update_default_date_filters(filters, chart_saved_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);
});
} else {
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);
return Promise.resolve();
}


+ 1
- 1
frappe/public/js/lib/jSignature.min.js Целия файл

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

// setting up new dataEngine


+ 5
- 5
frappe/public/js/lib/photoswipe/default-skin.css Целия файл

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

*/
/*
1. Buttons

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

/*
4. Caption

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

.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
*/
position: relative;
@@ -409,7 +409,7 @@ a.pswp__share--download:hover {
transform: rotate(0); } }

/*
6. Additional styles

*/


+ 81
- 81
frappe/public/js/lib/photoswipe/photoswipe-ui-default.js Целия файл

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

@@ -136,12 +136,12 @@ var PhotoSwipeUI_Default =
}
_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.
//
// 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;
_blockControlsTapTimeout = setTimeout(function() {
_blockControlsTap = false;
@@ -172,8 +172,8 @@ var PhotoSwipeUI_Default =
_toggleShareModal = function() {

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

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) );

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

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

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

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

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)
_toggleLoadingIndicator(false);
_toggleLoadingIndicator(false);
// items-controller.js function allowProgressiveImg
}
} else {
_toggleLoadingIndicator(true); // hide preloader
}

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

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

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


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

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

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



ui.init = function() {

@@ -574,9 +574,9 @@ var PhotoSwipeUI_Default =
_listen('preventDragEvent', function(e, isDown, preventObj) {
var t = e.target || e.srcElement;
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;
}
@@ -634,7 +634,7 @@ var PhotoSwipeUI_Default =
framework.addClass( _controls, 'pswp__ui--hidden');
ui.setIdle(false);
});

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

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

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

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

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

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


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

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

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

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

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


+ 263
- 263
frappe/public/js/lib/photoswipe/photoswipe.js
Файловите разлики са ограничени, защото са твърде много
Целия файл


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

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


+ 7
- 0
frappe/public/scss/common/grid.scss Целия файл

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

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

.rows .grid-row .data-row,


+ 10
- 10
frappe/public/scss/desk/desktop.scss Целия файл

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

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

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

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

.dropdown-item-icon {


+ 1
- 1
frappe/public/scss/desk/list.scss Целия файл

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


+ 1
- 1
frappe/public/scss/login.bundle.scss Целия файл

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


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

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

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


+ 1
- 1
frappe/public/scss/website/error-state.scss Целия файл

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


+ 3
- 1
frappe/public/scss/website/index.scss Целия файл

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

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

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

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

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

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

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


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

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

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

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

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


+ 12
- 6
frappe/social/doctype/energy_point_log/test_energy_point_log.py Целия файл

@@ -2,12 +2,14 @@
# Copyright (c) 2019, Frappe Technologies and Contributors
# License: MIT. See LICENSE
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 frappe.utils.testutils import add_custom_field, clear_custom_fields
from frappe.desk.form.assign_to import add as assign_to

class TestEnergyPointLog(unittest.TestCase):

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

# for criticism
criticism_points = 2
todo = create_a_todo(description='Bad patch')
energy_points_before_review = energy_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')
review_points_after_review = get_points('test2@example.com', 'review_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
}).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({
'doctype': 'ToDo',
'description': 'Fix a bug',
}).insert()
'description': description,
}).insert(ignore_permissions=True)


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


+ 1
- 1
frappe/templates/includes/feedback/feedback.html Целия файл

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

+ 5
- 1
frappe/templates/includes/footer/footer_info.html Целия файл

@@ -12,7 +12,11 @@
{# powered #}
<div class="footer-col-right col-sm-6 col-12 footer-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 %}
</div>
</div>


+ 0
- 2
frappe/templates/includes/website_theme/navbar.css Целия файл

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

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



+ 1
- 1
frappe/templates/pages/integrations/payment-success.html Целия файл

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


+ 6
- 2
frappe/tests/test_db_query.py Целия файл

@@ -97,6 +97,12 @@ class TestReportview(unittest.TestCase):
self.assertFalse(result
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):
data = DatabaseQuery("DocField").execute(
filters={"parent": "DocType"}, fields=["fieldname", "fieldtype"],
@@ -149,7 +155,6 @@ class TestReportview(unittest.TestCase):
filters={"creation": ["between", ["2016-07-06", "2016-07-07"]]},
fields=["name"])


def test_ignore_permissions_for_get_filters_cond(self):
frappe.set_user('test2@example.com')
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(frappe.get_all('Nested DocType', {'name': ('not ancestors of', 'Root')})) == len(frappe.get_all('Nested DocType')))


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


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

@@ -319,3 +319,21 @@ class TestDocument(unittest.TestCase):
self.assertIsInstance(doc, Note)
self.assertIsInstance(doc.as_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 Целия файл

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


+ 3
- 3
frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py Целия файл

@@ -7,7 +7,7 @@ import re
import frappe
from frappe import _
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.verified_command import get_signed_params, verify_request
import json
@@ -353,8 +353,8 @@ def process_data_deletion_request():

for request in requests:
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()

def remove_unverified_record():


+ 14
- 2
frappe/website/doctype/personal_data_deletion_request/test_personal_data_deletion_request.py Целия файл

@@ -4,10 +4,10 @@
import frappe
import unittest
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 (
create_user_if_not_exists,
create_user_if_not_exists
)
from datetime import datetime, timedelta

@@ -58,3 +58,15 @@ class TestPersonalDataDeletionRequest(unittest.TestCase):
self.assertFalse(
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 Целия файл

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

+ 2
- 1
frappe/website/doctype/website_settings/website_settings.py Целия файл

@@ -120,7 +120,8 @@ def get_website_settings(context=None):
"facebook_share", "google_plus_one", "twitter_share", "linked_in_share",
"disable_signup", "hide_footer_signup", "head_html", "title_prefix",
"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):
context[k] = settings.get(k)



+ 1
- 1
frappe/website/web_form/request_to_delete_data/request_to_delete_data.js Целия файл

@@ -5,7 +5,7 @@ frappe.ready(function() {
callback: (data) => {
if (data.message) {
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>`;
intro_wrapper.html(intro_wrapper.html() + sla_description_wrapper);
}


+ 50
- 1
frappe/workflow/doctype/workflow/test_workflow.py Целия файл

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


class TestWorkflow(unittest.TestCase):
@@ -15,9 +16,31 @@ class TestWorkflow(unittest.TestCase):
def setUp(self):
self.workflow = create_todo_workflow()
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):
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):
'''test default condition is set'''
@@ -75,7 +98,7 @@ class TestWorkflow(unittest.TestCase):
actions = get_common_transition_actions([todo1, todo2], 'ToDo')
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")
user = frappe.get_doc('User', 'test2@example.com')
user.add_roles('Test Approver', 'System Manager')
@@ -93,6 +116,32 @@ class TestWorkflow(unittest.TestCase):
self.assertEqual(workflow_actions[0].status, 'Completed')
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):
todo = create_new_todo()
apply_workflow(todo, 'Approve')


+ 33
- 11
frappe/workflow/doctype/workflow_action/workflow_action.json Целия файл

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

+ 131
- 36
frappe/workflow/doctype/workflow_action/workflow_action.py Целия файл

@@ -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.utils.user import get_users_with_role
from frappe.utils.data import get_link_to_form
from frappe.query_builder import DocType

class WorkflowAction(Document):
pass


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):
if not user: user = frappe.session.user

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):
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):
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

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')

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

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

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

if send_email_alert(workflow):
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'))
), 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
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
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):
transitions = frappe.get_all('Workflow Transition',
@@ -167,8 +253,10 @@ def get_next_possible_transitions(workflow_name, state, doc=None):
return transitions_to_return

def get_users_next_action_data(transitions, doc):
roles = set()
user_data_map = {}
for transition in transitions:
roles.add(transition.allowed)
users = get_users_with_role(transition.allowed)
filtered_users = filter_allowed_users(users, doc, transition)
for user in filtered_users:
@@ -182,19 +270,24 @@ def get_users_next_action_data(transitions, doc):
'action_name': transition.action,
'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):
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):
return frappe.db.exists({
'doctype': 'Workflow Action',
'reference_doctype': doc.get('doctype'),
'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):
if not (doctype and name):
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):
workflow_name = get_workflow_name(doc.get('doctype'))
workflow_state_field = get_workflow_state_field(workflow_name)


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


+ 33
- 0
frappe/workflow/doctype/workflow_action_permitted_role/workflow_action_permitted_role.json Целия файл

@@ -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 Целия файл

@@ -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 Целия файл

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

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


+ 1
- 1
frappe/www/me.html Целия файл

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

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

@@ -10,6 +10,6 @@ no_cache = 1
def get_context(context):
if frappe.session.user=='Guest':
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.show_sidebar=True

+ 2
- 2
frappe/www/third_party_apps.html Целия файл

@@ -53,8 +53,8 @@
{% endfor %}
{% else %}
<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")}}
</div>
<div class="text-muted mt-2">


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