浏览代码

Merge branch 'develop' into fix-rename-tool

version-14
gavin 3 年前
committed by GitHub
父节点
当前提交
e233b7beb7
找不到此签名对应的密钥 GPG 密钥 ID: 4AEE18F83AFDEB23
共有 90 个文件被更改,包括 987 次插入592 次删除
  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. +57
    -0
      cypress/integration/control_autocomplete.js
  6. +30
    -28
      frappe/commands/site.py
  7. +1
    -1
      frappe/core/doctype/data_import/importer.py
  8. +2
    -2
      frappe/core/doctype/docfield/docfield.json
  9. +3
    -2
      frappe/core/doctype/doctype/boilerplate/test_controller._py
  10. +5
    -4
      frappe/core/doctype/report/test_report.py
  11. +2
    -2
      frappe/custom/doctype/custom_field/custom_field.json
  12. +1
    -1
      frappe/custom/doctype/customize_form/customize_form.py
  13. +3
    -3
      frappe/custom/doctype/customize_form_field/customize_form_field.json
  14. +2
    -1
      frappe/database/mariadb/database.py
  15. +2
    -1
      frappe/database/postgres/database.py
  16. +2
    -2
      frappe/database/postgres/setup_db.py
  17. +7
    -1
      frappe/database/query.py
  18. +1
    -1
      frappe/desk/doctype/dashboard_chart/dashboard_chart.js
  19. +1
    -1
      frappe/desk/doctype/form_tour/form_tour.js
  20. +0
    -20
      frappe/desk/doctype/kanban_board/kanban_board.py
  21. +1
    -1
      frappe/desk/doctype/todo/todo_calendar.js
  22. +1
    -1
      frappe/desk/doctype/workspace/workspace.js
  23. +4
    -4
      frappe/desk/doctype/workspace/workspace.py
  24. +5
    -0
      frappe/desk/form/load.py
  25. +1
    -1
      frappe/desk/page/setup_wizard/setup_wizard.py
  26. +5
    -14
      frappe/desk/query_report.py
  27. +1
    -1
      frappe/email/doctype/notification/notification.py
  28. +1
    -1
      frappe/email/receive.py
  29. +10
    -5
      frappe/installer.py
  30. +1
    -1
      frappe/integrations/doctype/razorpay_settings/razorpay_settings.js
  31. +2
    -1
      frappe/model/__init__.py
  32. +9
    -9
      frappe/model/document.py
  33. +23
    -7
      frappe/modules/patch_handler.py
  34. +1
    -0
      frappe/patches.txt
  35. +1
    -1
      frappe/patches/v12_0/set_correct_url_in_files.py
  36. +41
    -0
      frappe/patches/v14_0/reset_creation_datetime.py
  37. +124
    -7
      frappe/public/js/frappe/form/controls/autocomplete.js
  38. +1
    -1
      frappe/public/js/frappe/form/controls/date.js
  39. +1
    -1
      frappe/public/js/frappe/form/controls/table.js
  40. +3
    -0
      frappe/public/js/frappe/form/controls/text_editor.js
  41. +3
    -0
      frappe/public/js/frappe/form/formatters.js
  42. +1
    -1
      frappe/public/js/frappe/form/grid_pagination.js
  43. +8
    -8
      frappe/public/js/frappe/form/grid_row.js
  44. +1
    -0
      frappe/public/js/frappe/form/save.js
  45. +1
    -1
      frappe/public/js/frappe/list/list_view_select.js
  46. +4
    -4
      frappe/public/js/frappe/ui/filters/field_select.js
  47. +1
    -1
      frappe/public/js/frappe/ui/link_preview.js
  48. +3
    -3
      frappe/public/js/frappe/utils/utils.js
  49. +0
    -12
      frappe/public/js/frappe/views/kanban/kanban_board.js
  50. +5
    -5
      frappe/public/js/frappe/views/reports/print_tree.html
  51. +2
    -1
      frappe/public/js/frappe/views/reports/query_report.js
  52. +3
    -3
      frappe/public/js/frappe/views/workspace/blocks/block.js
  53. +1
    -1
      frappe/public/js/frappe/views/workspace/blocks/header.js
  54. +3
    -3
      frappe/public/js/frappe/views/workspace/blocks/header_size.js
  55. +1
    -1
      frappe/public/js/frappe/views/workspace/blocks/paragraph.js
  56. +11
    -11
      frappe/public/js/frappe/views/workspace/workspace.js
  57. +2
    -2
      frappe/public/js/frappe/widgets/chart_widget.js
  58. +1
    -1
      frappe/public/js/lib/jSignature.min.js
  59. +5
    -5
      frappe/public/js/lib/photoswipe/default-skin.css
  60. +81
    -81
      frappe/public/js/lib/photoswipe/photoswipe-ui-default.js
  61. +263
    -263
      frappe/public/js/lib/photoswipe/photoswipe.js
  62. +2
    -2
      frappe/public/js/lib/prettydate.js
  63. +19
    -1
      frappe/public/scss/common/grid.scss
  64. +10
    -10
      frappe/public/scss/desk/desktop.scss
  65. +1
    -1
      frappe/public/scss/desk/list.scss
  66. +1
    -1
      frappe/public/scss/login.bundle.scss
  67. +6
    -6
      frappe/public/scss/website/blog.scss
  68. +1
    -1
      frappe/public/scss/website/error-state.scss
  69. +3
    -1
      frappe/public/scss/website/index.scss
  70. +1
    -1
      frappe/public/scss/website/navbar.scss
  71. +1
    -1
      frappe/public/scss/website/portal.scss
  72. +5
    -1
      frappe/public/scss/website/web_form.scss
  73. +2
    -1
      frappe/social/doctype/energy_point_log/energy_point_log.py
  74. +12
    -6
      frappe/social/doctype/energy_point_log/test_energy_point_log.py
  75. +1
    -1
      frappe/templates/includes/feedback/feedback.html
  76. +5
    -1
      frappe/templates/includes/footer/footer_info.html
  77. +0
    -2
      frappe/templates/includes/website_theme/navbar.css
  78. +1
    -1
      frappe/templates/pages/integrations/payment-success.html
  79. +6
    -2
      frappe/tests/test_db_query.py
  80. +18
    -0
      frappe/tests/test_document.py
  81. +5
    -4
      frappe/tests/test_permissions.py
  82. +0
    -2
      frappe/tests/test_utils.py
  83. +76
    -0
      frappe/tests/utils.py
  84. +1
    -1
      frappe/utils/make_random.py
  85. +11
    -3
      frappe/website/doctype/website_settings/website_settings.json
  86. +2
    -1
      frappe/website/doctype/website_settings/website_settings.py
  87. +1
    -1
      frappe/www/about.html
  88. +1
    -1
      frappe/www/me.html
  89. +1
    -1
      frappe/www/me.py
  90. +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" # Refactor "not a in b" -> "a not in b"
745297a49d516e5e3c4bb3e1b0c4235e7d31165d 745297a49d516e5e3c4bb3e1b0c4235e7d31165d

# Clean up whitespace
b2fc959307c7c79f5584625569d5aed04133ba13

.github/workflows/semgrep.yml → .github/workflows/linters.yml 查看文件

@@ -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 查看文件

@@ -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: coverage:
status: status:
patch: off
project: project:
default: false default: false
server: server:


+ 57
- 0
cypress/integration/control_autocomplete.js 查看文件

@@ -0,0 +1,57 @@
context('Control Autocomplete', () => {
before(() => {
cy.login();
cy.visit('/app/website');
});

function get_dialog_with_autocomplete(options) {
cy.visit('/app/website');
return cy.dialog({
title: 'Autocomplete',
fields: [
{
'label': 'Select an option',
'fieldname': 'autocomplete',
'fieldtype': 'Autocomplete',
'options': options || ['Option 1', 'Option 2', 'Option 3'],
}
]
});
}

it('should set the valid value', () => {
get_dialog_with_autocomplete().as('dialog');

cy.get('.frappe-control[data-fieldname=autocomplete] input').focus().as('input');
cy.wait(1000);
cy.get('@input').type('2', { delay: 300 });
cy.get('.frappe-control[data-fieldname=autocomplete]').findByRole('listbox').should('be.visible');
cy.get('.frappe-control[data-fieldname=autocomplete] input').type('{enter}', { delay: 300 });
cy.get('.frappe-control[data-fieldname=autocomplete] input').blur();
cy.get('@dialog').then(dialog => {
let value = dialog.get_value('autocomplete');
expect(value).to.eq('Option 2');
dialog.clear();
});
});

it('should set the valid value with different label', () => {
const options_with_label = [
{ label: "Option 1", value: "option_1" },
{ label: "Option 2", value: "option_2" }
];
get_dialog_with_autocomplete(options_with_label).as('dialog');

cy.get('.frappe-control[data-fieldname=autocomplete] input').focus().as('input');
cy.get('.frappe-control[data-fieldname=autocomplete]').findByRole('listbox').should('be.visible');
cy.get('@input').type('2', { delay: 300 });
cy.get('.frappe-control[data-fieldname=autocomplete] input').type('{enter}', { delay: 300 });
cy.get('.frappe-control[data-fieldname=autocomplete] input').blur();
cy.get('@dialog').then(dialog => {
let value = dialog.get_value('autocomplete');
expect(value).to.eq('option_2');
dialog.clear();
});
});

});

+ 30
- 28
frappe/commands/site.py 查看文件

@@ -19,36 +19,38 @@ from frappe.exceptions import SiteNotSpecifiedError
@click.option('--db-type', default='mariadb', type=click.Choice(['mariadb', 'postgres']), help='Optional "postgres" or "mariadb". Default is "mariadb"') @click.option('--db-type', default='mariadb', type=click.Choice(['mariadb', 'postgres']), help='Optional "postgres" or "mariadb". Default is "mariadb"')
@click.option('--db-host', help='Database Host') @click.option('--db-host', help='Database Host')
@click.option('--db-port', type=int, help='Database Port') @click.option('--db-port', type=int, help='Database Port')
@click.option('--mariadb-root-username', default='root', help='Root username for MariaDB')
@click.option('--mariadb-root-password', help='Root password for MariaDB')
@click.option('--db-root-username', '--mariadb-root-username', help='Root username for MariaDB or PostgreSQL, Default is "root"')
@click.option('--db-root-password', '--mariadb-root-password', help='Root password for MariaDB or PostgreSQL')
@click.option('--no-mariadb-socket', is_flag=True, default=False, help='Set MariaDB host to % and use TCP/IP Socket instead of using the UNIX Socket') @click.option('--no-mariadb-socket', is_flag=True, default=False, help='Set MariaDB host to % and use TCP/IP Socket instead of using the UNIX Socket')
@click.option('--admin-password', help='Administrator password for new site', default=None) @click.option('--admin-password', help='Administrator password for new site', default=None)
@click.option('--verbose', is_flag=True, default=False, help='Verbose') @click.option('--verbose', is_flag=True, default=False, help='Verbose')
@click.option('--force', help='Force restore if site/database already exists', is_flag=True, default=False) @click.option('--force', help='Force restore if site/database already exists', is_flag=True, default=False)
@click.option('--source_sql', help='Initiate database with a SQL file') @click.option('--source_sql', help='Initiate database with a SQL file')
@click.option('--install-app', multiple=True, help='Install app after installation') @click.option('--install-app', multiple=True, help='Install app after installation')
def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin_password=None,
verbose=False, install_apps=None, source_sql=None, force=None, no_mariadb_socket=False,
install_app=None, db_name=None, db_password=None, db_type=None, db_host=None, db_port=None):
@click.option('--set-default', is_flag=True, default=False, help='Set the new site as default site')
def new_site(site, db_root_username=None, db_root_password=None, admin_password=None,
verbose=False, install_apps=None, source_sql=None, force=None, no_mariadb_socket=False,
install_app=None, db_name=None, db_password=None, db_type=None, db_host=None, db_port=None,
set_default=False):
"Create a new site" "Create a new site"
from frappe.installer import _new_site from frappe.installer import _new_site


frappe.init(site=site, new_site=True) frappe.init(site=site, new_site=True)


_new_site(db_name, site, mariadb_root_username=mariadb_root_username,
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force,
no_mariadb_socket=no_mariadb_socket, db_password=db_password, db_type=db_type, db_host=db_host,
db_port=db_port, new_site=True)
_new_site(db_name, site, db_root_username=db_root_username,
db_root_password=db_root_password, admin_password=admin_password,
verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force,
no_mariadb_socket=no_mariadb_socket, db_password=db_password, db_type=db_type, db_host=db_host,
db_port=db_port, new_site=True)


if len(frappe.utils.get_sites()) == 1:
if set_default:
use(site) use(site)




@click.command('restore') @click.command('restore')
@click.argument('sql-file-path') @click.argument('sql-file-path')
@click.option('--mariadb-root-username', default='root', help='Root username for MariaDB')
@click.option('--mariadb-root-password', help='Root password for MariaDB')
@click.option('--db-root-username', '--mariadb-root-username', help='Root username for MariaDB or PostgreSQL, Default is "root"')
@click.option('--db-root-password', '--mariadb-root-password', help='Root password for MariaDB or PostgreSQL')
@click.option('--db-name', help='Database name for site in case it is a new one') @click.option('--db-name', help='Database name for site in case it is a new one')
@click.option('--admin-password', help='Administrator password for new site') @click.option('--admin-password', help='Administrator password for new site')
@click.option('--install-app', multiple=True, help='Install app after installation') @click.option('--install-app', multiple=True, help='Install app after installation')
@@ -57,7 +59,7 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin
@click.option('--force', is_flag=True, default=False, help='Ignore the validations and downgrade warnings. This action is not recommended') @click.option('--force', is_flag=True, default=False, help='Ignore the validations and downgrade warnings. This action is not recommended')
@click.option('--encryption-key', help='Backup encryption key') @click.option('--encryption-key', help='Backup encryption key')
@pass_context @pass_context
def restore(context, sql_file_path, encryption_key=None, mariadb_root_username=None, mariadb_root_password=None,
def restore(context, sql_file_path, encryption_key=None, db_root_username=None, db_root_password=None,
db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None,
with_private_files=None): with_private_files=None):
"Restore site database from an sql file" "Restore site database from an sql file"
@@ -150,8 +152,8 @@ def restore(context, sql_file_path, encryption_key=None, mariadb_root_username=N




try: try:
_new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username,
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
_new_site(frappe.conf.db_name, site, db_root_username=db_root_username,
db_root_password=db_root_password, admin_password=admin_password,
verbose=context.verbose, install_apps=install_app, source_sql=decompressed_file_name, verbose=context.verbose, install_apps=install_app, source_sql=decompressed_file_name,
force=True, db_type=frappe.conf.db_type) force=True, db_type=frappe.conf.db_type)


@@ -290,16 +292,16 @@ def partial_restore(context, sql_file_path, verbose, encryption_key=None):


@click.command('reinstall') @click.command('reinstall')
@click.option('--admin-password', help='Administrator Password for reinstalled site') @click.option('--admin-password', help='Administrator Password for reinstalled site')
@click.option('--mariadb-root-username', help='Root username for MariaDB')
@click.option('--mariadb-root-password', help='Root password for MariaDB')
@click.option('--db-root-username', '--mariadb-root-username', help='Root username for MariaDB or PostgreSQL, Default is "root"')
@click.option('--db-root-password', '--mariadb-root-password', help='Root password for MariaDB or PostgreSQL')
@click.option('--yes', is_flag=True, default=False, help='Pass --yes to skip confirmation') @click.option('--yes', is_flag=True, default=False, help='Pass --yes to skip confirmation')
@pass_context @pass_context
def reinstall(context, admin_password=None, mariadb_root_username=None, mariadb_root_password=None, yes=False):
def reinstall(context, admin_password=None, db_root_username=None, db_root_password=None, yes=False):
"Reinstall site ie. wipe all data and start over" "Reinstall site ie. wipe all data and start over"
site = get_site(context) site = get_site(context)
_reinstall(site, admin_password, mariadb_root_username, mariadb_root_password, yes, verbose=context.verbose)
_reinstall(site, admin_password, db_root_username, db_root_password, yes, verbose=context.verbose)


def _reinstall(site, admin_password=None, mariadb_root_username=None, mariadb_root_password=None, yes=False, verbose=False):
def _reinstall(site, admin_password=None, db_root_username=None, db_root_password=None, yes=False, verbose=False):
from frappe.installer import _new_site from frappe.installer import _new_site


if not yes: if not yes:
@@ -319,7 +321,7 @@ def _reinstall(site, admin_password=None, mariadb_root_username=None, mariadb_ro


frappe.init(site=site) frappe.init(site=site)
_new_site(frappe.conf.db_name, site, verbose=verbose, force=True, reinstall=True, install_apps=installed, _new_site(frappe.conf.db_name, site, verbose=verbose, force=True, reinstall=True, install_apps=installed,
mariadb_root_username=mariadb_root_username, mariadb_root_password=mariadb_root_password,
db_root_username=db_root_username, db_root_password=db_root_password,
admin_password=admin_password) admin_password=admin_password)


@click.command('install-app') @click.command('install-app')
@@ -656,16 +658,16 @@ def uninstall(context, app, dry_run, yes, no_backup, force):


@click.command('drop-site') @click.command('drop-site')
@click.argument('site') @click.argument('site')
@click.option('--root-login', default='root')
@click.option('--root-password')
@click.option('--db-root-username', '--mariadb-root-username', '--root-login', help='Root username for MariaDB or PostgreSQL, Default is "root"')
@click.option('--db-root-password', '--mariadb-root-password', '--root-password', help='Root password for MariaDB or PostgreSQL')
@click.option('--archived-sites-path') @click.option('--archived-sites-path')
@click.option('--no-backup', is_flag=True, default=False) @click.option('--no-backup', is_flag=True, default=False)
@click.option('--force', help='Force drop-site even if an error is encountered', is_flag=True, default=False) @click.option('--force', help='Force drop-site even if an error is encountered', is_flag=True, default=False)
def drop_site(site, root_login='root', root_password=None, archived_sites_path=None, force=False, no_backup=False):
_drop_site(site, root_login, root_password, archived_sites_path, force, no_backup)
def drop_site(site, db_root_username='root', db_root_password=None, archived_sites_path=None, force=False, no_backup=False):
_drop_site(site, db_root_username, db_root_password, archived_sites_path, force, no_backup)




def _drop_site(site, root_login='root', root_password=None, archived_sites_path=None, force=False, no_backup=False):
def _drop_site(site, db_root_username=None, db_root_password=None, archived_sites_path=None, force=False, no_backup=False):
"Remove site from database and filesystem" "Remove site from database and filesystem"
from frappe.database import drop_user_and_database from frappe.database import drop_user_and_database
from frappe.utils.backups import scheduled_backup from frappe.utils.backups import scheduled_backup
@@ -690,7 +692,7 @@ def _drop_site(site, root_login='root', root_password=None, archived_sites_path=
click.echo("\n".join(messages)) click.echo("\n".join(messages))
sys.exit(1) sys.exit(1)


drop_user_and_database(frappe.conf.db_name, root_login, root_password)
drop_user_and_database(frappe.conf.db_name, db_root_username, db_root_password)


archived_sites_path = archived_sites_path or os.path.join(frappe.get_app_path('frappe'), '..', '..', '..', 'archived', 'sites') archived_sites_path = archived_sites_path or os.path.join(frappe.get_app_path('frappe'), '..', '..', '..', 'archived', 'sites')




+ 1
- 1
frappe/core/doctype/data_import/importer.py 查看文件

@@ -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()




+ 2
- 2
frappe/core/doctype/docfield/docfield.json 查看文件

@@ -99,7 +99,7 @@
"label": "Type", "label": "Type",
"oldfieldname": "fieldtype", "oldfieldname": "fieldtype",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
"reqd": 1, "reqd": 1,
"search_index": 1 "search_index": 1
}, },
@@ -547,7 +547,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-01-27 21:22:20.529072",
"modified": "2022-02-14 11:56:19.812863",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "DocField", "name": "DocField",


+ 3
- 2
frappe/core/doctype/doctype/boilerplate/test_controller._py 查看文件

@@ -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

+ 5
- 4
frappe/core/doctype/report/test_report.py 查看文件

@@ -314,23 +314,24 @@ 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"
} }
] ]


result = add_total_row(result, columns, meta=None, report_settings=report_settings)
result = add_total_row(result, columns, meta=None, is_tree=report_settings['tree'],
parent_field=report_settings['parent_field'])
self.assertEqual(result[-1][0], "Total") self.assertEqual(result[-1][0], "Total")
self.assertEqual(result[-1][1], 200) self.assertEqual(result[-1][1], 200)
self.assertEqual(result[-1][2], 150.50) self.assertEqual(result[-1][2], 150.50)

+ 2
- 2
frappe/custom/doctype/custom_field/custom_field.json 查看文件

@@ -122,7 +122,7 @@
"label": "Field Type", "label": "Field Type",
"oldfieldname": "fieldtype", "oldfieldname": "fieldtype",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break",
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break",
"reqd": 1 "reqd": 1
}, },
{ {
@@ -431,7 +431,7 @@
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2022-01-27 21:47:01.065556",
"modified": "2022-02-14 15:42:21.885999",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Custom", "module": "Custom",
"name": "Custom Field", "name": "Custom Field",


+ 1
- 1
frappe/custom/doctype/customize_form/customize_form.py 查看文件

@@ -600,4 +600,4 @@ ALLOWED_FIELDTYPE_CHANGE = (
('Code', 'Geolocation'), ('Code', 'Geolocation'),
('Table', 'Table MultiSelect')) ('Table', 'Table MultiSelect'))


ALLOWED_OPTIONS_CHANGE = ('Read Only', 'HTML', 'Select', 'Data')
ALLOWED_OPTIONS_CHANGE = ('Read Only', 'HTML', 'Data')

+ 3
- 3
frappe/custom/doctype/customize_form_field/customize_form_field.json 查看文件

@@ -85,7 +85,7 @@
"label": "Type", "label": "Type",
"oldfieldname": "fieldtype", "oldfieldname": "fieldtype",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
"reqd": 1, "reqd": 1,
"search_index": 1 "search_index": 1
}, },
@@ -450,7 +450,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-02-08 19:38:16.111199",
"modified": "2022-02-25 16:01:12.616736",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Custom", "module": "Custom",
"name": "Customize Form Field", "name": "Customize Form Field",
@@ -460,4 +460,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"states": [] "states": []
}
}

+ 2
- 1
frappe/database/mariadb/database.py 查看文件

@@ -52,7 +52,8 @@ class MariaDBDatabase(Database):
'Barcode': ('longtext', ''), 'Barcode': ('longtext', ''),
'Geolocation': ('longtext', ''), 'Geolocation': ('longtext', ''),
'Duration': ('decimal', '21,9'), 'Duration': ('decimal', '21,9'),
'Icon': ('varchar', self.VARCHAR_LEN)
'Icon': ('varchar', self.VARCHAR_LEN),
'Autocomplete': ('varchar', self.VARCHAR_LEN),
} }


def get_connection(self): def get_connection(self):


+ 2
- 1
frappe/database/postgres/database.py 查看文件

@@ -62,7 +62,8 @@ class PostgresDatabase(Database):
'Barcode': ('text', ''), 'Barcode': ('text', ''),
'Geolocation': ('text', ''), 'Geolocation': ('text', ''),
'Duration': ('decimal', '21,9'), 'Duration': ('decimal', '21,9'),
'Icon': ('varchar', self.VARCHAR_LEN)
'Icon': ('varchar', self.VARCHAR_LEN),
'Autocomplete': ('varchar', self.VARCHAR_LEN),
} }


def get_connection(self): def get_connection(self):


+ 2
- 2
frappe/database/postgres/setup_db.py 查看文件

@@ -4,7 +4,7 @@ import frappe




def setup_database(force, source_sql=None, verbose=False): def setup_database(force, source_sql=None, verbose=False):
root_conn = get_root_connection()
root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password)
root_conn.commit() root_conn.commit()
root_conn.sql("DROP DATABASE IF EXISTS `{0}`".format(frappe.conf.db_name)) root_conn.sql("DROP DATABASE IF EXISTS `{0}`".format(frappe.conf.db_name))
root_conn.sql("DROP USER IF EXISTS {0}".format(frappe.conf.db_name)) root_conn.sql("DROP USER IF EXISTS {0}".format(frappe.conf.db_name))
@@ -70,7 +70,7 @@ def import_db_from_sql(source_sql=None, verbose=False):
print(f"\nSTDOUT by psql:\n{restore_proc.stdout.decode()}\nImported from Database File: {source_sql}") print(f"\nSTDOUT by psql:\n{restore_proc.stdout.decode()}\nImported from Database File: {source_sql}")


def setup_help_database(help_db_name): def setup_help_database(help_db_name):
root_conn = get_root_connection()
root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password)
root_conn.sql("DROP DATABASE IF EXISTS `{0}`".format(help_db_name)) root_conn.sql("DROP DATABASE IF EXISTS `{0}`".format(help_db_name))
root_conn.sql("DROP USER IF EXISTS {0}".format(help_db_name)) root_conn.sql("DROP USER IF EXISTS {0}".format(help_db_name))
root_conn.sql("CREATE DATABASE `{0}`".format(help_db_name)) root_conn.sql("CREATE DATABASE `{0}`".format(help_db_name))


+ 7
- 1
frappe/database/query.py 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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,


+ 5
- 0
frappe/desk/form/load.py 查看文件

@@ -11,8 +11,10 @@ from frappe.model.utils.user_settings import get_user_settings
from frappe.permissions import get_doc_permissions from frappe.permissions import get_doc_permissions
from frappe.desk.form.document_follow import is_document_followed from frappe.desk.form.document_follow import is_document_followed
from frappe import _ from frappe import _
from frappe import _dict
from urllib.parse import quote from urllib.parse import quote



@frappe.whitelist() @frappe.whitelist()
def getdoc(doctype, name, user=None): def getdoc(doctype, name, user=None):
""" """
@@ -50,8 +52,11 @@ def getdoc(doctype, name, user=None):


doc.add_seen() doc.add_seen()
set_link_titles(doc) set_link_titles(doc)
if frappe.response.docs is None:
frappe.response = _dict({"docs": []})
frappe.response.docs.append(doc) frappe.response.docs.append(doc)



@frappe.whitelist() @frappe.whitelist()
def getdoctype(doctype, with_parent=False, cached_timestamp=None): def getdoctype(doctype, with_parent=False, cached_timestamp=None):
"""load doctype""" """load doctype"""


+ 1
- 1
frappe/desk/page/setup_wizard/setup_wizard.py 查看文件

@@ -392,7 +392,7 @@ def make_records(records, debug=False):
doc.flags.ignore_mandatory = True doc.flags.ignore_mandatory = True


try: try:
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
doc.insert(ignore_permissions=True)
frappe.db.commit() frappe.db.commit()


except frappe.DuplicateEntryError as e: except frappe.DuplicateEntryError as e:


+ 5
- 14
frappe/desk/query_report.py 查看文件

@@ -73,7 +73,7 @@ def get_report_result(report, filters):
return res return res


@frappe.read_only() @frappe.read_only()
def generate_report_result(report, filters=None, user=None, custom_columns=None, report_settings=None):
def generate_report_result(report, filters=None, user=None, custom_columns=None, is_tree=False, parent_field=None):
user = user or frappe.session.user user = user or frappe.session.user
filters = filters or [] filters = filters or []


@@ -108,7 +108,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None,
result = get_filtered_data(report.ref_doctype, columns, result, user) result = get_filtered_data(report.ref_doctype, columns, result, user)


if cint(report.add_total_row) and result and not skip_total_row: if cint(report.add_total_row) and result and not skip_total_row:
result = add_total_row(result, columns, report_settings=report_settings)
result = add_total_row(result, columns, is_tree=is_tree, parent_field=parent_field)


return { return {
"result": result, "result": result,
@@ -210,7 +210,7 @@ def get_script(report_name):


@frappe.whitelist() @frappe.whitelist()
@frappe.read_only() @frappe.read_only()
def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None, report_settings=None):
def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None, is_tree=False, parent_field=None):
report = get_report_doc(report_name) report = get_report_doc(report_name)
if not user: if not user:
user = frappe.session.user user = frappe.session.user
@@ -238,7 +238,7 @@ def run(report_name, filters=None, user=None, ignore_prepared_report=False, cust
dn = "" dn = ""
result = get_prepared_report_result(report, filters, dn, user) result = get_prepared_report_result(report, filters, dn, user)
else: else:
result = generate_report_result(report, filters, user, custom_columns, report_settings)
result = generate_report_result(report, filters, user, custom_columns, is_tree, parent_field)


result["add_total_row"] = report.add_total_row and not result.get( result["add_total_row"] = report.add_total_row and not result.get(
"skip_total_row", False "skip_total_row", False
@@ -435,18 +435,9 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visi
return result, column_widths return result, column_widths




def add_total_row(result, columns, meta=None, report_settings=None):
def add_total_row(result, columns, meta=None, is_tree=False, parent_field=None):
total_row = [""] * len(columns) total_row = [""] * len(columns)
has_percent = [] has_percent = []
is_tree = False
parent_field = ''

if report_settings:
if isinstance(report_settings, (str,)):
report_settings = json.loads(report_settings)

is_tree = report_settings.get('tree')
parent_field = report_settings.get('parent_field')


for i, col in enumerate(columns): for i, col in enumerate(columns):
fieldtype, options, fieldname = None, None, None fieldtype, options, fieldname = None, None, None


+ 1
- 1
frappe/email/doctype/notification/notification.py 查看文件

@@ -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 查看文件

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


+ 10
- 5
frappe/installer.py 查看文件

@@ -14,8 +14,8 @@ from frappe.defaults import _clear_cache
def _new_site( def _new_site(
db_name, db_name,
site, site,
mariadb_root_username=None,
mariadb_root_password=None,
db_root_username=None,
db_root_password=None,
admin_password=None, admin_password=None,
verbose=False, verbose=False,
install_apps=None, install_apps=None,
@@ -60,8 +60,8 @@ def _new_site(
installing = touch_file(get_site_path("locks", "installing.lock")) installing = touch_file(get_site_path("locks", "installing.lock"))


install_db( install_db(
root_login=mariadb_root_username,
root_password=mariadb_root_password,
root_login=db_root_username,
root_password=db_root_password,
db_name=db_name, db_name=db_name,
admin_password=admin_password, admin_password=admin_password,
verbose=verbose, verbose=verbose,
@@ -92,7 +92,7 @@ def _new_site(
print("*** Scheduler is", scheduler_status, "***") print("*** Scheduler is", scheduler_status, "***")




def install_db(root_login="root", root_password=None, db_name=None, source_sql=None,
def install_db(root_login=None, root_password=None, db_name=None, source_sql=None,
admin_password=None, verbose=True, force=0, site_config=None, reinstall=False, admin_password=None, verbose=True, force=0, site_config=None, reinstall=False,
db_password=None, db_type=None, db_host=None, db_port=None, no_mariadb_socket=False): db_password=None, db_type=None, db_host=None, db_port=None, no_mariadb_socket=False):
import frappe.database import frappe.database
@@ -101,6 +101,11 @@ def install_db(root_login="root", root_password=None, db_name=None, source_sql=N
if not db_type: if not db_type:
db_type = frappe.conf.db_type or 'mariadb' db_type = frappe.conf.db_type or 'mariadb'


if not root_login and db_type == 'mariadb':
root_login='root'
elif not root_login and db_type == 'postgres':
root_login='postgres'

make_conf(db_name, site_config=site_config, db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port) make_conf(db_name, site_config=site_config, db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port)
frappe.flags.in_install_db = True frappe.flags.in_install_db = True




+ 1
- 1
frappe/integrations/doctype/razorpay_settings/razorpay_settings.js 查看文件

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


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

+ 2
- 1
frappe/model/__init__.py 查看文件

@@ -35,7 +35,8 @@ data_fieldtypes = (
'Barcode', 'Barcode',
'Geolocation', 'Geolocation',
'Duration', 'Duration',
'Icon'
'Icon',
'Autocomplete',
) )


attachment_fieldtypes = ( attachment_fieldtypes = (


+ 9
- 9
frappe/model/document.py 查看文件

@@ -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)
@@ -1154,7 +1154,7 @@ class Document(BaseDocument):
for f in hooks: for f in hooks:
add_to_return_value(self, f(self, method, *args, **kwargs)) add_to_return_value(self, f(self, method, *args, **kwargs))


return self._return_value
return self.__dict__.pop("_return_value", None)


return runner return runner




+ 23
- 7
frappe/modules/patch_handler.py 查看文件

@@ -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




+ 1
- 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.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


+ 1
- 1
frappe/patches/v12_0/set_correct_url_in_files.py 查看文件

@@ -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 查看文件

@@ -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()

+ 124
- 7
frappe/public/js/frappe/form/controls/autocomplete.js 查看文件

@@ -11,7 +11,26 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
set_options() { set_options() {
if (this.df.options) { if (this.df.options) {
let options = this.df.options || []; let options = this.df.options || [];
this._data = this.parse_options(options);
this.set_data(options);
}
}

format_for_input(value) {
if (value == null) {
return "";
} else if (this._data && this._data.length) {
const item = this._data.find(i => i.value == value);
return item ? item.label : value;
} else {
return value;
}
}

get_input_value() {
if (this.$input) {
const label = this.$input.val();
const item = this._data?.find(i => i.label == label);
return item ? item.value : label;
} }
} }


@@ -23,7 +42,7 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
autoFirst: true, autoFirst: true,
list: this.get_data(), list: this.get_data(),
data: function(item) { data: function(item) {
if (!(item instanceof Object)) {
if (typeof item !== 'object') {
var d = { value: item }; var d = { value: item };
item = d; item = d;
} }
@@ -65,6 +84,18 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
}; };
} }


init_option_cache() {
if (!this.$input.cache) {
this.$input.cache = {};
}
if (!this.$input.cache[this.doctype]) {
this.$input.cache[this.doctype] = {};
}
if (!this.$input.cache[this.doctype][this.df.fieldname]) {
this.$input.cache[this.doctype][this.df.fieldname] = {};
}
}

setup_awesomplete() { setup_awesomplete() {
this.awesomplete = new Awesomplete( this.awesomplete = new Awesomplete(
this.input, this.input,
@@ -75,12 +106,18 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
.find('.awesomplete ul') .find('.awesomplete ul')
.css('min-width', '100%'); .css('min-width', '100%');


this.$input.on(
'input',
frappe.utils.debounce(() => {
this.init_option_cache();

this.$input.on('input', frappe.utils.debounce((e) => {
const cached_options = this.$input.cache[this.doctype][this.df.fieldname][e.target.value];
if (cached_options && cached_options.length) {
this.set_data(cached_options);
} else if (this.get_query || this.df.get_query) {
this.execute_query_if_exists(e.target.value);
} else {
this.awesomplete.list = this.get_data(); this.awesomplete.list = this.get_data();
}, 500)
);
}
}, 500));


this.$input.on('focus', () => { this.$input.on('focus', () => {
if (!this.$input.val()) { if (!this.$input.val()) {
@@ -89,6 +126,17 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
} }
}); });


this.$input.on("blur", () => {
if(this.selected) {
this.selected = false;
return;
}
var value = this.get_input_value();
if(value!==this.last_value) {
this.parse_validate_and_set_in_model(value);
}
});

this.$input.on("awesomplete-open", () => { this.$input.on("awesomplete-open", () => {
this.autocomplete_open = true; this.autocomplete_open = true;
}); });
@@ -127,6 +175,75 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
return options; return options;
} }


execute_query_if_exists(term) {
const args = { txt: term };
let get_query = this.get_query || this.df.get_query;

if (!get_query) {
return;
}

let set_nulls = function(obj) {
$.each(obj, function(key, value) {
if (value !== undefined) {
obj[key] = value;
}
});
return obj;
};

let process_query_object = function(obj) {
if (obj.query) {
args.query = obj.query;
}

if (obj.params) {
set_nulls(obj.params);
Object.assign(args, obj.params);
}

// turn off value translation
if (obj.translate_values !== undefined) {
this.translate_values = obj.translate_values;
}
};

if ($.isPlainObject(get_query)) {
process_query_object(get_query);
} else if (typeof get_query === "string") {
args.query = get_query;
} else {
// get_query by function
var q = get_query(
(this.frm && this.frm.doc) || this.doc,
this.doctype,
this.docname
);

if (typeof q === "string") {
// returns a string
args.query = q;
} else if ($.isPlainObject(q)) {
// returns an object
process_query_object(q);
}
}

if (args.query) {
frappe.call({
method: args.query,
args: args,
callback: ({ message }) => {
if(!this.$input.is(":focus")) {
return;
}
this.$input.cache[this.doctype][this.df.fieldname][term] = message;
this.set_data(message);
}
})
}
}

get_data() { get_data() {
return this._data || []; return this._data || [];
} }


+ 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() { 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 查看文件

@@ -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 查看文件

@@ -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();
} }


+ 3
- 0
frappe/public/js/frappe/form/formatters.js 查看文件

@@ -21,6 +21,9 @@ frappe.form.formatters = {
} }
return value==null ? "" : value; return value==null ? "" : value;
}, },
Autocomplete: function(value) {
return __(frappe.form.formatters["Data"](value));
},
Select: function(value) { Select: function(value) {
return __(frappe.form.formatters["Data"](value)); return __(frappe.form.formatters["Data"](value));
}, },


+ 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 // 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;
} }


+ 8
- 8
frappe/public/js/frappe/form/grid_row.js 查看文件

@@ -183,21 +183,20 @@ export default class GridRow {
render_template() { render_template() {
this.set_row_index(); this.set_row_index();


if(this.row_display) {
if (this.row_display) {
this.row_display.remove(); this.row_display.remove();
} }


// row index // row index
if(this.doc) {
if(!this.row_index) {
this.row_index = $('<div style="float: left; margin-left: 15px; margin-top: 8px; \
margin-right: -20px;">'+this.row_check_html+' <span></span></div>').appendTo(this.row);
}
if (!this.row_index) {
this.row_index = $(`<div class="template-row-index">${this.row_check_html}<span></span></div>`).appendTo(this.row);
}
if (this.doc) {
this.row_index.find('span').html(this.doc.idx); this.row_index.find('span').html(this.doc.idx);
} }


this.row_display = $('<div class="row-data sortable-handle template-row">'+
+'</div>').appendTo(this.row)
this.row_display = $('<div class="row-data sortable-handle template-row"></div>').appendTo(this.row)
.html(frappe.render(this.grid.template, { .html(frappe.render(this.grid.template, {
doc: this.doc ? frappe.get_format_helper(this.doc) : null, doc: this.doc ? frappe.get_format_helper(this.doc) : null,
frm: this.frm, frm: this.frm,
@@ -616,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 查看文件

@@ -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];
} }




+ 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"); 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 = ``;




+ 4
- 4
frappe/public/js/frappe/ui/filters/field_select.js 查看文件

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


+ 1
- 1
frappe/public/js/frappe/ui/link_preview.js 查看文件

@@ -73,7 +73,7 @@ frappe.ui.LinkPreview = class {
} }


this.popover_timeout = setTimeout(() => { this.popover_timeout = setTimeout(() => {
if (this.popover) {
if (this.popover && this.popover.options) {
let new_content = this.get_popover_html(preview_data); let new_content = this.get_popover_html(preview_data);
this.popover.options.content = new_content; this.popover.options.content = new_content;
} else { } else {


+ 3
- 3
frappe/public/js/frappe/utils/utils.js 查看文件

@@ -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 查看文件

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


+ 5
- 5
frappe/public/js/frappe/views/reports/print_tree.html 查看文件

@@ -10,14 +10,14 @@
<link href="{{ base_url }}/assets/frappe/css/bootstrap.css" rel="stylesheet"> <link href="{{ base_url }}/assets/frappe/css/bootstrap.css" rel="stylesheet">
<link type="text/css" rel="stylesheet" <link type="text/css" rel="stylesheet"
href="{{ base_url }}/assets/frappe/css/font-awesome.css"> href="{{ base_url }}/assets/frappe/css/font-awesome.css">
<link rel="stylesheet" type="text/css" href="{{ base_url }}/assets/frappe/css/tree.css">
<link rel="stylesheet" type="text/css" href="{{ base_url }}/assets/frappe/css/tree.css">
<style> <style>
{{ print_css }} {{ print_css }}
</style> </style>
<style> <style>
.tree.opened::before,
.tree-node.opened::before,
.tree:last-child::after,
.tree.opened::before,
.tree-node.opened::before,
.tree:last-child::after,
.tree-node:last-child::after { .tree-node:last-child::after {
z-index: 1; z-index: 1;
border-left: 1px solid #d1d8dd; border-left: 1px solid #d1d8dd;
@@ -28,7 +28,7 @@
text-decoration: none; text-decoration: none;
cursor: default; cursor: default;
} }
.tree.opened > .tree-children > .tree-node > .tree-link::before,
.tree.opened > .tree-children > .tree-node > .tree-link::before,
.tree-node.opened > .tree-children > .tree-node > .tree-link::before { .tree-node.opened > .tree-children > .tree-node > .tree-link::before {
border-top: 1px solid #d1d8dd; border-top: 1px solid #d1d8dd;
z-index: 1; z-index: 1;


+ 2
- 1
frappe/public/js/frappe/views/reports/query_report.js 查看文件

@@ -578,7 +578,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
args: { args: {
report_name: this.report_name, report_name: this.report_name,
filters: filters, filters: filters,
report_settings: this.report_settings
is_tree: this.report_settings.tree,
parent_field: this.report_settings.parent_field
}, },
callback: resolve, callback: resolve,
always: () => this.page.btn_secondary.prop('disabled', false) always: () => this.page.btn_secondary.prop('disabled', false)


+ 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('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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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
文件差异内容过多而无法显示
查看文件


+ 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]*/, "")), 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" ||


+ 19
- 1
frappe/public/scss/common/grid.scss 查看文件

@@ -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,
@@ -54,7 +61,7 @@
} }


.form-grid .grid-heading-row .template-row { .form-grid .grid-heading-row .template-row {
margin-left: 20px;
margin-left: 8px;
} }


.form-grid .template-row { .form-grid .template-row {
@@ -88,6 +95,17 @@
margin-top: 2px; margin-top: 2px;
} }


.template-row-index {
float: left;
margin-left: 15px;
margin-top: 8px;
margin-right: -20px;

span {
margin-left: 5px;
}
}

.editable-form .grid-static-col.bold { .editable-form .grid-static-col.bold {
font-weight: bold; font-weight: bold;
} }


+ 10
- 10
frappe/public/scss/desk/desktop.scss 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

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

+ 5
- 1
frappe/public/scss/website/web_form.scss 查看文件

@@ -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;
@@ -50,6 +50,10 @@
&:last-child { &:last-child {
padding-right: 0; padding-right: 0;
} }

@include media-breakpoint-down(sm) {
padding: 0;
}
} }
} }




+ 2
- 1
frappe/social/doctype/energy_point_log/energy_point_log.py 查看文件

@@ -164,6 +164,7 @@ def get_alert_dict(doc):


return alert_dict return alert_dict



def create_energy_points_log(ref_doctype, ref_name, doc, apply_only_once=False): def create_energy_points_log(ref_doctype, ref_name, doc, apply_only_once=False):
doc = frappe._dict(doc) doc = frappe._dict(doc)


@@ -171,7 +172,7 @@ def create_energy_points_log(ref_doctype, ref_name, doc, apply_only_once=False):
ref_name, doc.rule, None if apply_only_once else doc.user) ref_name, doc.rule, None if apply_only_once else doc.user)


if log_exists: if log_exists:
return
return frappe.get_doc('Energy Point Log', log_exists)


new_log = frappe.new_doc('Energy Point Log') new_log = frappe.new_doc('Energy Point Log')
new_log.reference_doctype = ref_doctype new_log.reference_doctype = ref_doctype


+ 12
- 6
frappe/social/doctype/energy_point_log/test_energy_point_log.py 查看文件

@@ -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 查看文件

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

+ 5
- 1
frappe/templates/includes/footer/footer_info.html 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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")

+ 5
- 4
frappe/tests/test_permissions.py 查看文件

@@ -4,7 +4,6 @@


import frappe import frappe
import frappe.defaults import frappe.defaults
import unittest
import frappe.model.meta import frappe.model.meta
from frappe.permissions import (add_user_permission, remove_user_permission, from frappe.permissions import (add_user_permission, remove_user_permission,
clear_user_permissions_for_doctype, get_doc_permissions, add_permission, update_permission_property) clear_user_permissions_for_doctype, get_doc_permissions, add_permission, update_permission_property)
@@ -14,9 +13,12 @@ from frappe.core.doctype.user_permission.user_permission import clear_user_permi
from frappe.desk.form.load import getdoc from frappe.desk.form.load import getdoc
from frappe.utils.data import now_datetime from frappe.utils.data import now_datetime


from frappe.tests.utils import FrappeTestCase

test_dependencies = ['Blogger', 'Blog Post', "User", "Contact", "Salutation"] test_dependencies = ['Blogger', 'Blog Post', "User", "Contact", "Salutation"]


class TestPermissions(unittest.TestCase):

class TestPermissions(FrappeTestCase):
def setUp(self): def setUp(self):
frappe.clear_cache(doctype="Blog Post") frappe.clear_cache(doctype="Blog Post")


@@ -221,7 +223,7 @@ class TestPermissions(unittest.TestCase):


# check that Document.owner cannot be changed # check that Document.owner cannot be changed
user.reload() user.reload()
user.owner = frappe.db.get_value("User", {"name": ("!=", user.name)})
user.owner = "Guest"
self.assertRaises(frappe.CannotChangeConstantError, user.save) self.assertRaises(frappe.CannotChangeConstantError, user.save)


def test_set_only_once(self): def test_set_only_once(self):
@@ -557,7 +559,6 @@ class TestPermissions(unittest.TestCase):
# Remove delete perm # Remove delete perm
update('Blog Post', 'Website Manager', 0, 'delete', 0) update('Blog Post', 'Website Manager', 0, 'delete', 0)



frappe.clear_cache(doctype="Blog Post") frappe.clear_cache(doctype="Blog Post")


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


+ 0
- 2
frappe/tests/test_utils.py 查看文件

@@ -510,5 +510,3 @@ class TestLinkTitle(unittest.TestCase):
todo.delete() todo.delete()
user.delete() user.delete()
prop_setter.delete() prop_setter.delete()



+ 76
- 0
frappe/tests/utils.py 查看文件

@@ -0,0 +1,76 @@
import copy
import signal
import unittest
from contextlib import contextmanager

import frappe


class FrappeTestCase(unittest.TestCase):
"""Base test class for Frappe tests."""
@classmethod
def setUpClass(cls) -> None:
frappe.db.commit()
return super().setUpClass()

@classmethod
def tearDownClass(cls) -> None:
frappe.db.rollback()
return super().tearDownClass()


@contextmanager
def change_settings(doctype, settings_dict):
""" A context manager to ensure that settings are changed before running
function and restored after running it regardless of exceptions occured.
This is useful in tests where you want to make changes in a function but
don't retain those changes.
import and use as decorator to cover full function or using `with` statement.

example:
@change_settings("Print Settings", {"send_print_as_pdf": 1})
def test_case(self):
...
"""

try:
settings = frappe.get_doc(doctype)
# remember setting
previous_settings = copy.deepcopy(settings_dict)
for key in previous_settings:
previous_settings[key] = getattr(settings, key)

# change setting
for key, value in settings_dict.items():
setattr(settings, key, value)
settings.save()
# singles are cached by default, clear to avoid flake
frappe.db.value_cache[settings] = {}
yield # yield control to calling function

finally:
# restore settings
settings = frappe.get_doc(doctype)
for key, value in previous_settings.items():
setattr(settings, key, value)
settings.save()


def timeout(seconds=30, error_message="Test timed out."):
""" Timeout decorator to ensure a test doesn't run for too long.

adapted from https://stackoverflow.com/a/2282656"""
def decorator(func):
def _handle_timeout(signum, frame):
raise Exception(error_message)

def wrapper(*args, **kwargs):
signal.signal(signal.SIGALRM, _handle_timeout)
signal.alarm(seconds)
try:
result = func(*args, **kwargs)
finally:
signal.alarm(0)
return result
return wrapper
return decorator

+ 1
- 1
frappe/utils/make_random.py 查看文件

@@ -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),


+ 11
- 3
frappe/website/doctype/website_settings/website_settings.json 查看文件

@@ -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"
@@ -406,6 +408,11 @@
"fieldname": "auto_account_deletion", "fieldname": "auto_account_deletion",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Auto Account Deletion within (Days)" "label": "Auto Account Deletion within (Days)"
},
{
"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-28 23:05:42.493192",
"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 查看文件

@@ -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/www/about.html 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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 查看文件

@@ -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">


正在加载...
取消
保存