Merge remote-tracking branch 'upstream/develop' into fix-note-2
This commit is contained in:
commit
df2b1ff456
136 changed files with 1697 additions and 1019 deletions
|
|
@ -13,3 +13,9 @@ charset = utf-8
|
|||
indent_style = tab
|
||||
indent_size = 4
|
||||
max_line_length = 99
|
||||
|
||||
# JSON files - mostly doctype schema files
|
||||
[{*.json}]
|
||||
insert_final_newline = false
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
|
|
|||
1
.flake8
1
.flake8
|
|
@ -69,6 +69,7 @@ ignore =
|
|||
F841,
|
||||
E713,
|
||||
E712,
|
||||
B028,
|
||||
|
||||
max-line-length = 200
|
||||
exclude=,test_*.py
|
||||
|
|
|
|||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1,5 +1,5 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Community Forum
|
||||
url: https://discuss.erpnext.com/
|
||||
url: https://discuss.frappe.io/c/framework/5
|
||||
about: For general QnA, discussions and community help.
|
||||
|
|
|
|||
25
.github/helper/roulette.py
vendored
25
.github/helper/roulette.py
vendored
|
|
@ -4,8 +4,10 @@ import re
|
|||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from functools import lru_cache
|
||||
from urllib.error import HTTPError
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
|
|
@ -15,11 +17,30 @@ def fetch_pr_data(pr_number, repo, endpoint=""):
|
|||
if endpoint:
|
||||
api_url += f"/{endpoint}"
|
||||
|
||||
req = urllib.request.Request(api_url)
|
||||
res = urllib.request.urlopen(req)
|
||||
res = req(api_url)
|
||||
return json.loads(res.read().decode("utf8"))
|
||||
|
||||
|
||||
def req(url):
|
||||
"Simple resilient request call to handle rate limits."
|
||||
headers = None
|
||||
token = os.environ.get("GITHUB_TOKEN")
|
||||
if token:
|
||||
headers = {"authorization": f"Bearer {token}"}
|
||||
|
||||
retries = 0
|
||||
while True:
|
||||
try:
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
return urllib.request.urlopen(req)
|
||||
except HTTPError as exc:
|
||||
if exc.code == 403 and retries < 5:
|
||||
retries += 1
|
||||
time.sleep(retries)
|
||||
continue
|
||||
raise
|
||||
|
||||
|
||||
def get_files_list(pr_number, repo="frappe/frappe"):
|
||||
return [change["filename"] for change in fetch_pr_data(pr_number, repo, "files")]
|
||||
|
||||
|
|
|
|||
2
.github/workflows/create-release.yml
vendored
2
.github/workflows/create-release.yml
vendored
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 18
|
||||
- name: Setup dependencies
|
||||
run: |
|
||||
npm install @semantic-release/git @semantic-release/exec --no-save
|
||||
|
|
|
|||
2
.github/workflows/patch-mariadb-tests.yml
vendored
2
.github/workflows/patch-mariadb-tests.yml
vendored
|
|
@ -9,6 +9,7 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
# Do not change this as GITHUB_TOKEN is being used by roulette
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
|
@ -31,6 +32,7 @@ jobs:
|
|||
TYPE: "server"
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
REPO_NAME: ${{ github.repository }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
test:
|
||||
name: Patch
|
||||
|
|
|
|||
2
.github/workflows/server-tests.yml
vendored
2
.github/workflows/server-tests.yml
vendored
|
|
@ -12,6 +12,7 @@ concurrency:
|
|||
|
||||
|
||||
permissions:
|
||||
# Do not change this as GITHUB_TOKEN is being used by roulette
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
|
@ -34,6 +35,7 @@ jobs:
|
|||
TYPE: "server"
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
REPO_NAME: ${{ github.repository }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
test:
|
||||
name: Unit Tests
|
||||
|
|
|
|||
2
.github/workflows/ui-tests.yml
vendored
2
.github/workflows/ui-tests.yml
vendored
|
|
@ -11,6 +11,7 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
# Do not change this as GITHUB_TOKEN is being used by roulette
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
|
@ -33,6 +34,7 @@ jobs:
|
|||
TYPE: "ui"
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
REPO_NAME: ${{ github.repository }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Security Policy
|
||||
|
||||
The Frappe team and community take security issues in the Frappe Framework seriously. To report a security issue, fill out the form at [https://erpnext.com/security/report](https://erpnext.com/security/report).
|
||||
The Frappe team and community take security issues in the Frappe Framework seriously. To report a security issue, please go through the information mentioned [here](https://frappe.io/security).
|
||||
|
||||
You can help us make Frappe and consequently all Frappe dependent apps like [ERPNext](https://erpnext.com) more secure by following the [Reporting guidelines](https://erpnext.com/security).
|
||||
|
||||
We appreciate your efforts to responsibly disclose your findings. We'll endeavor to respond quickly, and will keep you updated throughout the process.
|
||||
We appreciate your efforts to responsibly disclose your findings. We'll endeavor to respond quickly, and will keep you updated throughout the process.
|
||||
|
|
|
|||
10
codecov.yml
10
codecov.yml
|
|
@ -9,7 +9,7 @@ coverage:
|
|||
target: auto
|
||||
threshold: 0.5%
|
||||
flags:
|
||||
- server-mariadb
|
||||
- server
|
||||
patch:
|
||||
default:
|
||||
target: 85%
|
||||
|
|
@ -17,7 +17,7 @@ coverage:
|
|||
only_pulls: true
|
||||
if_ci_failed: ignore
|
||||
flags:
|
||||
- server-mariadb
|
||||
- server
|
||||
|
||||
comment:
|
||||
layout: "diff, flags"
|
||||
|
|
@ -25,11 +25,7 @@ comment:
|
|||
show_critical_paths: true
|
||||
|
||||
flags:
|
||||
server-mariadb:
|
||||
paths:
|
||||
- "**/*.py"
|
||||
carryforward: true
|
||||
server-postgres:
|
||||
server:
|
||||
paths:
|
||||
- "**/*.py"
|
||||
carryforward: true
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ context("Date Control", () => {
|
|||
});
|
||||
}
|
||||
|
||||
it("Selecting a date from the datepicker", () => {
|
||||
it("Selecting a date from the datepicker & check prev & next button", () => {
|
||||
cy.clear_dialogs();
|
||||
cy.clear_datepickers();
|
||||
|
||||
|
|
@ -39,13 +39,7 @@ context("Date Control", () => {
|
|||
|
||||
// Verify if the selected date is set the date field
|
||||
cy.window().its("cur_dialog.fields_dict.date.value").should("be.equal", "2020-01-15");
|
||||
});
|
||||
|
||||
it("Checking next and previous button", () => {
|
||||
cy.clear_dialogs();
|
||||
cy.clear_datepickers();
|
||||
|
||||
get_dialog({ default: "2020-01-15" }).as("dialog");
|
||||
cy.get_field("date", "Date").click();
|
||||
|
||||
//Clicking on the next button in the datepicker
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ context("Form Builder", () => {
|
|||
cy.get_open_dialog().find(".msgprint").should("contain", "Options is required");
|
||||
cy.hide_dialog();
|
||||
|
||||
cy.get(first_field).click();
|
||||
cy.get(first_field).click({ force: true });
|
||||
|
||||
cy.get(".sidebar-container .frappe-control[data-fieldname='options'] input")
|
||||
.click()
|
||||
|
|
@ -114,7 +114,7 @@ context("Form Builder", () => {
|
|||
cy.get_open_dialog().find(".msgprint").should("contain", "In List View");
|
||||
cy.hide_dialog();
|
||||
|
||||
cy.get(first_field).click();
|
||||
cy.get(first_field).click({ force: true });
|
||||
cy.get(".sidebar-container .field label .label-area").contains("In List View").click();
|
||||
|
||||
// validate In Global Search
|
||||
|
|
@ -244,7 +244,7 @@ context("Form Builder", () => {
|
|||
let first_field =
|
||||
".tab-content.active .section-columns-container:first .column:first .field:first";
|
||||
|
||||
cy.get(".fields-container .field[title='Check']").drag(first_field, {
|
||||
cy.get(".fields-container .field[title='Data']").drag(first_field, {
|
||||
target: { x: 100, y: 10 },
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
context("Kanban Board", () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.login("frappe@example.com");
|
||||
cy.visit("/app");
|
||||
});
|
||||
|
||||
|
|
@ -96,4 +96,36 @@ context("Kanban Board", () => {
|
|||
.first()
|
||||
.should("not.contain", "ID:");
|
||||
});
|
||||
|
||||
it("Checks if Kanban Board edits are blocked for non-System Manager and non-owner of the Board", () => {
|
||||
// create admin kanban board
|
||||
cy.call("frappe.tests.ui_test_helpers.create_todo", { description: "Frappe User ToDo" });
|
||||
|
||||
cy.switch_to_user("Administrator");
|
||||
cy.call("frappe.tests.ui_test_helpers.create_admin_kanban");
|
||||
// remove sys manager
|
||||
cy.remove_role("frappe@example.com", "System Manager");
|
||||
|
||||
cy.switch_to_user("frappe@example.com");
|
||||
|
||||
cy.visit("/app/todo/view/kanban/Admin Kanban");
|
||||
|
||||
// Menu button should be hidden (dropdown for 'Save Filters' and 'Delete Kanban Board')
|
||||
cy.get(".no-list-sidebar .menu-btn-group .btn-default[data-original-title='Menu']").should(
|
||||
"have.length",
|
||||
0
|
||||
);
|
||||
// Kanban Columns should be visible (read-only)
|
||||
cy.get(".kanban .kanban-column").should("have.length", 2);
|
||||
// User should be able to add card (has access to ToDo)
|
||||
cy.get(".kanban .add-card").should("have.length", 2);
|
||||
// Column actions should be hidden (dropdown for 'Archive' and indicators)
|
||||
cy.get(".kanban .column-options").should("have.length", 0);
|
||||
|
||||
cy.add_role("frappe@example.com", "System Manager");
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.call("logout");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,16 +1,7 @@
|
|||
context("Permissions API", () => {
|
||||
before(() => {
|
||||
cy.visit("/login");
|
||||
|
||||
cy.login("Administrator");
|
||||
cy.call("frappe.tests.ui_test_helpers.add_remove_role", {
|
||||
action: "remove",
|
||||
user: "frappe@example.com",
|
||||
role: "System Manager",
|
||||
});
|
||||
cy.call("logout");
|
||||
|
||||
cy.login("frappe@example.com");
|
||||
cy.remove_role("frappe@example.com", "System Manager");
|
||||
cy.visit("/app");
|
||||
});
|
||||
|
||||
|
|
@ -44,14 +35,7 @@ context("Permissions API", () => {
|
|||
});
|
||||
|
||||
after(() => {
|
||||
cy.call("logout");
|
||||
|
||||
cy.login("Administrator");
|
||||
cy.call("frappe.tests.ui_test_helpers.add_remove_role", {
|
||||
action: "add",
|
||||
user: "frappe@example.com",
|
||||
role: "System Manager",
|
||||
});
|
||||
cy.add_role("frappe@example.com", "System Manager");
|
||||
cy.call("logout");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -190,6 +190,48 @@ context("Workspace 2.0", () => {
|
|||
cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
|
||||
});
|
||||
|
||||
it("Hide/Unhide Workspaces", () => {
|
||||
// hide
|
||||
cy.intercept({
|
||||
method: "POST",
|
||||
url: "api/method/frappe.desk.doctype.workspace.workspace.hide_page",
|
||||
}).as("hide_page");
|
||||
|
||||
cy.get(".codex-editor__redactor .ce-block");
|
||||
cy.get(".standard-actions .btn-secondary[data-label=Edit]").click();
|
||||
|
||||
cy.get('.sidebar-item-container[item-name="Duplicate Page"]')
|
||||
.find(".sidebar-item-control .setting-btn")
|
||||
.click();
|
||||
cy.get('.sidebar-item-container[item-name="Duplicate Page"]')
|
||||
.find('.dropdown-item[title="Hide Workspace"]')
|
||||
.click({ force: true });
|
||||
cy.wait(300);
|
||||
cy.get('.standard-actions .btn-secondary[data-label="Discard"]').click();
|
||||
cy.get('.sidebar-item-container[item-name="Duplicate Page"]').should("not.be.visible");
|
||||
|
||||
cy.wait("@hide_page");
|
||||
|
||||
// unhide
|
||||
cy.intercept({
|
||||
method: "POST",
|
||||
url: "api/method/frappe.desk.doctype.workspace.workspace.unhide_page",
|
||||
}).as("unhide_page");
|
||||
|
||||
cy.get(".codex-editor__redactor .ce-block");
|
||||
cy.get(".standard-actions .btn-secondary[data-label=Edit]").click();
|
||||
|
||||
cy.get('.sidebar-item-container[item-name="Duplicate Page"]')
|
||||
.find('[title="Unhide Workspace"]')
|
||||
.click({ force: true });
|
||||
cy.wait(300);
|
||||
|
||||
cy.get('.standard-actions .btn-secondary[data-label="Discard"]').click();
|
||||
cy.get('.sidebar-item-container[item-name="Duplicate Page"]').should("be.visible");
|
||||
|
||||
cy.wait("@unhide_page");
|
||||
});
|
||||
|
||||
it("Delete Duplicate Page", () => {
|
||||
cy.intercept({
|
||||
method: "POST",
|
||||
|
|
|
|||
|
|
@ -371,6 +371,45 @@ Cypress.Commands.add("update_doc", (doctype, docname, args) => {
|
|||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("switch_to_user", (user) => {
|
||||
cy.call("logout");
|
||||
cy.login(user);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("add_role", (user, role) => {
|
||||
cy.window()
|
||||
.its("frappe")
|
||||
.then((frappe) => {
|
||||
const session_user = frappe.session.user;
|
||||
add_remove_role("add", user, role, session_user);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("remove_role", (user, role) => {
|
||||
cy.window()
|
||||
.its("frappe")
|
||||
.then((frappe) => {
|
||||
const session_user = frappe.session.user;
|
||||
add_remove_role("remove", user, role, session_user);
|
||||
});
|
||||
});
|
||||
|
||||
const add_remove_role = (action, user, role, session_user) => {
|
||||
if (session_user !== "Administrator") {
|
||||
cy.switch_to_user("Administrator");
|
||||
}
|
||||
|
||||
cy.call("frappe.tests.ui_test_helpers.add_remove_role", {
|
||||
action: action,
|
||||
user: user,
|
||||
role: role,
|
||||
});
|
||||
|
||||
if (session_user !== "Administrator") {
|
||||
cy.switch_to_user(session_user);
|
||||
}
|
||||
};
|
||||
|
||||
Cypress.Commands.add("open_list_filter", () => {
|
||||
cy.get(".filter-section .filter-button").click();
|
||||
cy.wait(300);
|
||||
|
|
|
|||
|
|
@ -770,7 +770,12 @@ def is_whitelisted(method):
|
|||
|
||||
is_guest = session["user"] == "Guest"
|
||||
if method not in whitelisted or is_guest and method not in guest_methods:
|
||||
throw(_("Not permitted"), PermissionError)
|
||||
summary = _("You are not permitted to access this resource.")
|
||||
detail = _("Function {0} is not whitelisted.").format(
|
||||
bold(f"{method.__module__}.{method.__name__}")
|
||||
)
|
||||
msg = f"<details><summary>{summary}</summary>{detail}</details>"
|
||||
throw(msg, PermissionError, title="Method Not Allowed")
|
||||
|
||||
if is_guest and method not in xss_safe_methods:
|
||||
# strictly sanitize form_dict
|
||||
|
|
@ -1399,23 +1404,37 @@ def get_all_apps(with_internal_apps=True, sites_path=None):
|
|||
|
||||
|
||||
@request_cache
|
||||
def get_installed_apps(sort=False, frappe_last=False):
|
||||
"""Get list of installed apps in current site."""
|
||||
def get_installed_apps(sort=False, frappe_last=False, *, _ensure_on_bench=False):
|
||||
"""
|
||||
Get list of installed apps in current site.
|
||||
|
||||
:param sort: [DEPRECATED] Sort installed apps based on the sequence in sites/apps.txt
|
||||
:param frappe_last: [DEPRECATED] Keep frappe last. Do not use this, reverse the app list instead.
|
||||
:param ensure_on_bench: Only return apps that are present on bench.
|
||||
"""
|
||||
from frappe.utils.deprecations import deprecation_warning
|
||||
|
||||
if getattr(flags, "in_install_db", True):
|
||||
return []
|
||||
|
||||
if not db:
|
||||
connect()
|
||||
|
||||
if not local.all_apps:
|
||||
local.all_apps = cache().get_value("all_apps", get_all_apps)
|
||||
|
||||
installed = json.loads(db.get_global("installed_apps") or "[]")
|
||||
|
||||
if sort:
|
||||
if not local.all_apps:
|
||||
local.all_apps = cache().get_value("all_apps", get_all_apps)
|
||||
|
||||
deprecation_warning("`sort` argument is deprecated and will be removed in v15.")
|
||||
installed = [app for app in local.all_apps if app in installed]
|
||||
|
||||
if _ensure_on_bench:
|
||||
all_apps = cache().get_value("all_apps", get_all_apps)
|
||||
installed = [app for app in installed if app in all_apps]
|
||||
|
||||
if frappe_last:
|
||||
deprecation_warning("`frappe_last` argument is deprecated and will be removed in v15.")
|
||||
if "frappe" in installed:
|
||||
installed.remove("frappe")
|
||||
installed.append("frappe")
|
||||
|
|
@ -1445,7 +1464,7 @@ def _load_app_hooks(app_name: str | None = None):
|
|||
import types
|
||||
|
||||
hooks = {}
|
||||
apps = [app_name] if app_name else get_installed_apps(sort=True)
|
||||
apps = [app_name] if app_name else get_installed_apps(_ensure_on_bench=True)
|
||||
|
||||
for app in apps:
|
||||
try:
|
||||
|
|
@ -1855,9 +1874,6 @@ def get_list(doctype, *args, **kwargs):
|
|||
|
||||
# filter as a list of lists
|
||||
frappe.get_list("ToDo", fields="*", filters = [["modified", ">", "2014-01-01"]])
|
||||
|
||||
# filter as a list of dicts
|
||||
frappe.get_list("ToDo", fields="*", filters = {"description": ("like", "test%")})
|
||||
"""
|
||||
import frappe.model.db_query
|
||||
|
||||
|
|
@ -1882,9 +1898,6 @@ def get_all(doctype, *args, **kwargs):
|
|||
|
||||
# filter as a list of lists
|
||||
frappe.get_all("ToDo", fields=["*"], filters = [["modified", ">", "2014-01-01"]])
|
||||
|
||||
# filter as a list of dicts
|
||||
frappe.get_all("ToDo", fields=["*"], filters = {"description": ("like", "test%")})
|
||||
"""
|
||||
kwargs["ignore_permissions"] = True
|
||||
if not "limit_page_length" in kwargs:
|
||||
|
|
@ -1907,7 +1920,7 @@ def get_value(*args, **kwargs):
|
|||
return db.get_value(*args, **kwargs)
|
||||
|
||||
|
||||
def as_json(obj: dict | list, indent=1, separators=None) -> str:
|
||||
def as_json(obj: dict | list, indent=1, separators=None, ensure_ascii=True) -> str:
|
||||
from frappe.utils.response import json_handler
|
||||
|
||||
if separators is None:
|
||||
|
|
@ -1915,13 +1928,24 @@ def as_json(obj: dict | list, indent=1, separators=None) -> str:
|
|||
|
||||
try:
|
||||
return json.dumps(
|
||||
obj, indent=indent, sort_keys=True, default=json_handler, separators=separators
|
||||
obj,
|
||||
indent=indent,
|
||||
sort_keys=True,
|
||||
default=json_handler,
|
||||
separators=separators,
|
||||
ensure_ascii=ensure_ascii,
|
||||
)
|
||||
except TypeError:
|
||||
# this would break in case the keys are not all os "str" type - as defined in the JSON
|
||||
# adding this to ensure keys are sorted (expected behaviour)
|
||||
sorted_obj = dict(sorted(obj.items(), key=lambda kv: str(kv[0])))
|
||||
return json.dumps(sorted_obj, indent=indent, default=json_handler, separators=separators)
|
||||
return json.dumps(
|
||||
sorted_obj,
|
||||
indent=indent,
|
||||
default=json_handler,
|
||||
separators=separators,
|
||||
ensure_ascii=ensure_ascii,
|
||||
)
|
||||
|
||||
|
||||
def are_emails_muted():
|
||||
|
|
|
|||
|
|
@ -303,6 +303,17 @@ def has_permission(doctype, docname, perm_type="read"):
|
|||
return {"has_permission": frappe.has_permission(doctype, perm_type.lower(), docname)}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_doc_permissions(doctype, docname):
|
||||
"""Returns an evaluated document permissions dict like `{"read":1, "write":1}`
|
||||
|
||||
:param doctype: DocType of the document to be evaluated
|
||||
:param docname: `name` of the document to be evaluated
|
||||
"""
|
||||
doc = frappe.get_doc(doctype, docname)
|
||||
return {"permissions": frappe.permissions.get_doc_permissions(doc)}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_password(doctype, name, fieldname):
|
||||
"""Return a password type property. Only applicable for System Managers
|
||||
|
|
|
|||
|
|
@ -109,7 +109,8 @@ def new_site(
|
|||
"--with-public-files", help="Restores the public files of the site, given path to its tar file"
|
||||
)
|
||||
@click.option(
|
||||
"--with-private-files", help="Restores the private files of the site, given path to its tar file"
|
||||
"--with-private-files",
|
||||
help="Restores the private files of the site, given path to its tar file",
|
||||
)
|
||||
@click.option(
|
||||
"--force",
|
||||
|
|
@ -191,7 +192,8 @@ def _restore(
|
|||
fg="red",
|
||||
)
|
||||
click.secho(
|
||||
"Use `bench partial-restore` to restore a partial backup to an existing site.", fg="yellow"
|
||||
"Use `bench partial-restore` to restore a partial backup to an existing site.",
|
||||
fg="yellow",
|
||||
)
|
||||
_backup.decryption_rollback()
|
||||
sys.exit(1)
|
||||
|
|
@ -222,7 +224,8 @@ def _restore(
|
|||
fg="red",
|
||||
)
|
||||
click.secho(
|
||||
"Use `bench partial-restore` to restore a partial backup to an existing site.", fg="yellow"
|
||||
"Use `bench partial-restore` to restore a partial backup to an existing site.",
|
||||
fg="yellow",
|
||||
)
|
||||
_backup.decryption_rollback()
|
||||
sys.exit(1)
|
||||
|
|
@ -324,7 +327,8 @@ def partial_restore(context, sql_file_path, verbose, encryption_key=None):
|
|||
# Check for full backup file
|
||||
if "Partial Backup" not in header:
|
||||
click.secho(
|
||||
"Full backup file detected.Use `bench restore` to restore a Frappe Site.", fg="red"
|
||||
"Full backup file detected.Use `bench restore` to restore a Frappe Site.",
|
||||
fg="red",
|
||||
)
|
||||
_backup.decryption_rollback()
|
||||
sys.exit(1)
|
||||
|
|
@ -355,7 +359,8 @@ def partial_restore(context, sql_file_path, verbose, encryption_key=None):
|
|||
# Check for Full backup file.
|
||||
if "Partial Backup" not in header:
|
||||
click.secho(
|
||||
"Full Backup file detected.Use `bench restore` to restore a Frappe Site.", fg="red"
|
||||
"Full Backup file detected.Use `bench restore` to restore a Frappe Site.",
|
||||
fg="red",
|
||||
)
|
||||
_backup.decryption_rollback()
|
||||
sys.exit(1)
|
||||
|
|
@ -391,7 +396,12 @@ def reinstall(
|
|||
|
||||
|
||||
def _reinstall(
|
||||
site, admin_password=None, db_root_username=None, db_root_password=None, yes=False, verbose=False
|
||||
site,
|
||||
admin_password=None,
|
||||
db_root_username=None,
|
||||
db_root_password=None,
|
||||
yes=False,
|
||||
verbose=False,
|
||||
):
|
||||
from frappe.installer import _new_site
|
||||
from frappe.utils.synchronization import filelock
|
||||
|
|
@ -719,7 +729,10 @@ def use(site, sites_path="."):
|
|||
@click.option("--backup-path-private-files", default=None, help="Set path for saving private file")
|
||||
@click.option("--backup-path-conf", default=None, help="Set path for saving config file")
|
||||
@click.option(
|
||||
"--ignore-backup-conf", default=False, is_flag=True, help="Ignore excludes/includes set in config"
|
||||
"--ignore-backup-conf",
|
||||
default=False,
|
||||
is_flag=True,
|
||||
help="Ignore excludes/includes set in config",
|
||||
)
|
||||
@click.option("--verbose", default=False, is_flag=True, help="Add verbosity")
|
||||
@click.option("--compress", default=False, is_flag=True, help="Compress private and public files")
|
||||
|
|
@ -774,7 +787,8 @@ def backup(
|
|||
continue
|
||||
if frappe.get_system_settings("encrypt_backup") and frappe.get_site_config().encryption_key:
|
||||
click.secho(
|
||||
"Backup encryption is turned on. Please note the backup encryption key.", fg="yellow"
|
||||
"Backup encryption is turned on. Please note the backup encryption key.",
|
||||
fg="yellow",
|
||||
)
|
||||
|
||||
odb.print_summary()
|
||||
|
|
@ -1120,14 +1134,31 @@ def stop_recording(context):
|
|||
@click.option(
|
||||
"--bind-tls", is_flag=True, default=False, help="Returns a reference to the https tunnel."
|
||||
)
|
||||
@click.option(
|
||||
"--use-default-authtoken",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Use the auth token present in ngrok's config.",
|
||||
)
|
||||
@pass_context
|
||||
def start_ngrok(context, bind_tls):
|
||||
def start_ngrok(context, bind_tls, use_default_authtoken):
|
||||
"""Start a ngrok tunnel to your local development server."""
|
||||
from pyngrok import ngrok
|
||||
|
||||
site = get_site(context)
|
||||
frappe.init(site=site)
|
||||
|
||||
ngrok_authtoken = frappe.conf.ngrok_authtoken
|
||||
if not use_default_authtoken:
|
||||
if not ngrok_authtoken:
|
||||
click.echo(
|
||||
f"\n{click.style('ngrok_authtoken', fg='yellow')} not found in site config.\n"
|
||||
"Please register for a free ngrok account at: https://dashboard.ngrok.com/signup and place the obtained authtoken in the site config.",
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
ngrok.set_auth_token(ngrok_authtoken)
|
||||
|
||||
port = frappe.conf.http_port or frappe.conf.webserver_port
|
||||
tunnel = ngrok.connect(addr=str(port), host_header=site, bind_tls=bind_tls)
|
||||
print(f"Public URL: {tunnel.public_url}")
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ def unzip_file(name: str):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_attached_images(doctype: str, names: list[str]) -> frappe._dict:
|
||||
def get_attached_images(doctype: str, names: list[str] | str) -> frappe._dict:
|
||||
"""get list of image urls attached in form
|
||||
returns {name: ['image.jpg', 'image.png']}"""
|
||||
|
||||
|
|
|
|||
|
|
@ -260,7 +260,7 @@ def export_json(doctype, path, filters=None, or_filters=None, name=None, order_b
|
|||
path = os.path.join("..", path)
|
||||
|
||||
with open(path, "w") as outfile:
|
||||
outfile.write(frappe.as_json(out))
|
||||
outfile.write(frappe.as_json(out, ensure_ascii=False))
|
||||
|
||||
|
||||
def export_csv(doctype, path):
|
||||
|
|
|
|||
|
|
@ -546,15 +546,16 @@
|
|||
{
|
||||
"depends_on": "eval:!in_list([\"Tab Break\", \"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
|
||||
"fieldname": "documentation_url",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Documentation URL"
|
||||
"fieldtype": "Data",
|
||||
"label": "Documentation URL",
|
||||
"options": "URL"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-17 14:14:39.404696",
|
||||
"modified": "2023-01-11 20:46:43.164926",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocField",
|
||||
|
|
|
|||
|
|
@ -29,3 +29,12 @@ class DocField(Document):
|
|||
if self.fieldtype == "Select":
|
||||
options = self.options or ""
|
||||
return [d for d in options.split("\n") if d]
|
||||
|
||||
def __repr__(self):
|
||||
unsaved = "unsaved" if not self.name else ""
|
||||
doctype = self.__class__.__name__
|
||||
|
||||
docstatus = f" docstatus={self.docstatus}" if self.docstatus else ""
|
||||
parent = f" parent={self.parent}" if getattr(self, "parent", None) else ""
|
||||
|
||||
return f"<{self.fieldtype}{doctype}: {self.fieldname}{docstatus}{parent}{unsaved}>"
|
||||
|
|
|
|||
|
|
@ -195,10 +195,12 @@ class DocType(Document):
|
|||
|
||||
def set_default_in_list_view(self):
|
||||
"""Set default in-list-view for first 4 mandatory fields"""
|
||||
not_allowed_in_list_view = get_fields_not_allowed_in_list_view(self.meta)
|
||||
|
||||
if not [d.fieldname for d in self.fields if d.in_list_view]:
|
||||
cnt = 0
|
||||
for d in self.fields:
|
||||
if d.reqd and not d.hidden and not d.fieldtype in table_fields:
|
||||
if d.reqd and not d.hidden and not d.fieldtype in not_allowed_in_list_view:
|
||||
d.in_list_view = 1
|
||||
cnt += 1
|
||||
if cnt == 4:
|
||||
|
|
@ -1446,10 +1448,7 @@ def validate_fields(meta):
|
|||
fields = meta.get("fields")
|
||||
fieldname_list = [d.fieldname for d in fields]
|
||||
|
||||
not_allowed_in_list_view = list(copy.copy(no_value_fields))
|
||||
not_allowed_in_list_view.append("Attach Image")
|
||||
if meta.istable:
|
||||
not_allowed_in_list_view.remove("Button")
|
||||
not_allowed_in_list_view = get_fields_not_allowed_in_list_view(meta)
|
||||
|
||||
for d in fields:
|
||||
if not d.permlevel:
|
||||
|
|
@ -1490,6 +1489,14 @@ def validate_fields(meta):
|
|||
check_image_field(meta)
|
||||
|
||||
|
||||
def get_fields_not_allowed_in_list_view(meta) -> list[str]:
|
||||
not_allowed_in_list_view = list(copy.copy(no_value_fields))
|
||||
not_allowed_in_list_view.append("Attach Image")
|
||||
if meta.istable:
|
||||
not_allowed_in_list_view.remove("Button")
|
||||
return not_allowed_in_list_view
|
||||
|
||||
|
||||
def validate_permissions_for_doctype(doctype, for_remove=False, alert=False):
|
||||
"""Validates if permissions are set correctly."""
|
||||
doctype = frappe.get_doc("DocType", doctype)
|
||||
|
|
|
|||
|
|
@ -722,6 +722,28 @@ class TestDocType(FrappeTestCase):
|
|||
self.assertEqual(frappe.get_meta(doctype).get_field(field).default, "DELETETHIS")
|
||||
frappe.delete_doc("DocType", doctype)
|
||||
|
||||
def test_not_in_list_view_for_not_allowed_mandatory_field(self):
|
||||
doctype = new_doctype(
|
||||
fields=[
|
||||
{
|
||||
"fieldname": "cover_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Cover Image",
|
||||
"reqd": 1, # mandatory
|
||||
},
|
||||
{
|
||||
"fieldname": "book_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Book Name",
|
||||
"reqd": 1, # mandatory
|
||||
},
|
||||
],
|
||||
).insert()
|
||||
|
||||
self.assertFalse(doctype.fields[0].in_list_view)
|
||||
self.assertTrue(doctype.fields[1].in_list_view)
|
||||
frappe.delete_doc("DocType", doctype.name)
|
||||
|
||||
|
||||
def new_doctype(
|
||||
name: str | None = None,
|
||||
|
|
@ -759,8 +781,7 @@ def new_doctype(
|
|||
}
|
||||
)
|
||||
|
||||
if fields:
|
||||
for f in fields:
|
||||
doc.append("fields", f)
|
||||
if fields and len(fields) > 0:
|
||||
doc.set("fields", fields)
|
||||
|
||||
return doc
|
||||
|
|
|
|||
|
|
@ -85,8 +85,8 @@ class Domain(Document):
|
|||
def set_default_portal_role(self):
|
||||
"""Set default portal role based on domain"""
|
||||
if self.data.get("default_portal_role"):
|
||||
frappe.db.set_value(
|
||||
"Portal Settings", None, "default_role", self.data.get("default_portal_role")
|
||||
frappe.db.set_single_value(
|
||||
"Portal Settings", "default_role", self.data.get("default_portal_role")
|
||||
)
|
||||
|
||||
def setup_properties(self):
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ frappe.ui.form.on("File", {
|
|||
|
||||
preview_file: function (frm) {
|
||||
let $preview = "";
|
||||
let file_name = frm.doc.file_name.split("?")[0];
|
||||
let file_extension = file_name.split(".").pop()?.toLowerCase();
|
||||
|
||||
if (frappe.utils.is_image_file(frm.doc.file_url)) {
|
||||
$preview = $(`<div class="img_preview">
|
||||
|
|
@ -40,7 +42,7 @@ frappe.ui.form.on("File", {
|
|||
${__("Your browser does not support the video element.")}
|
||||
</video>
|
||||
</div>`);
|
||||
} else if (frm.doc.file_name.split("?")[0].endsWith(".pdf")) {
|
||||
} else if (file_extension === "pdf") {
|
||||
$preview = $(`<div class="img_preview">
|
||||
<object style="background:#323639;" width="100%">
|
||||
<embed
|
||||
|
|
@ -51,7 +53,7 @@ frappe.ui.form.on("File", {
|
|||
>
|
||||
</object>
|
||||
</div>`);
|
||||
} else if (frm.doc.file_name.split("?")[0].endsWith(".mp3")) {
|
||||
} else if (file_extension === "mp3") {
|
||||
$preview = $(`<div class="img_preview">
|
||||
<audio width="480" height="60" controls>
|
||||
<source src="${frm.doc.file_url}" type="audio/mpeg">
|
||||
|
|
|
|||
|
|
@ -42,27 +42,26 @@ def _supports_log_clearing(doctype: str) -> bool:
|
|||
|
||||
class LogSettings(Document):
|
||||
def validate(self):
|
||||
self.validate_supported_doctypes()
|
||||
self.validate_duplicates()
|
||||
self._remove_unsupported_doctypes()
|
||||
self._deduplicate_entries()
|
||||
self.add_default_logtypes()
|
||||
|
||||
def validate_supported_doctypes(self):
|
||||
for entry in self.logs_to_clear:
|
||||
def _remove_unsupported_doctypes(self):
|
||||
for entry in list(self.logs_to_clear):
|
||||
if _supports_log_clearing(entry.ref_doctype):
|
||||
continue
|
||||
|
||||
msg = _("{} does not support automated log clearing.").format(frappe.bold(entry.ref_doctype))
|
||||
if frappe.conf.developer_mode:
|
||||
msg += "<br>" + _("Implement `clear_old_logs` method to enable auto error clearing.")
|
||||
frappe.throw(msg, title=_("DocType not supported by Log Settings."))
|
||||
frappe.msgprint(msg, title=_("DocType not supported by Log Settings."))
|
||||
self.remove(entry)
|
||||
|
||||
def validate_duplicates(self):
|
||||
def _deduplicate_entries(self):
|
||||
seen = set()
|
||||
for entry in self.logs_to_clear:
|
||||
for entry in list(self.logs_to_clear):
|
||||
if entry.ref_doctype in seen:
|
||||
frappe.throw(
|
||||
_("{} appears more than once in configured log doctypes.").format(entry.ref_doctype)
|
||||
)
|
||||
self.remove(entry)
|
||||
seen.add(entry.ref_doctype)
|
||||
|
||||
def add_default_logtypes(self):
|
||||
|
|
|
|||
|
|
@ -50,11 +50,14 @@ class SystemSettings(Document):
|
|||
|
||||
social_login_enabled = frappe.db.exists("Social Login Key", {"enable_social_login": 1})
|
||||
ldap_enabled = frappe.db.get_single_value("LDAP Settings", "enabled")
|
||||
login_with_email_link_enabled = frappe.db.get_single_value(
|
||||
"System Settings", "login_with_email_link"
|
||||
)
|
||||
|
||||
if not (social_login_enabled or ldap_enabled):
|
||||
if not (social_login_enabled or ldap_enabled or login_with_email_link_enabled):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please enable atleast one Social Login Key or LDAP before disabling username/password based login."
|
||||
"Please enable atleast one Social Login Key or LDAP or Login With Email Link before disabling username/password based login."
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ test_records = frappe.get_test_records("User")
|
|||
class TestUser(FrappeTestCase):
|
||||
def tearDown(self):
|
||||
# disable password strength test
|
||||
frappe.db.set_value("System Settings", "System Settings", "enable_password_policy", 0)
|
||||
frappe.db.set_value("System Settings", "System Settings", "minimum_password_score", "")
|
||||
frappe.db.set_value("System Settings", "System Settings", "password_reset_limit", 3)
|
||||
frappe.db.set_single_value("System Settings", "enable_password_policy", 0)
|
||||
frappe.db.set_single_value("System Settings", "minimum_password_score", "")
|
||||
frappe.db.set_single_value("System Settings", "password_reset_limit", 3)
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def test_user_type(self):
|
||||
|
|
@ -111,7 +111,7 @@ class TestUser(FrappeTestCase):
|
|||
|
||||
self.assertEqual(frappe.db.get_value("User", "xxxtest@example.com"), None)
|
||||
|
||||
frappe.db.set_value("Website Settings", "Website Settings", "_test", "_test_val")
|
||||
frappe.db.set_single_value("Website Settings", "_test", "_test_val")
|
||||
self.assertEqual(frappe.db.get_value("Website Settings", None, "_test"), "_test_val")
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Website Settings", "Website Settings", "_test"), "_test_val"
|
||||
|
|
@ -179,15 +179,15 @@ class TestUser(FrappeTestCase):
|
|||
|
||||
def test_password_strength(self):
|
||||
# Test Password without Password Strength Policy
|
||||
frappe.db.set_value("System Settings", "System Settings", "enable_password_policy", 0)
|
||||
frappe.db.set_single_value("System Settings", "enable_password_policy", 0)
|
||||
|
||||
# password policy is disabled, test_password_strength should be ignored
|
||||
result = test_password_strength("test_password")
|
||||
self.assertFalse(result.get("feedback", None))
|
||||
|
||||
# Test Password with Password Strenth Policy Set
|
||||
frappe.db.set_value("System Settings", "System Settings", "enable_password_policy", 1)
|
||||
frappe.db.set_value("System Settings", "System Settings", "minimum_password_score", 2)
|
||||
frappe.db.set_single_value("System Settings", "enable_password_policy", 1)
|
||||
frappe.db.set_single_value("System Settings", "minimum_password_score", 2)
|
||||
|
||||
# Score 1; should now fail
|
||||
result = test_password_strength("bee2ve")
|
||||
|
|
@ -275,7 +275,7 @@ class TestUser(FrappeTestCase):
|
|||
|
||||
def test_rate_limiting_for_reset_password(self):
|
||||
# Allow only one reset request for a day
|
||||
frappe.db.set_value("System Settings", "System Settings", "password_reset_limit", 1)
|
||||
frappe.db.set_single_value("System Settings", "password_reset_limit", 1)
|
||||
frappe.db.commit()
|
||||
|
||||
url = get_url()
|
||||
|
|
@ -443,9 +443,7 @@ class TestUser(FrappeTestCase):
|
|||
def test_reset_password_link_expiry(self):
|
||||
new_password = "new_password"
|
||||
# set the reset password expiry to 1 second
|
||||
frappe.db.set_value(
|
||||
"System Settings", "System Settings", "reset_password_link_expiry_duration", 1
|
||||
)
|
||||
frappe.db.set_single_value("System Settings", "reset_password_link_expiry_duration", 1)
|
||||
frappe.set_user("testpassword@example.com")
|
||||
test_user = frappe.get_doc("User", "testpassword@example.com")
|
||||
test_user.reset_password()
|
||||
|
|
|
|||
|
|
@ -305,12 +305,10 @@ class User(Document):
|
|||
.from_(user_role_doctype)
|
||||
.select(user_doctype.name)
|
||||
.where(user_role_doctype.role == "System Manager")
|
||||
.where(user_doctype.docstatus < 2)
|
||||
.where(user_doctype.enabled == 1)
|
||||
.where(user_role_doctype.parent == user_doctype.name)
|
||||
.where(user_role_doctype.parent.notin(["Administrator", self.name]))
|
||||
.limit(1)
|
||||
.distinct()
|
||||
).run()
|
||||
|
||||
def get_fullname(self):
|
||||
|
|
@ -582,7 +580,7 @@ class User(Document):
|
|||
if len(email_accounts) != len(set(email_accounts)):
|
||||
frappe.throw(_("Email Account added multiple times"))
|
||||
|
||||
def get_social_login_userid(self, provider):
|
||||
def get_social_login_userid(self, provider: str):
|
||||
try:
|
||||
for p in self.social_logins:
|
||||
if p.provider == provider:
|
||||
|
|
|
|||
|
|
@ -111,54 +111,59 @@ frappe.ui.form.on("Customize Form", {
|
|||
frm.page.clear_icons();
|
||||
|
||||
if (frm.doc.doc_type) {
|
||||
frm.page.set_title(__("Customize Form - {0}", [frm.doc.doc_type]));
|
||||
frappe.customize_form.set_primary_action(frm);
|
||||
frappe.model.with_doctype(frm.doc.doc_type).then(() => {
|
||||
frm.page.set_title(__("Customize Form - {0}", [frm.doc.doc_type]));
|
||||
frappe.customize_form.set_primary_action(frm);
|
||||
|
||||
if (!frm.is_new()) {
|
||||
frm.add_custom_button(__("Try new form builder", [__(frm.doc.doc_type)]), () => {
|
||||
frappe.set_route("form-builder", frm.doc.doc_type, "customize");
|
||||
});
|
||||
}
|
||||
if (!frm.is_new()) {
|
||||
frm.add_custom_button(
|
||||
__("Try new form builder", [__(frm.doc.doc_type)]),
|
||||
() => {
|
||||
frappe.set_route("form-builder", frm.doc.doc_type, "customize");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Go to {0} List", [__(frm.doc.doc_type)]),
|
||||
function () {
|
||||
frappe.set_route("List", frm.doc.doc_type);
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
frm.add_custom_button(
|
||||
__("Go to {0} List", [__(frm.doc.doc_type)]),
|
||||
function () {
|
||||
frappe.set_route("List", frm.doc.doc_type);
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Reload"),
|
||||
function () {
|
||||
frm.script_manager.trigger("doc_type");
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
frm.add_custom_button(
|
||||
__("Reload"),
|
||||
function () {
|
||||
frm.script_manager.trigger("doc_type");
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Reset to defaults"),
|
||||
function () {
|
||||
frappe.customize_form.confirm(__("Remove all customizations?"), frm);
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
frm.add_custom_button(
|
||||
__("Reset to defaults"),
|
||||
function () {
|
||||
frappe.customize_form.confirm(__("Remove all customizations?"), frm);
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Set Permissions"),
|
||||
function () {
|
||||
frappe.set_route("permission-manager", frm.doc.doc_type);
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
frm.add_custom_button(
|
||||
__("Set Permissions"),
|
||||
function () {
|
||||
frappe.set_route("permission-manager", frm.doc.doc_type);
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
|
||||
const is_autoname_autoincrement = frm.doc.autoname === "autoincrement";
|
||||
frm.set_df_property("naming_rule", "hidden", is_autoname_autoincrement);
|
||||
frm.set_df_property("autoname", "read_only", is_autoname_autoincrement);
|
||||
frm.toggle_display(
|
||||
["queue_in_background"],
|
||||
frappe.get_meta(frm.doc.doc_type).is_submittable || 0
|
||||
);
|
||||
const is_autoname_autoincrement = frm.doc.autoname === "autoincrement";
|
||||
frm.set_df_property("naming_rule", "hidden", is_autoname_autoincrement);
|
||||
frm.set_df_property("autoname", "read_only", is_autoname_autoincrement);
|
||||
frm.toggle_display(
|
||||
["queue_in_background"],
|
||||
frappe.get_meta(frm.doc.doc_type).is_submittable || 0
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
frm.events.setup_export(frm);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ from frappe.model.utils.link_count import flush_local_link_count
|
|||
from frappe.query_builder.functions import Count
|
||||
from frappe.utils import cast as cast_fieldtype
|
||||
from frappe.utils import cint, get_datetime, get_table_name, getdate, now, sbool
|
||||
from frappe.utils.deprecations import deprecated
|
||||
from frappe.utils.deprecations import deprecated, deprecation_warning
|
||||
|
||||
IFNULL_PATTERN = re.compile(r"ifnull\(", flags=re.IGNORECASE)
|
||||
INDEX_PATTERN = re.compile(r"\s*\([^)]+\)\s*")
|
||||
|
|
@ -362,7 +362,7 @@ class Database:
|
|||
self.sql(query, debug=debug)
|
||||
|
||||
def check_transaction_status(self, query):
|
||||
"""Raises exception if more than 20,000 `INSERT`, `UPDATE` queries are
|
||||
"""Raises exception if more than 200,000 `INSERT`, `UPDATE` queries are
|
||||
executed in one transaction. This is to ensure that writes are always flushed otherwise this
|
||||
could cause the system to hang."""
|
||||
self.check_implicit_commit(query)
|
||||
|
|
@ -689,13 +689,30 @@ class Database:
|
|||
def get_list(*args, **kwargs):
|
||||
return frappe.get_list(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _get_update_dict(
|
||||
fieldname: str | dict, value: Any, *, modified: str, modified_by: str, update_modified: bool
|
||||
) -> dict[str, Any]:
|
||||
"""Create update dict that represents column-values to be updated."""
|
||||
update_dict = fieldname if isinstance(fieldname, dict) else {fieldname: value}
|
||||
|
||||
if update_modified:
|
||||
modified = modified or now()
|
||||
modified_by = modified_by or frappe.session.user
|
||||
update_dict.update({"modified": modified, "modified_by": modified_by})
|
||||
|
||||
return update_dict
|
||||
|
||||
def set_single_value(
|
||||
self,
|
||||
doctype: str,
|
||||
fieldname: str | dict,
|
||||
value: str | int | None = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
*,
|
||||
modified=None,
|
||||
modified_by=None,
|
||||
update_modified=True,
|
||||
debug=False,
|
||||
):
|
||||
"""Set field value of Single DocType.
|
||||
|
||||
|
|
@ -708,7 +725,23 @@ class Database:
|
|||
# Update the `deny_multiple_sessions` field in System Settings DocType.
|
||||
company = frappe.db.set_single_value("System Settings", "deny_multiple_sessions", True)
|
||||
"""
|
||||
return self.set_value(doctype, doctype, fieldname, value, *args, **kwargs)
|
||||
|
||||
to_update = self._get_update_dict(
|
||||
fieldname, value, modified=modified, modified_by=modified_by, update_modified=update_modified
|
||||
)
|
||||
|
||||
frappe.db.delete(
|
||||
"Singles", filters={"field": ("in", tuple(to_update)), "doctype": doctype}, debug=debug
|
||||
)
|
||||
|
||||
singles_data = ((doctype, key, sbool(value)) for key, value in to_update.items())
|
||||
frappe.qb.into("Singles").columns("doctype", "field", "value").insert(*singles_data).run(
|
||||
debug=debug
|
||||
)
|
||||
frappe.clear_document_cache(doctype, doctype)
|
||||
|
||||
if doctype in self.value_cache:
|
||||
del self.value_cache[doctype]
|
||||
|
||||
def get_single_value(self, doctype, fieldname, cache=True):
|
||||
"""Get property of Single DocType. Cache locally by default
|
||||
|
|
@ -834,40 +867,40 @@ class Database:
|
|||
:param update_modified: default True. Set as false, if you don't want to update the timestamp.
|
||||
:param debug: Print the query in the developer / js console.
|
||||
"""
|
||||
is_single_doctype = not (dn and dt != dn)
|
||||
to_update = field if isinstance(field, dict) else {field: val}
|
||||
|
||||
if update_modified:
|
||||
modified = modified or now()
|
||||
modified_by = modified_by or frappe.session.user
|
||||
to_update.update({"modified": modified, "modified_by": modified_by})
|
||||
|
||||
if is_single_doctype:
|
||||
frappe.db.delete(
|
||||
"Singles", filters={"field": ("in", tuple(to_update)), "doctype": dt}, debug=debug
|
||||
if _is_single_doctype := not (dn and dt != dn):
|
||||
deprecation_warning(
|
||||
"Calling db.set_value on single doctype is deprecated. This behaviour will be removed in version 15. Use db.set_single_value instead."
|
||||
)
|
||||
self.set_single_value(
|
||||
doctype=dt,
|
||||
fieldname=field,
|
||||
value=val,
|
||||
debug=debug,
|
||||
update_modified=update_modified,
|
||||
modified=modified,
|
||||
modified_by=modified_by,
|
||||
)
|
||||
return
|
||||
|
||||
singles_data = ((dt, key, sbool(value)) for key, value in to_update.items())
|
||||
query = (
|
||||
frappe.qb.into("Singles").columns("doctype", "field", "value").insert(*singles_data)
|
||||
).run(debug=debug)
|
||||
frappe.clear_document_cache(dt, dt)
|
||||
to_update = self._get_update_dict(
|
||||
field, val, modified=modified, modified_by=modified_by, update_modified=update_modified
|
||||
)
|
||||
|
||||
query = frappe.qb.engine.build_conditions(table=dt, filters=dn, update=True)
|
||||
|
||||
if isinstance(dn, str):
|
||||
frappe.clear_document_cache(dt, dn)
|
||||
else:
|
||||
query = frappe.qb.engine.build_conditions(table=dt, filters=dn, update=True)
|
||||
# TODO: Fix this; doesn't work rn - gavin@frappe.io
|
||||
# frappe.cache().hdel_keys(dt, "document_cache")
|
||||
# Workaround: clear all document caches
|
||||
frappe.cache().delete_value("document_cache")
|
||||
|
||||
if isinstance(dn, str):
|
||||
frappe.clear_document_cache(dt, dn)
|
||||
else:
|
||||
# TODO: Fix this; doesn't work rn - gavin@frappe.io
|
||||
# frappe.cache().hdel_keys(dt, "document_cache")
|
||||
# Workaround: clear all document caches
|
||||
frappe.cache().delete_value("document_cache")
|
||||
for column, value in to_update.items():
|
||||
query = query.set(column, value)
|
||||
|
||||
for column, value in to_update.items():
|
||||
query = query.set(column, value)
|
||||
|
||||
query.run(debug=debug)
|
||||
query.run(debug=debug)
|
||||
|
||||
if dt in self.value_cache:
|
||||
del self.value_cache[dt]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from pymysql.constants.ER import DUP_ENTRY
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.database.schema import DBTable
|
||||
|
|
@ -115,17 +117,15 @@ class MariaDBTable(DBTable):
|
|||
frappe.db.sql(query)
|
||||
|
||||
except Exception as e:
|
||||
# sanitize
|
||||
if e.args[0] == 1060:
|
||||
frappe.throw(str(e))
|
||||
elif e.args[0] == 1062:
|
||||
if query := locals().get("query"): # this weirdness is to avoid potentially unbounded vars
|
||||
print(f"Failed to alter schema using query: {query}")
|
||||
|
||||
if e.args[0] == DUP_ENTRY:
|
||||
fieldname = str(e).split("'")[-2]
|
||||
frappe.throw(
|
||||
_("{0} field cannot be set as unique in {1}, as there are non-unique existing values").format(
|
||||
fieldname, self.table_name
|
||||
)
|
||||
)
|
||||
elif e.args[0] == 1067:
|
||||
frappe.throw(str(e.args[1]))
|
||||
else:
|
||||
raise e
|
||||
|
||||
raise
|
||||
|
|
|
|||
|
|
@ -379,7 +379,17 @@ def get_workspace_sidebar_items():
|
|||
|
||||
# pages sorted based on sequence id
|
||||
order_by = "sequence_id asc"
|
||||
fields = ["name", "title", "for_user", "parent_page", "content", "public", "module", "icon"]
|
||||
fields = [
|
||||
"name",
|
||||
"title",
|
||||
"for_user",
|
||||
"parent_page",
|
||||
"content",
|
||||
"public",
|
||||
"module",
|
||||
"icon",
|
||||
"is_hidden",
|
||||
]
|
||||
all_pages = frappe.get_all(
|
||||
"Workspace", fields=fields, filters=filters, order_by=order_by, ignore_permissions=True
|
||||
)
|
||||
|
|
@ -391,7 +401,7 @@ def get_workspace_sidebar_items():
|
|||
try:
|
||||
workspace = Workspace(page, True)
|
||||
if has_access or workspace.is_permitted():
|
||||
if page.public:
|
||||
if page.public and (has_access or not page.is_hidden):
|
||||
pages.append(page)
|
||||
elif page.for_user == frappe.session.user:
|
||||
private_pages.append(page)
|
||||
|
|
|
|||
|
|
@ -88,10 +88,15 @@ def update_order(board_name, order):
|
|||
"""Save the order of cards in columns"""
|
||||
board = frappe.get_doc("Kanban Board", board_name)
|
||||
doctype = board.reference_doctype
|
||||
updated_cards = []
|
||||
|
||||
if not frappe.has_permission(doctype, "write"):
|
||||
# Return board data from db
|
||||
return board, updated_cards
|
||||
|
||||
fieldname = board.field_name
|
||||
order_dict = json.loads(order)
|
||||
|
||||
updated_cards = []
|
||||
for col_name, cards in order_dict.items():
|
||||
for card in cards:
|
||||
column = frappe.get_value(doctype, {"name": card}, fieldname)
|
||||
|
|
@ -103,8 +108,7 @@ def update_order(board_name, order):
|
|||
if column.column_name == col_name:
|
||||
column.order = json.dumps(cards)
|
||||
|
||||
board.save()
|
||||
return board, updated_cards
|
||||
return board.save(ignore_permissions=True), updated_cards
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
@ -114,6 +118,9 @@ def update_order_for_single_card(
|
|||
"""Save the order of cards in columns"""
|
||||
board = frappe.get_doc("Kanban Board", board_name)
|
||||
doctype = board.reference_doctype
|
||||
|
||||
frappe.has_permission(doctype, "write", throw=True)
|
||||
|
||||
fieldname = board.field_name
|
||||
old_index = frappe.parse_json(old_index)
|
||||
new_index = frappe.parse_json(new_index)
|
||||
|
|
@ -130,7 +137,7 @@ def update_order_for_single_card(
|
|||
# save updated order
|
||||
board.columns[from_col_idx].order = frappe.as_json(from_col_order)
|
||||
board.columns[to_col_idx].order = frappe.as_json(to_col_order)
|
||||
board.save()
|
||||
board.save(ignore_permissions=True)
|
||||
|
||||
# update changed value in doc
|
||||
frappe.set_value(doctype, docname, fieldname, to_colname)
|
||||
|
|
@ -151,13 +158,14 @@ def get_kanban_column_order_and_index(board, colname):
|
|||
def add_card(board_name, docname, colname):
|
||||
board = frappe.get_doc("Kanban Board", board_name)
|
||||
|
||||
frappe.has_permission(board.reference_doctype, "write", throw=True)
|
||||
|
||||
col_order, col_idx = get_kanban_column_order_and_index(board, colname)
|
||||
col_order.insert(0, docname)
|
||||
|
||||
board.columns[col_idx].order = frappe.as_json(col_order)
|
||||
|
||||
board.save()
|
||||
return board
|
||||
return board.save(ignore_permissions=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ frappe.ui.form.on("Workspace", {
|
|||
}
|
||||
}
|
||||
|
||||
if (frappe.boot.developer_mode) {
|
||||
frm.set_df_property("module", "read_only", 0);
|
||||
}
|
||||
|
||||
frm.layout.show_message(message);
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"restrict_to_domain",
|
||||
"hide_custom",
|
||||
"public",
|
||||
"is_hidden",
|
||||
"content",
|
||||
"tab_break_2",
|
||||
"charts",
|
||||
|
|
@ -71,7 +72,8 @@
|
|||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Module",
|
||||
"options": "Module Def"
|
||||
"options": "Module Def",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
|
|
@ -173,11 +175,17 @@
|
|||
"fieldtype": "Table",
|
||||
"label": "Quick Lists",
|
||||
"options": "Workspace Quick List"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_hidden",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Hidden"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2022-08-16 18:01:42.632238",
|
||||
"modified": "2023-01-07 19:37:39.512482",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Workspace",
|
||||
|
|
@ -195,15 +203,6 @@
|
|||
"role": "Workspace Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ def save_page(title, public, new_widgets, blocks):
|
|||
|
||||
if not public:
|
||||
filters = {"for_user": frappe.session.user, "label": title + "-" + frappe.session.user}
|
||||
pages = frappe.get_list("Workspace", filters=filters)
|
||||
pages = frappe.get_all("Workspace", filters=filters)
|
||||
if pages:
|
||||
doc = frappe.get_doc("Workspace", pages[0])
|
||||
|
||||
|
|
@ -209,12 +209,8 @@ def save_page(title, public, new_widgets, blocks):
|
|||
@frappe.whitelist()
|
||||
def update_page(name, title, icon, parent, public):
|
||||
public = frappe.parse_json(public)
|
||||
|
||||
doc = frappe.get_doc("Workspace", name)
|
||||
|
||||
filters = {"parent_page": doc.title, "public": doc.public}
|
||||
child_docs = frappe.get_list("Workspace", filters=filters)
|
||||
|
||||
if doc:
|
||||
doc.title = title
|
||||
doc.icon = icon
|
||||
|
|
@ -230,6 +226,9 @@ def update_page(name, title, icon, parent, public):
|
|||
rename_doc("Workspace", name, new_name, force=True, ignore_permissions=True)
|
||||
|
||||
# update new name and public in child pages
|
||||
child_docs = frappe.get_all(
|
||||
"Workspace", filters={"parent_page": doc.title, "public": doc.public}
|
||||
)
|
||||
if child_docs:
|
||||
for child in child_docs:
|
||||
child_doc = frappe.get_doc("Workspace", child.name)
|
||||
|
|
@ -248,6 +247,32 @@ def update_page(name, title, icon, parent, public):
|
|||
return {"name": title, "public": public, "label": new_name}
|
||||
|
||||
|
||||
def hide_unhide_page(page_name: str, is_hidden: bool):
|
||||
page = frappe.get_doc("Workspace", page_name)
|
||||
|
||||
if page.get("public") and not is_workspace_manager():
|
||||
frappe.throw(
|
||||
_("Need Workspace Manager role to hide/unhide public workspaces"), frappe.PermissionError
|
||||
)
|
||||
|
||||
if not page.get("public") and page.get("for_user") != frappe.session.user:
|
||||
frappe.throw(_("Cannot update private workspace of other users"), frappe.PermissionError)
|
||||
|
||||
page.is_hidden = int(is_hidden)
|
||||
page.save(ignore_permissions=True)
|
||||
return True
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def hide_page(page_name: str):
|
||||
return hide_unhide_page(page_name, 1)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def unhide_page(page_name: str):
|
||||
return hide_unhide_page(page_name, 0)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def duplicate_page(page_name, new_page):
|
||||
if not loads(new_page):
|
||||
|
|
@ -338,7 +363,7 @@ def last_sequence_id(doc):
|
|||
if not doc_exists:
|
||||
return 0
|
||||
|
||||
return frappe.db.get_list(
|
||||
return frappe.get_all(
|
||||
"Workspace",
|
||||
fields=["sequence_id"],
|
||||
filters={"public": doc.public, "for_user": doc.for_user},
|
||||
|
|
@ -347,7 +372,7 @@ def last_sequence_id(doc):
|
|||
|
||||
|
||||
def get_page_list(fields, filters):
|
||||
return frappe.get_list("Workspace", fields=fields, filters=filters, order_by="sequence_id asc")
|
||||
return frappe.get_all("Workspace", fields=fields, filters=filters, order_by="sequence_id asc")
|
||||
|
||||
|
||||
def is_workspace_manager():
|
||||
|
|
|
|||
|
|
@ -435,9 +435,6 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di
|
|||
continue
|
||||
linkmeta = link_meta_bundle[0]
|
||||
|
||||
if not linkmeta.has_permission():
|
||||
continue
|
||||
|
||||
if not linkmeta.get("issingle"):
|
||||
fields = [
|
||||
d.fieldname
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import json
|
|||
import frappe
|
||||
from frappe.core.doctype.submission_queue.submission_queue import queue_submission
|
||||
from frappe.desk.form.load import run_onload
|
||||
from frappe.model.docstatus import DocStatus
|
||||
from frappe.monitor import add_data_to_monitor
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
|
|
@ -17,8 +18,14 @@ def savedocs(doc, action):
|
|||
set_local_name(doc)
|
||||
|
||||
# action
|
||||
doc.docstatus = {"Save": 0, "Submit": 1, "Update": 1, "Cancel": 2}[action]
|
||||
if doc.docstatus == 1:
|
||||
doc.docstatus = {
|
||||
"Save": DocStatus.draft(),
|
||||
"Submit": DocStatus.submitted(),
|
||||
"Update": DocStatus.submitted(),
|
||||
"Cancel": DocStatus.cancelled(),
|
||||
}[action]
|
||||
|
||||
if doc.docstatus.is_submitted():
|
||||
if action == "Submit" and doc.meta.queue_in_background and not is_scheduler_inactive():
|
||||
queue_submission(doc, action)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -115,7 +115,6 @@ class Leaderboard {
|
|||
default: frappe.defaults.get_default("company"),
|
||||
reqd: 1,
|
||||
change: (e) => {
|
||||
this.options.selected_company = e.currentTarget.value;
|
||||
this.make_request();
|
||||
},
|
||||
});
|
||||
|
|
@ -182,7 +181,9 @@ class Leaderboard {
|
|||
let $li = $(e.currentTarget);
|
||||
let doctype = $li.find(".doctype-text").attr("doctype-value");
|
||||
|
||||
this.options.selected_company = frappe.defaults.get_default("company");
|
||||
this.company_select.set_value(
|
||||
frappe.defaults.get_default("company") || this.company_select.get_value()
|
||||
);
|
||||
this.options.selected_doctype = doctype;
|
||||
this.options.selected_filter = this.filters[doctype];
|
||||
this.options.selected_filter_item = this.filters[doctype][0];
|
||||
|
|
@ -237,13 +238,16 @@ class Leaderboard {
|
|||
}
|
||||
|
||||
get_leaderboard(notify) {
|
||||
if (!this.options.selected_company) {
|
||||
frappe.throw(__("Please select Company"));
|
||||
let company = this.company_select.get_value();
|
||||
if (!company && !this.leaderboard_config[this.options.selected_doctype].company_disabled) {
|
||||
notify(this, null);
|
||||
frappe.show_alert(__("Please select Company"));
|
||||
return;
|
||||
}
|
||||
frappe
|
||||
.call(this.leaderboard_config[this.options.selected_doctype].method, {
|
||||
date_range: this.get_date_range(),
|
||||
company: this.options.selected_company,
|
||||
company: company,
|
||||
field: this.options.selected_filter_item,
|
||||
limit: this.leaderboard_limit,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
|
|||
}
|
||||
|
||||
get_setup_slides_filtered_by_domain() {
|
||||
var filtered_slides = [];
|
||||
let filtered_slides = [];
|
||||
frappe.setup.slides.forEach(function (slide) {
|
||||
if (frappe.setup.domains) {
|
||||
let active_domains = frappe.setup.domains;
|
||||
|
|
@ -329,7 +329,7 @@ frappe.setup.SetupWizardSlide = class SetupWizardSlide extends frappe.ui.Slide {
|
|||
}
|
||||
|
||||
set_init_values() {
|
||||
var me = this;
|
||||
let me = this;
|
||||
// set values from frappe.setup.values
|
||||
if (frappe.wizard.values && this.fields) {
|
||||
this.fields.forEach(function (f) {
|
||||
|
|
@ -348,7 +348,7 @@ frappe.setup.slides_settings = [
|
|||
{
|
||||
// Welcome (language) slide
|
||||
name: "welcome",
|
||||
title: __("Hello!"),
|
||||
title: __("Welcome"),
|
||||
|
||||
fields: [
|
||||
{
|
||||
|
|
@ -418,16 +418,9 @@ frappe.setup.slides_settings = [
|
|||
{
|
||||
// Profile slide
|
||||
name: "user",
|
||||
title: __("The First User: You"),
|
||||
title: __("Let's setup your account"),
|
||||
icon: "fa fa-user",
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "Attach Image",
|
||||
fieldname: "attach_user_image",
|
||||
label: __("Attach Your Picture"),
|
||||
is_private: 0,
|
||||
align: "center",
|
||||
},
|
||||
{
|
||||
fieldname: "full_name",
|
||||
label: __("Full Name"),
|
||||
|
|
@ -456,15 +449,6 @@ frappe.setup.slides_settings = [
|
|||
[frappe.boot.user.first_name, frappe.boot.user.last_name].join(" ").trim()
|
||||
);
|
||||
}
|
||||
|
||||
var user_image = frappe.get_cookie("user_image");
|
||||
var $attach_user_image = slide.form.fields_dict.attach_user_image.$wrapper;
|
||||
|
||||
if (user_image) {
|
||||
$attach_user_image.find(".missing-image").toggle(false);
|
||||
$attach_user_image.find("img").attr("src", decodeURIComponent(user_image));
|
||||
$attach_user_image.find(".img-container").toggle(true);
|
||||
}
|
||||
delete slide.form.fields_dict.email;
|
||||
} else {
|
||||
slide.form.fields_dict.email.df.reqd = 1;
|
||||
|
|
@ -484,7 +468,7 @@ frappe.setup.slides_settings = [
|
|||
let email = frappe.setup.data.email;
|
||||
slide.form.fields_dict.email.set_input(email);
|
||||
if (frappe.get_gravatar(email, 200)) {
|
||||
var $attach_user_image = slide.form.fields_dict.attach_user_image.$wrapper;
|
||||
let $attach_user_image = slide.form.fields_dict.attach_user_image.$wrapper;
|
||||
$attach_user_image.find(".missing-image").toggle(false);
|
||||
$attach_user_image.find("img").attr("src", frappe.get_gravatar(email, 200));
|
||||
$attach_user_image.find(".img-container").toggle(true);
|
||||
|
|
@ -569,7 +553,7 @@ frappe.setup.utils = {
|
|||
.on("change", function () {
|
||||
clearTimeout(slide.language_call_timeout);
|
||||
slide.language_call_timeout = setTimeout(() => {
|
||||
var lang = $(this).val() || "English";
|
||||
let lang = $(this).val() || "English";
|
||||
frappe._messages = {};
|
||||
frappe.call({
|
||||
method: "frappe.desk.page.setup_wizard.setup_wizard.load_messages",
|
||||
|
|
@ -595,9 +579,9 @@ frappe.setup.utils = {
|
|||
Bind a slide's country, timezone and currency fields
|
||||
*/
|
||||
slide.get_input("country").on("change", function () {
|
||||
var country = slide.get_input("country").val();
|
||||
var $timezone = slide.get_input("timezone");
|
||||
var data = frappe.setup.data.regional_data;
|
||||
let country = slide.get_input("country").val();
|
||||
let $timezone = slide.get_input("timezone");
|
||||
let data = frappe.setup.data.regional_data;
|
||||
|
||||
$timezone.empty();
|
||||
|
||||
|
|
@ -618,12 +602,12 @@ frappe.setup.utils = {
|
|||
});
|
||||
|
||||
slide.get_input("currency").on("change", function () {
|
||||
var currency = slide.get_input("currency").val();
|
||||
let currency = slide.get_input("currency").val();
|
||||
if (!currency) return;
|
||||
frappe.model.with_doc("Currency", currency, function () {
|
||||
frappe.provide("locals.:Currency." + currency);
|
||||
var currency_doc = frappe.model.get_doc("Currency", currency);
|
||||
var number_format = currency_doc.number_format;
|
||||
let currency_doc = frappe.model.get_doc("Currency", currency);
|
||||
let number_format = currency_doc.number_format;
|
||||
if (number_format === "#.###") {
|
||||
number_format = "#.###,##";
|
||||
} else if (number_format === "#,###") {
|
||||
|
|
|
|||
|
|
@ -267,10 +267,10 @@ def add_all_roles_to(name):
|
|||
|
||||
def disable_future_access():
|
||||
frappe.db.set_default("desktop:home_page", "workspace")
|
||||
frappe.db.set_value("System Settings", "System Settings", "setup_complete", 1)
|
||||
frappe.db.set_single_value("System Settings", "setup_complete", 1)
|
||||
|
||||
# Enable onboarding after install
|
||||
frappe.db.set_value("System Settings", "System Settings", "enable_onboarding", 1)
|
||||
frappe.db.set_single_value("System Settings", "enable_onboarding", 1)
|
||||
|
||||
if not frappe.flags.in_test:
|
||||
# remove all roles and add 'Administrator' to prevent future access
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
from datetime import datetime
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder import Interval, Order
|
||||
from frappe.query_builder.functions import Date, Sum, UnixTimestamp
|
||||
from frappe.utils import getdate
|
||||
|
||||
|
||||
|
|
@ -11,21 +13,18 @@ def get_energy_points_heatmap_data(user, date):
|
|||
except Exception:
|
||||
date = getdate()
|
||||
|
||||
eps_log = frappe.qb.DocType("Energy Point Log")
|
||||
|
||||
return dict(
|
||||
frappe.db.sql(
|
||||
"""select unix_timestamp(date(creation)), sum(points)
|
||||
from `tabEnergy Point Log`
|
||||
where
|
||||
date(creation) > subdate('{date}', interval 1 year) and
|
||||
date(creation) < subdate('{date}', interval -1 year) and
|
||||
user = %s and
|
||||
type != 'Review'
|
||||
group by date(creation)
|
||||
order by creation asc""".format(
|
||||
date=date
|
||||
),
|
||||
user,
|
||||
)
|
||||
frappe.qb.from_(eps_log)
|
||||
.select(UnixTimestamp(Date(eps_log.creation)), Sum(eps_log.points))
|
||||
.where(eps_log.user == user)
|
||||
.where(eps_log["type"] != "Review")
|
||||
.where(Date(eps_log.creation) > Date(date) - Interval(years=1))
|
||||
.where(Date(eps_log.creation) < Date(date) + Interval(years=1))
|
||||
.groupby(Date(eps_log.creation))
|
||||
.orderby(Date(eps_log.creation), order=Order.asc)
|
||||
.run()
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -51,7 +50,7 @@ def get_user_rank(user):
|
|||
month_start = datetime.today().replace(day=1)
|
||||
monthly_rank = frappe.get_all(
|
||||
"Energy Point Log",
|
||||
group_by="user",
|
||||
group_by="`tabEnergy Point Log`.`user`",
|
||||
filters={"creation": [">", month_start], "type": ["!=", "Review"]},
|
||||
fields=["user", "sum(points)"],
|
||||
order_by="sum(points) desc",
|
||||
|
|
@ -60,7 +59,7 @@ def get_user_rank(user):
|
|||
|
||||
all_time_rank = frappe.get_all(
|
||||
"Energy Point Log",
|
||||
group_by="user",
|
||||
group_by="`tabEnergy Point Log`.`user`",
|
||||
filters={"type": ["!=", "Review"]},
|
||||
fields=["user", "sum(points)"],
|
||||
order_by="sum(points) desc",
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from frappe.utils import add_user_info, format_duration
|
|||
@frappe.read_only()
|
||||
def get():
|
||||
args = get_form_params()
|
||||
# If virtual doctype get data from controller het_list method
|
||||
# If virtual doctype, get data from controller get_list method
|
||||
if is_virtual_doctype(args.doctype):
|
||||
controller = get_controller(args.doctype)
|
||||
data = compress(controller.get_list(args))
|
||||
|
|
@ -294,7 +294,7 @@ def save_report(name, doctype, report_settings):
|
|||
if report.report_type != "Report Builder":
|
||||
frappe.throw(_("Only reports of type Report Builder can be edited"))
|
||||
|
||||
if report.owner != frappe.session.user and not frappe.has_permission("Report", "write"):
|
||||
if report.owner != frappe.session.user and not report.has_permission("write"):
|
||||
frappe.throw(_("Insufficient Permissions for editing Report"), frappe.PermissionError)
|
||||
else:
|
||||
report = frappe.new_doc("Report")
|
||||
|
|
@ -323,7 +323,7 @@ def delete_report(name):
|
|||
if report.report_type != "Report Builder":
|
||||
frappe.throw(_("Only reports of type Report Builder can be deleted"))
|
||||
|
||||
if report.owner != frappe.session.user and not frappe.has_permission("Report", "delete"):
|
||||
if report.owner != frappe.session.user and not report.has_permission("delete"):
|
||||
frappe.throw(_("Insufficient Permissions for deleting Report"), frappe.PermissionError)
|
||||
|
||||
report.delete(ignore_permissions=True)
|
||||
|
|
|
|||
|
|
@ -67,27 +67,29 @@ frappe.email_defaults_pop = {
|
|||
};
|
||||
|
||||
function oauth_access(frm) {
|
||||
return frappe.call({
|
||||
method: "frappe.email.oauth.oauth_access",
|
||||
args: {
|
||||
email_account: frm.doc.name,
|
||||
service: frm.doc.service || "",
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
window.open(r.message.url, "_self");
|
||||
}
|
||||
},
|
||||
frappe.model.with_doc("Connected App", frm.doc.connected_app, () => {
|
||||
const connected_app = frappe.get_doc("Connected App", frm.doc.connected_app);
|
||||
return frappe.call({
|
||||
doc: connected_app,
|
||||
method: "initiate_web_application_flow",
|
||||
args: {
|
||||
success_uri: window.location.pathname,
|
||||
user: frm.doc.connected_user,
|
||||
},
|
||||
callback: function (r) {
|
||||
window.open(r.message, "_self");
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function set_default_max_attachment_size(frm, field) {
|
||||
if (frm.doc.__islocal && !frm.doc[field]) {
|
||||
function set_default_max_attachment_size(frm) {
|
||||
if (frm.doc.__islocal && !frm.doc["attachment_limit"]) {
|
||||
frappe.call({
|
||||
method: "frappe.core.api.file.get_max_file_size",
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
frm.set_value(field, Number(r.message) / (1024 * 1024));
|
||||
frm.set_value("attachment_limit", Number(r.message) / (1024 * 1024));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -104,8 +106,6 @@ frappe.ui.form.on("Email Account", {
|
|||
frm.set_value(key, value);
|
||||
});
|
||||
}
|
||||
frm.events.show_gmail_message_for_less_secure_apps(frm);
|
||||
frm.events.toggle_auth_method(frm);
|
||||
},
|
||||
|
||||
use_imap: function (frm) {
|
||||
|
|
@ -133,12 +133,6 @@ frappe.ui.form.on("Email Account", {
|
|||
},
|
||||
|
||||
onload: function (frm) {
|
||||
if (frappe.utils.get_query_params().successful_authorization === "1") {
|
||||
frappe.show_alert(__("Successfully Authorized"));
|
||||
// FIXME: find better alternative
|
||||
window.history.replaceState(null, "", window.location.pathname);
|
||||
}
|
||||
|
||||
frm.set_df_property("append_to", "only_select", true);
|
||||
frm.set_query(
|
||||
"append_to",
|
||||
|
|
@ -153,15 +147,13 @@ frappe.ui.form.on("Email Account", {
|
|||
frm.add_child("imap_folder", { folder_name: "INBOX" });
|
||||
frm.refresh_field("imap_folder");
|
||||
}
|
||||
frm.toggle_display(["auth_method"], frm.doc.service === "GMail");
|
||||
set_default_max_attachment_size(frm, "attachment_limit");
|
||||
set_default_max_attachment_size(frm);
|
||||
frm.events.show_oauth_authorization_message(frm);
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
frm.events.enable_incoming(frm);
|
||||
frm.events.notify_if_unreplied(frm);
|
||||
frm.events.show_gmail_message_for_less_secure_apps(frm);
|
||||
frm.events.show_oauth_authorization_message(frm);
|
||||
|
||||
if (frappe.route_flags.delete_user_from_locals && frappe.route_flags.linked_user) {
|
||||
delete frappe.route_flags.delete_user_from_locals;
|
||||
|
|
@ -169,47 +161,31 @@ frappe.ui.form.on("Email Account", {
|
|||
}
|
||||
},
|
||||
|
||||
after_save(frm) {
|
||||
if (frm.doc.auth_method === "OAuth" && !frm.doc.refresh_token) {
|
||||
oauth_access(frm);
|
||||
}
|
||||
},
|
||||
|
||||
toggle_auth_method: function (frm) {
|
||||
if (frm.doc.service !== "GMail") {
|
||||
frm.toggle_display(["auth_method"], false);
|
||||
frm.doc.auth_method = "Basic";
|
||||
} else {
|
||||
frm.toggle_display(["auth_method"], true);
|
||||
}
|
||||
},
|
||||
|
||||
show_gmail_message_for_less_secure_apps: function (frm) {
|
||||
frm.dashboard.clear_headline();
|
||||
let msg = __(
|
||||
"GMail will only work if you enable 2-step authentication and use app-specific password OR use OAuth."
|
||||
);
|
||||
let cta = __("Read the step by step guide here.");
|
||||
msg += ` <a target="_blank" href="https://docs.erpnext.com/docs/v13/user/manual/en/setting-up/email/email_account_setup_with_gmail">${cta}</a>`;
|
||||
if (frm.doc.service === "GMail") {
|
||||
frm.dashboard.set_headline_alert(msg);
|
||||
}
|
||||
authorize_api_access: function (frm) {
|
||||
oauth_access(frm);
|
||||
},
|
||||
|
||||
show_oauth_authorization_message(frm) {
|
||||
if (frm.doc.auth_method === "OAuth" && !frm.doc.refresh_token) {
|
||||
let msg = __(
|
||||
'OAuth has been enabled but not authorised. Please use "Authorise API Access" button to do the same.'
|
||||
);
|
||||
frm.dashboard.clear_headline();
|
||||
frm.dashboard.set_headline_alert(msg, "yellow");
|
||||
if (frm.doc.auth_method === "OAuth" && frm.doc.connected_app) {
|
||||
frappe.call({
|
||||
method: "frappe.integrations.doctype.connected_app.connected_app.has_token",
|
||||
args: {
|
||||
connected_app: frm.doc.connected_app,
|
||||
connected_user: frm.doc.connected_user,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (!r.message) {
|
||||
let msg = __(
|
||||
'OAuth has been enabled but not authorised. Please use "Authorise API Access" button to do the same.'
|
||||
);
|
||||
frm.dashboard.clear_headline();
|
||||
frm.dashboard.set_headline_alert(msg, "yellow");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
authorize_api_access: function (frm) {
|
||||
oauth_access(frm);
|
||||
},
|
||||
|
||||
domain: frappe.utils.debounce((frm) => {
|
||||
if (frm.doc.domain) {
|
||||
frappe.call({
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@
|
|||
"awaiting_password",
|
||||
"ascii_encode_password",
|
||||
"column_break_10",
|
||||
"refresh_token",
|
||||
"access_token",
|
||||
"connected_app",
|
||||
"connected_user",
|
||||
"login_id_is_different",
|
||||
"login_id",
|
||||
"mailbox_settings",
|
||||
|
|
@ -203,7 +203,6 @@
|
|||
"label": "Use SSL"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:!doc.domain && doc.enable_incoming",
|
||||
"description": "Ignore attachments over this size",
|
||||
"fetch_from": "domain.attachment_limit",
|
||||
|
|
@ -577,25 +576,11 @@
|
|||
"label": "IMAP Details"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.service === \"GMail\" && doc.auth_method === \"OAuth\" && !doc.__islocal && !doc.__unsaved",
|
||||
"depends_on": "eval: doc.auth_method === \"OAuth\" && !doc.__islocal && !doc.__unsaved",
|
||||
"fieldname": "authorize_api_access",
|
||||
"fieldtype": "Button",
|
||||
"label": "Authorize API Access"
|
||||
},
|
||||
{
|
||||
"fieldname": "refresh_token",
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 1,
|
||||
"label": "Refresh Token",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "access_token",
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 1,
|
||||
"label": "Access Token",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "Basic",
|
||||
"fieldname": "auth_method",
|
||||
|
|
@ -610,12 +595,28 @@
|
|||
"fieldname": "use_starttls",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use STARTTLS"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.auth_method === \"OAuth\"",
|
||||
"fieldname": "connected_app",
|
||||
"fieldtype": "Link",
|
||||
"label": "Connected App",
|
||||
"mandatory_depends_on": "eval: doc.auth_method === \"OAuth\"",
|
||||
"options": "Connected App"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.auth_method === \"OAuth\"",
|
||||
"fieldname": "connected_user",
|
||||
"fieldtype": "Link",
|
||||
"label": "Connected User",
|
||||
"mandatory_depends_on": "eval: doc.auth_method === \"OAuth\"",
|
||||
"options": "User"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-inbox",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-08-23 00:31:05.305462",
|
||||
"modified": "2022-12-28 14:56:18.754804",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Email Account",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ from frappe.utils import cint, comma_or, cstr, parse_addr, validate_email_addres
|
|||
from frappe.utils.background_jobs import enqueue, get_jobs
|
||||
from frappe.utils.error import raise_error_on_no_output
|
||||
from frappe.utils.jinja import render_template
|
||||
from frappe.utils.password import decrypt, encrypt
|
||||
from frappe.utils.user import get_system_managers
|
||||
|
||||
|
||||
|
|
@ -83,23 +82,16 @@ class EmailAccount(Document):
|
|||
return
|
||||
|
||||
use_oauth = self.auth_method == "OAuth"
|
||||
validate_oauth = use_oauth and not (self.is_new() and not self.get_oauth_token())
|
||||
self.use_starttls = cint(self.use_imap and self.use_starttls and not self.use_ssl)
|
||||
|
||||
if getattr(self, "service", "") != "GMail" and use_oauth:
|
||||
self.auth_method = "Basic"
|
||||
use_oauth = False
|
||||
|
||||
if use_oauth:
|
||||
# no need for awaiting password for oauth
|
||||
self.awaiting_password = 0
|
||||
self.password = None
|
||||
|
||||
elif self.refresh_token:
|
||||
# clear access & refresh token
|
||||
self.refresh_token = self.access_token = None
|
||||
|
||||
if not frappe.local.flags.in_install and not self.awaiting_password:
|
||||
if self.refresh_token or self.password or self.smtp_server in ("127.0.0.1", "localhost"):
|
||||
if validate_oauth or self.password or self.smtp_server in ("127.0.0.1", "localhost"):
|
||||
if self.enable_incoming:
|
||||
self.get_incoming_server()
|
||||
self.no_failed = 0
|
||||
|
|
@ -188,6 +180,7 @@ class EmailAccount(Document):
|
|||
if frappe.cache().get_value("workers:no-internet") == True:
|
||||
return None
|
||||
|
||||
oauth_token = self.get_oauth_token()
|
||||
args = frappe._dict(
|
||||
{
|
||||
"email_account_name": self.email_account_name,
|
||||
|
|
@ -196,14 +189,12 @@ class EmailAccount(Document):
|
|||
"use_ssl": self.use_ssl,
|
||||
"use_starttls": self.use_starttls,
|
||||
"username": getattr(self, "login_id", None) or self.email_id,
|
||||
"service": getattr(self, "service", ""),
|
||||
"use_imap": self.use_imap,
|
||||
"email_sync_rule": email_sync_rule,
|
||||
"incoming_port": get_port(self),
|
||||
"initial_sync_count": self.initial_sync_count or 100,
|
||||
"use_oauth": self.auth_method == "OAuth",
|
||||
"refresh_token": decrypt(self.refresh_token) if self.refresh_token else None,
|
||||
"access_token": decrypt(self.access_token) if self.access_token else None,
|
||||
"access_token": oauth_token.get_password("access_token") if oauth_token else None,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -392,8 +383,6 @@ class EmailAccount(Document):
|
|||
},
|
||||
"name": {"conf_names": ("email_sender_name",), "default": "Frappe"},
|
||||
"auth_method": {"conf_names": ("auth_method"), "default": "Basic"},
|
||||
"access_token": {"conf_names": ("mail_access_token")},
|
||||
"refresh_token": {"conf_names": ("mail_refresh_token")},
|
||||
"from_site_config": {"default": True},
|
||||
}
|
||||
|
||||
|
|
@ -401,15 +390,13 @@ class EmailAccount(Document):
|
|||
for doc_field_name, d in field_to_conf_name_map.items():
|
||||
conf_names, default = d.get("conf_names") or [], d.get("default")
|
||||
value = [frappe.conf.get(k) for k in conf_names if frappe.conf.get(k)]
|
||||
|
||||
if doc_field_name in ("refresh_token", "access_token"):
|
||||
account_details[doc_field_name] = value and encrypt(value[0])
|
||||
else:
|
||||
account_details[doc_field_name] = (value and value[0]) or default
|
||||
account_details[doc_field_name] = (value and value[0]) or default
|
||||
|
||||
return account_details
|
||||
|
||||
def sendmail_config(self):
|
||||
oauth_token = self.get_oauth_token()
|
||||
|
||||
return {
|
||||
"email_account": self.name,
|
||||
"server": self.smtp_server,
|
||||
|
|
@ -418,10 +405,8 @@ class EmailAccount(Document):
|
|||
"password": self._password,
|
||||
"use_ssl": cint(self.use_ssl_for_outgoing),
|
||||
"use_tls": cint(self.use_tls),
|
||||
"service": getattr(self, "service", ""),
|
||||
"use_oauth": self.auth_method == "OAuth",
|
||||
"refresh_token": decrypt(self.refresh_token) if self.refresh_token else None,
|
||||
"access_token": decrypt(self.access_token) if self.access_token else None,
|
||||
"access_token": oauth_token.get_password("access_token") if oauth_token else None,
|
||||
}
|
||||
|
||||
def get_smtp_server(self):
|
||||
|
|
@ -681,6 +666,11 @@ class EmailAccount(Document):
|
|||
except Exception:
|
||||
self.log_error("Unable to add to Sent folder")
|
||||
|
||||
def get_oauth_token(self):
|
||||
if self.auth_method == "OAuth":
|
||||
connected_app = frappe.get_doc("Connected App", self.connected_app)
|
||||
return connected_app.get_active_token(self.connected_user)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_append_to(
|
||||
|
|
@ -776,25 +766,29 @@ def notify_unreplied():
|
|||
|
||||
def pull(now=False):
|
||||
"""Will be called via scheduler, pull emails from all enabled Email accounts."""
|
||||
from frappe.integrations.doctype.connected_app.connected_app import has_token
|
||||
|
||||
if frappe.cache().get_value("workers:no-internet") == True:
|
||||
if test_internet():
|
||||
frappe.cache().set_value("workers:no-internet", False)
|
||||
else:
|
||||
return
|
||||
return
|
||||
|
||||
doctype = frappe.qb.DocType("Email Account")
|
||||
email_accounts = (
|
||||
frappe.qb.from_(doctype)
|
||||
.select(doctype.name)
|
||||
.select(doctype.name, doctype.auth_method, doctype.connected_app, doctype.connected_user)
|
||||
.where(doctype.enable_incoming == 1)
|
||||
.where(
|
||||
(doctype.awaiting_password == 0)
|
||||
| ((doctype.auth_method == "OAuth") & (doctype.refresh_token.isnotnull()))
|
||||
)
|
||||
.where(doctype.awaiting_password == 0)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
for email_account in email_accounts:
|
||||
if email_account.auth_method == "OAuth" and not has_token(
|
||||
email_account.connected_app, email_account.connected_user
|
||||
):
|
||||
# don't try to pull from accounts which dont have access token (for Oauth)
|
||||
continue
|
||||
|
||||
if now:
|
||||
pull_from_email_account(email_account.name)
|
||||
|
||||
|
|
@ -917,7 +911,7 @@ def remove_user_email_inbox(email_account):
|
|||
@frappe.whitelist()
|
||||
def set_email_password(email_account, password):
|
||||
account = frappe.get_doc("Email Account", email_account)
|
||||
if account.awaiting_password and not account.auth_method == "OAuth":
|
||||
if account.awaiting_password and account.auth_method != "OAuth":
|
||||
account.awaiting_password = 0
|
||||
account.password = password
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -57,18 +57,16 @@
|
|||
],
|
||||
"icon": "fa fa-comment",
|
||||
"links": [],
|
||||
"modified": "2022-01-04 14:12:50.321633",
|
||||
"modified": "2023-01-02 03:56:48.437280",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Email Template",
|
||||
"naming_rule": "Set by user",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"read": 1,
|
||||
"role": "All",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
"role": "All"
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
|
|
@ -85,5 +83,6 @@
|
|||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -2,15 +2,8 @@ import base64
|
|||
from imaplib import IMAP4
|
||||
from poplib import POP3
|
||||
from smtplib import SMTP
|
||||
from urllib.parse import quote
|
||||
|
||||
import frappe
|
||||
from frappe.integrations.google_oauth import GoogleOAuth
|
||||
from frappe.utils.password import encrypt
|
||||
|
||||
|
||||
class OAuthenticationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Oauth:
|
||||
|
|
@ -20,46 +13,32 @@ class Oauth:
|
|||
email_account: str,
|
||||
email: str,
|
||||
access_token: str,
|
||||
refresh_token: str,
|
||||
service: str,
|
||||
mechanism: str = "XOAUTH2",
|
||||
) -> None:
|
||||
|
||||
self.email_account = email_account
|
||||
self.email = email
|
||||
self.service = service
|
||||
self._mechanism = mechanism
|
||||
self._conn = conn
|
||||
self._access_token = access_token
|
||||
self._refresh_token = refresh_token
|
||||
|
||||
self._validate()
|
||||
|
||||
def _validate(self) -> None:
|
||||
if self.service != "GMail":
|
||||
raise NotImplementedError(
|
||||
f"Service {self.service} currently doesn't have oauth implementation."
|
||||
)
|
||||
|
||||
if not self._refresh_token:
|
||||
if not self._access_token:
|
||||
frappe.throw(
|
||||
frappe._("Please Authorize OAuth."),
|
||||
OAuthenticationError,
|
||||
frappe._("OAuth Error"),
|
||||
frappe._("Please Authorize OAuth for Email Account {}").format(self.email_account),
|
||||
title=frappe._("OAuth Error"),
|
||||
)
|
||||
|
||||
@property
|
||||
def _auth_string(self) -> str:
|
||||
return f"user={self.email}\1auth=Bearer {self._access_token}\1\1"
|
||||
|
||||
def connect(self, _retry: int = 0) -> None:
|
||||
"""Connection method with retry on exception for Oauth"""
|
||||
def connect(self) -> None:
|
||||
try:
|
||||
if isinstance(self._conn, POP3):
|
||||
res = self._connect_pop()
|
||||
|
||||
if not res.startswith(b"+OK"):
|
||||
raise
|
||||
self._connect_pop()
|
||||
|
||||
elif isinstance(self._conn, IMAP4):
|
||||
self._connect_imap()
|
||||
|
|
@ -68,100 +47,29 @@ class Oauth:
|
|||
# SMTP
|
||||
self._connect_smtp()
|
||||
|
||||
except Exception as e:
|
||||
# maybe the access token expired - refreshing
|
||||
access_token = self._refresh_access_token()
|
||||
except Exception:
|
||||
frappe.log_error(
|
||||
"Email Connection Error - Authentication Failed",
|
||||
reference_doctype="Email Account",
|
||||
reference_name=self.email_account,
|
||||
)
|
||||
# raising a bare exception here as we have a lot of exception handling present
|
||||
# where the connect method is called from - hence just logging and raising.
|
||||
raise
|
||||
|
||||
if not access_token or _retry > 0:
|
||||
frappe.log_error(
|
||||
"OAuth Error - Authentication Failed", str(e), "Email Account", self.email_account
|
||||
)
|
||||
# raising a bare exception here as we have a lot of exception handling present
|
||||
# where the connect method is called from - hence just logging and raising.
|
||||
raise
|
||||
|
||||
self._access_token = access_token
|
||||
self.connect(_retry + 1)
|
||||
|
||||
def _connect_pop(self) -> bytes:
|
||||
# poplib doesn't have AUTH command implementation
|
||||
def _connect_pop(self) -> None:
|
||||
# NOTE: poplib doesn't have AUTH command implementation
|
||||
res = self._conn._shortcmd(
|
||||
"AUTH {} {}".format(
|
||||
self._mechanism, base64.b64encode(bytes(self._auth_string, "utf-8")).decode("utf-8")
|
||||
)
|
||||
)
|
||||
|
||||
return res
|
||||
if not res.startswith(b"+OK"):
|
||||
raise
|
||||
|
||||
def _connect_imap(self) -> None:
|
||||
self._conn.authenticate(self._mechanism, lambda x: self._auth_string)
|
||||
|
||||
def _connect_smtp(self) -> None:
|
||||
self._conn.auth(self._mechanism, lambda x: self._auth_string, initial_response_ok=False)
|
||||
|
||||
def _refresh_access_token(self) -> str:
|
||||
"""Refreshes access token via calling `refresh_access_token` method of oauth service object"""
|
||||
service_obj = self._get_service_object()
|
||||
access_token = service_obj.refresh_access_token(self._refresh_token).get("access_token")
|
||||
|
||||
if access_token:
|
||||
# set the new access token in db
|
||||
frappe.db.set_value(
|
||||
"Email Account",
|
||||
self.email_account,
|
||||
"access_token",
|
||||
encrypt(access_token),
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
return access_token
|
||||
|
||||
def _get_service_object(self):
|
||||
"""Get Oauth service object"""
|
||||
|
||||
return {
|
||||
"GMail": GoogleOAuth("mail", validate=False),
|
||||
}[self.service]
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
def oauth_access(email_account: str, service: str):
|
||||
"""Used as a default endpoint/caller for all oauth services.
|
||||
Returns authorization url for redirection"""
|
||||
|
||||
if not service:
|
||||
frappe.throw(frappe._("No Service is selected. Please select one and try again!"))
|
||||
|
||||
if service == "GMail":
|
||||
return authorize_google_access(email_account)
|
||||
|
||||
raise NotImplementedError(f"Service {service} currently doesn't have oauth implementation.")
|
||||
|
||||
|
||||
def authorize_google_access(email_account: str, code: str = None):
|
||||
"""Facilitates google oauth for email.
|
||||
This is invoked 2 times - first time when user clicks `Authorize API Access` for getting the authorization url
|
||||
and second time for setting the refresh and access token in db when google redirects back with oauth code."""
|
||||
|
||||
doctype = "Email Account"
|
||||
oauth_obj = GoogleOAuth("mail")
|
||||
|
||||
if not code:
|
||||
return oauth_obj.get_authentication_url(
|
||||
{
|
||||
"redirect": f"/app/Form/{quote(doctype)}/{quote(email_account)}",
|
||||
"success_query_param": "successful_authorization=1",
|
||||
"email_account": email_account,
|
||||
},
|
||||
)
|
||||
|
||||
res = oauth_obj.authorize(code)
|
||||
frappe.db.set_value(
|
||||
doctype,
|
||||
email_account,
|
||||
{
|
||||
"refresh_token": encrypt(res.get("refresh_token")),
|
||||
"access_token": encrypt(res.get("access_token")),
|
||||
},
|
||||
update_modified=False,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -109,8 +109,6 @@ class EmailServer:
|
|||
self.settings.email_account,
|
||||
self.settings.username,
|
||||
self.settings.access_token,
|
||||
self.settings.refresh_token,
|
||||
self.settings.service,
|
||||
).connect()
|
||||
|
||||
else:
|
||||
|
|
@ -142,8 +140,6 @@ class EmailServer:
|
|||
self.settings.email_account,
|
||||
self.settings.username,
|
||||
self.settings.access_token,
|
||||
self.settings.refresh_token,
|
||||
self.settings.service,
|
||||
).connect()
|
||||
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -54,9 +54,7 @@ class SMTPServer:
|
|||
use_tls=None,
|
||||
use_ssl=None,
|
||||
use_oauth=0,
|
||||
refresh_token=None,
|
||||
access_token=None,
|
||||
service=None,
|
||||
):
|
||||
self.login = login
|
||||
self.email_account = email_account
|
||||
|
|
@ -66,9 +64,7 @@ class SMTPServer:
|
|||
self.use_tls = use_tls
|
||||
self.use_ssl = use_ssl
|
||||
self.use_oauth = use_oauth
|
||||
self.refresh_token = refresh_token
|
||||
self.access_token = access_token
|
||||
self.service = service
|
||||
self._session = None
|
||||
|
||||
if not self.server:
|
||||
|
|
@ -112,9 +108,7 @@ class SMTPServer:
|
|||
self.secure_session(_session)
|
||||
|
||||
if self.use_oauth:
|
||||
Oauth(
|
||||
_session, self.email_account, self.login, self.access_token, self.refresh_token, self.service
|
||||
).connect()
|
||||
Oauth(_session, self.email_account, self.login, self.access_token).connect()
|
||||
|
||||
elif self.password:
|
||||
res = _session.login(str(self.login or ""), str(self.password or ""))
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import json
|
|||
|
||||
# all country info
|
||||
import os
|
||||
from functools import lru_cache
|
||||
|
||||
import frappe
|
||||
from frappe.utils.momentjs import get_all_timezones
|
||||
|
|
@ -27,8 +28,13 @@ def get_all():
|
|||
return all_data
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_country_timezone_info():
|
||||
return _get_country_timezone_info()
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
def _get_country_timezone_info():
|
||||
return {"country_info": get_all(), "all_timezones": get_all_timezones()}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -354,6 +354,7 @@ global_search_doctypes = {
|
|||
}
|
||||
|
||||
override_whitelisted_methods = {
|
||||
# Legacy File APIs
|
||||
"frappe.core.doctype.file.file.download_file": "download_file",
|
||||
"frappe.core.doctype.file.file.unzip_file": "frappe.core.api.file.unzip_file",
|
||||
"frappe.core.doctype.file.file.get_attached_images": "frappe.core.api.file.get_attached_images",
|
||||
|
|
@ -363,6 +364,14 @@ override_whitelisted_methods = {
|
|||
"frappe.core.doctype.file.file.create_new_folder": "frappe.core.api.file.create_new_folder",
|
||||
"frappe.core.doctype.file.file.move_file": "frappe.core.api.file.move_file",
|
||||
"frappe.core.doctype.file.file.zip_files": "frappe.core.api.file.zip_files",
|
||||
# Legacy (& Consistency) OAuth2 APIs
|
||||
"frappe.www.login.login_via_google": "frappe.integrations.oauth2_logins.login_via_google",
|
||||
"frappe.www.login.login_via_github": "frappe.integrations.oauth2_logins.login_via_github",
|
||||
"frappe.www.login.login_via_facebook": "frappe.integrations.oauth2_logins.login_via_facebook",
|
||||
"frappe.www.login.login_via_frappe": "frappe.integrations.oauth2_logins.login_via_frappe",
|
||||
"frappe.www.login.login_via_office365": "frappe.integrations.oauth2_logins.login_via_office365",
|
||||
"frappe.www.login.login_via_salesforce": "frappe.integrations.oauth2_logins.login_via_salesforce",
|
||||
"frappe.www.login.login_via_fairlogin": "frappe.integrations.oauth2_logins.login_via_fairlogin",
|
||||
}
|
||||
|
||||
ignore_links_on_delete = [
|
||||
|
|
@ -381,4 +390,5 @@ ignore_links_on_delete = [
|
|||
"Email Queue",
|
||||
"Document Share Key",
|
||||
"Integration Request",
|
||||
"Unhandled Email",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ if any((os.getenv("CI"), frappe.conf.developer_mode, frappe.conf.allow_tests)):
|
|||
# Disable mandatory TLS in developer mode and tests
|
||||
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
|
||||
|
||||
os.environ["OAUTHLIB_RELAX_TOKEN_SCOPE"] = "1"
|
||||
|
||||
|
||||
class ConnectedApp(Document):
|
||||
"""Connect to a remote oAuth Server. Retrieve and store user's access token
|
||||
|
|
@ -57,7 +59,7 @@ class ConnectedApp(Document):
|
|||
def initiate_web_application_flow(self, user=None, success_uri=None):
|
||||
"""Return an authorization URL for the user. Save state in Token Cache."""
|
||||
user = user or frappe.session.user
|
||||
oauth = self.get_oauth2_session(init=True)
|
||||
oauth = self.get_oauth2_session(user, init=True)
|
||||
query_params = self.get_query_params()
|
||||
authorization_url, state = oauth.authorization_url(self.authorization_uri, **query_params)
|
||||
token_cache = self.get_token_cache(user)
|
||||
|
|
@ -102,8 +104,27 @@ class ConnectedApp(Document):
|
|||
def get_query_params(self):
|
||||
return {param.key: param.value for param in self.query_parameters}
|
||||
|
||||
def get_active_token(self, user=None):
|
||||
user = user or frappe.session.user
|
||||
token_cache = self.get_token_cache(user)
|
||||
if token_cache and token_cache.is_expired():
|
||||
oauth_session = self.get_oauth2_session(user)
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
try:
|
||||
token = oauth_session.refresh_token(
|
||||
body=f"redirect_uri={self.redirect_uri}",
|
||||
token_url=self.token_uri,
|
||||
)
|
||||
except Exception:
|
||||
self.log_error("Token Refresh Error")
|
||||
return None
|
||||
|
||||
token_cache.update_data(token)
|
||||
|
||||
return token_cache
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["GET"], allow_guest=True)
|
||||
def callback(code=None, state=None):
|
||||
"""Handle client's code.
|
||||
|
||||
|
|
@ -111,8 +132,6 @@ def callback(code=None, state=None):
|
|||
transmit a code that can be used by the local server to obtain an access
|
||||
token.
|
||||
"""
|
||||
if frappe.request.method != "GET":
|
||||
frappe.throw(_("Invalid request method: {}").format(frappe.request.method))
|
||||
|
||||
if frappe.session.user == "Guest":
|
||||
frappe.local.response["type"] = "redirect"
|
||||
|
|
@ -136,9 +155,16 @@ def callback(code=None, state=None):
|
|||
code=code,
|
||||
client_secret=connected_app.get_password("client_secret"),
|
||||
include_client_id=True,
|
||||
**query_params
|
||||
**query_params,
|
||||
)
|
||||
token_cache.update_data(token)
|
||||
|
||||
frappe.local.response["type"] = "redirect"
|
||||
frappe.local.response["location"] = token_cache.get("success_uri") or connected_app.get_url()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def has_token(connected_app, connected_user=None):
|
||||
app = frappe.get_doc("Connected App", connected_app)
|
||||
token_cache = app.get_token_cache(connected_user or frappe.session.user)
|
||||
return bool(token_cache and token_cache.get_password("access_token", False))
|
||||
|
|
|
|||
|
|
@ -393,7 +393,7 @@ def dropbox_auth_finish(return_access_token=False):
|
|||
|
||||
|
||||
def set_dropbox_access_token(access_token):
|
||||
frappe.db.set_value("Dropbox Settings", None, "dropbox_access_token", access_token)
|
||||
frappe.db.set_single_value("Dropbox Settings", "dropbox_access_token", access_token)
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ def authorize_access(reauthorize=False, code=None):
|
|||
|
||||
if not oauth_code or reauthorize:
|
||||
if reauthorize:
|
||||
frappe.db.set_value("Google Drive", None, "backup_folder_id", "")
|
||||
frappe.db.set_single_value("Google Drive", "backup_folder_id", "")
|
||||
return oauth_obj.get_authentication_url(
|
||||
{
|
||||
"redirect": f"/app/Form/{quote('Google Drive')}",
|
||||
|
|
@ -62,8 +62,7 @@ def authorize_access(reauthorize=False, code=None):
|
|||
)
|
||||
|
||||
r = oauth_obj.authorize(oauth_code)
|
||||
frappe.db.set_value(
|
||||
"Google Drive",
|
||||
frappe.db.set_single_value(
|
||||
"Google Drive",
|
||||
{"authorization_code": oauth_code, "refresh_token": r.get("refresh_token")},
|
||||
)
|
||||
|
|
@ -95,7 +94,7 @@ def check_for_folder_in_google_drive():
|
|||
|
||||
try:
|
||||
folder = google_drive.files().create(body=file_metadata, fields="id").execute()
|
||||
frappe.db.set_value("Google Drive", None, "backup_folder_id", folder.get("id"))
|
||||
frappe.db.set_single_value("Google Drive", "backup_folder_id", folder.get("id"))
|
||||
frappe.db.commit()
|
||||
except HttpError as e:
|
||||
frappe.throw(
|
||||
|
|
@ -120,7 +119,7 @@ def check_for_folder_in_google_drive():
|
|||
|
||||
for f in google_drive_folders.get("files"):
|
||||
if f.get("name") == account.backup_folder_name:
|
||||
frappe.db.set_value("Google Drive", None, "backup_folder_id", f.get("id"))
|
||||
frappe.db.set_single_value("Google Drive", "backup_folder_id", f.get("id"))
|
||||
frappe.db.commit()
|
||||
backup_folder_exists = True
|
||||
break
|
||||
|
|
@ -170,7 +169,7 @@ def upload_system_backup_to_google_drive():
|
|||
if not fileurl:
|
||||
continue
|
||||
|
||||
file_metadata = {"name": fileurl, "parents": [account.backup_folder_id]}
|
||||
file_metadata = {"name": os.path.basename(fileurl), "parents": [account.backup_folder_id]}
|
||||
|
||||
try:
|
||||
media = MediaFileUpload(
|
||||
|
|
@ -186,7 +185,7 @@ def upload_system_backup_to_google_drive():
|
|||
send_email(False, "Google Drive", "Google Drive", "email", error_status=e)
|
||||
|
||||
set_progress(3, "Uploading successful.")
|
||||
frappe.db.set_value("Google Drive", None, "last_backup_on", frappe.utils.now_datetime())
|
||||
frappe.db.set_single_value("Google Drive", "last_backup_on", frappe.utils.now_datetime())
|
||||
send_email(True, "Google Drive", "Google Drive", "email")
|
||||
return _("Google Drive Backup Successful.")
|
||||
|
||||
|
|
|
|||
|
|
@ -17,24 +17,24 @@ class TestGoogleSettings(FrappeTestCase):
|
|||
|
||||
def test_picker_disabled(self):
|
||||
"""Google Drive Picker should be disabled if it is not enabled in Google Settings."""
|
||||
frappe.db.set_value("Google Settings", None, "enable", 1)
|
||||
frappe.db.set_value("Google Settings", None, "google_drive_picker_enabled", 0)
|
||||
frappe.db.set_single_value("Google Settings", "enable", 1)
|
||||
frappe.db.set_single_value("Google Settings", "google_drive_picker_enabled", 0)
|
||||
settings = get_file_picker_settings()
|
||||
|
||||
self.assertEqual(settings, {})
|
||||
|
||||
def test_google_disabled(self):
|
||||
"""Google Drive Picker should be disabled if Google integration is not enabled."""
|
||||
frappe.db.set_value("Google Settings", None, "enable", 0)
|
||||
frappe.db.set_value("Google Settings", None, "google_drive_picker_enabled", 1)
|
||||
frappe.db.set_single_value("Google Settings", "enable", 0)
|
||||
frappe.db.set_single_value("Google Settings", "google_drive_picker_enabled", 1)
|
||||
settings = get_file_picker_settings()
|
||||
|
||||
self.assertEqual(settings, {})
|
||||
|
||||
def test_picker_enabled(self):
|
||||
"""If picker is enabled, get_file_picker_settings should return the credentials."""
|
||||
frappe.db.set_value("Google Settings", None, "enable", 1)
|
||||
frappe.db.set_value("Google Settings", None, "google_drive_picker_enabled", 1)
|
||||
frappe.db.set_single_value("Google Settings", "enable", 1)
|
||||
frappe.db.set_single_value("Google Settings", "google_drive_picker_enabled", 1)
|
||||
settings = get_file_picker_settings()
|
||||
|
||||
self.assertEqual(True, settings.get("enabled", False))
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@
|
|||
},
|
||||
{
|
||||
"default": "https://s3.amazonaws.com",
|
||||
"description": "Only change this if you want to use other S3 compatible object storage backends.",
|
||||
"fieldname": "endpoint_url",
|
||||
"fieldtype": "Data",
|
||||
"label": "Endpoint URL"
|
||||
|
|
@ -129,7 +130,7 @@
|
|||
"hide_toolbar": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-07 15:30:55.047689",
|
||||
"modified": "2023-01-11 15:38:20.333833",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "S3 Backup Settings",
|
||||
|
|
@ -149,5 +150,6 @@
|
|||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -86,10 +86,11 @@
|
|||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-11-13 13:35:53.714352",
|
||||
"modified": "2023-01-01 21:01:24.405729",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "Token Cache",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
|
@ -106,5 +107,5 @@
|
|||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
"states": []
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytz
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
|
@ -50,16 +52,18 @@ class TokenCache(Document):
|
|||
return self
|
||||
|
||||
def get_expires_in(self):
|
||||
expiry_time = frappe.utils.get_datetime(self.modified) + timedelta(self.expires_in)
|
||||
return (datetime.now() - expiry_time).total_seconds()
|
||||
modified = frappe.utils.get_datetime(self.modified)
|
||||
expiry_utc = modified.astimezone(pytz.utc) + timedelta(seconds=self.expires_in)
|
||||
now_utc = datetime.utcnow().replace(tzinfo=pytz.utc)
|
||||
return cint((expiry_utc - now_utc).total_seconds())
|
||||
|
||||
def is_expired(self):
|
||||
return self.get_expires_in() < 0
|
||||
|
||||
def get_json(self):
|
||||
return {
|
||||
"access_token": self.get_password("access_token", ""),
|
||||
"refresh_token": self.get_password("refresh_token", ""),
|
||||
"access_token": self.get_password("access_token", False),
|
||||
"refresh_token": self.get_password("refresh_token", False),
|
||||
"expires_in": self.get_expires_in(),
|
||||
"token_type": self.token_type,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ def enqueue_webhook(doc, webhook) -> None:
|
|||
webhook: Webhook = frappe.get_doc("Webhook", webhook.get("name"))
|
||||
headers = get_webhook_headers(doc, webhook)
|
||||
data = get_webhook_data(doc, webhook)
|
||||
r = None
|
||||
|
||||
for i in range(3):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import json
|
||||
|
|
@ -9,42 +9,42 @@ from frappe.utils.oauth import login_via_oauth2, login_via_oauth2_id_token
|
|||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def login_via_google(code, state):
|
||||
def login_via_google(code: str, state: str):
|
||||
login_via_oauth2("google", code, state, decoder=decoder_compat)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def login_via_github(code, state):
|
||||
def login_via_github(code: str, state: str):
|
||||
login_via_oauth2("github", code, state)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def login_via_facebook(code, state):
|
||||
def login_via_facebook(code: str, state: str):
|
||||
login_via_oauth2("facebook", code, state, decoder=decoder_compat)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def login_via_frappe(code, state):
|
||||
def login_via_frappe(code: str, state: str):
|
||||
login_via_oauth2("frappe", code, state, decoder=decoder_compat)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def login_via_office365(code, state):
|
||||
def login_via_office365(code: str, state: str):
|
||||
login_via_oauth2_id_token("office_365", code, state, decoder=decoder_compat)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def login_via_salesforce(code, state):
|
||||
def login_via_salesforce(code: str, state: str):
|
||||
login_via_oauth2("salesforce", code, state, decoder=decoder_compat)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def login_via_fairlogin(code, state):
|
||||
def login_via_fairlogin(code: str, state: str):
|
||||
login_via_oauth2("fairlogin", code, state, decoder=decoder_compat)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def custom(code, state):
|
||||
def custom(code: str, state: str):
|
||||
"""
|
||||
Callback for processing code and state for user added providers
|
||||
|
||||
|
|
|
|||
|
|
@ -975,8 +975,14 @@ class BaseDocument:
|
|||
)
|
||||
if self_value != db_value:
|
||||
frappe.throw(
|
||||
_("Not allowed to change {0} after submission").format(df.label),
|
||||
_("{0} Not allowed to change {1} after submission from {2} to {3}").format(
|
||||
f"Row #{self.idx}:" if self.get("parent") else "",
|
||||
frappe.bold(_(df.label)),
|
||||
frappe.bold(db_value),
|
||||
frappe.bold(self_value),
|
||||
),
|
||||
frappe.UpdateAfterSubmitError,
|
||||
title=_("Cannot Update After Submit"),
|
||||
)
|
||||
|
||||
def _sanitize_content(self):
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import frappe.defaults
|
|||
import frappe.model.meta
|
||||
from frappe import _, get_module_path
|
||||
from frappe.desk.doctype.tag.tag import delete_tags_for_document
|
||||
from frappe.model.docstatus import DocStatus
|
||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
from frappe.model.naming import revert_series_if_last
|
||||
from frappe.model.utils import is_virtual_doctype
|
||||
|
|
@ -265,7 +266,7 @@ def check_if_doc_is_linked(doc, method="Delete"):
|
|||
# don't check for communication and todo!
|
||||
continue
|
||||
|
||||
if method != "Delete" and (method != "Cancel" or item.docstatus != 1):
|
||||
if method != "Delete" and (method != "Cancel" or not DocStatus(item.docstatus).is_submitted()):
|
||||
# don't raise exception if not
|
||||
# linked to a non-cancelled doc when deleting or to a submitted doc when cancelling
|
||||
continue
|
||||
|
|
@ -302,13 +303,12 @@ def check_if_doc_is_dynamically_linked(doc, method="Delete"):
|
|||
refdoc.get(df.options) == doc.doctype
|
||||
and refdoc.get(df.fieldname) == doc.name
|
||||
and (
|
||||
(method == "Delete" and refdoc.docstatus < 2)
|
||||
or (method == "Cancel" and refdoc.docstatus == 1)
|
||||
# linked to an non-cancelled doc when deleting
|
||||
(method == "Delete" and not DocStatus(refdoc.docstatus).is_cancelled())
|
||||
# linked to a submitted doc when cancelling
|
||||
or (method == "Cancel" and DocStatus(refdoc.docstatus).is_submitted())
|
||||
)
|
||||
):
|
||||
# raise exception only if
|
||||
# linked to an non-cancelled doc when deleting
|
||||
# or linked to a submitted doc when cancelling
|
||||
raise_link_exists_exception(doc, df.parent, df.parent)
|
||||
else:
|
||||
# dynamic link in table
|
||||
|
|
@ -321,14 +321,11 @@ def check_if_doc_is_dynamically_linked(doc, method="Delete"):
|
|||
(doc.doctype, doc.name),
|
||||
as_dict=True,
|
||||
):
|
||||
|
||||
if (method == "Delete" and refdoc.docstatus < 2) or (
|
||||
method == "Cancel" and refdoc.docstatus == 1
|
||||
# linked to an non-cancelled doc when deleting
|
||||
# or linked to a submitted doc when cancelling
|
||||
if (method == "Delete" and not DocStatus(refdoc.docstatus).is_cancelled()) or (
|
||||
method == "Cancel" and DocStatus(refdoc.docstatus).is_submitted()
|
||||
):
|
||||
# raise exception only if
|
||||
# linked to an non-cancelled doc when deleting
|
||||
# or linked to a submitted doc when cancelling
|
||||
|
||||
reference_doctype = refdoc.parenttype if meta.istable else df.parent
|
||||
reference_docname = refdoc.parent if meta.istable else refdoc.name
|
||||
at_position = f"at Row: {refdoc.idx}" if meta.istable else ""
|
||||
|
|
|
|||
|
|
@ -6,58 +6,5 @@
|
|||
import frappe
|
||||
|
||||
|
||||
def rename(doctype, fieldname, newname):
|
||||
"""rename docfield"""
|
||||
df = frappe.db.sql(
|
||||
"""select * from tabDocField where parent=%s and fieldname=%s""", (doctype, fieldname), as_dict=1
|
||||
)
|
||||
if not df:
|
||||
return
|
||||
|
||||
df = df[0]
|
||||
|
||||
if frappe.db.get_value("DocType", doctype, "issingle"):
|
||||
update_single(df, newname)
|
||||
else:
|
||||
update_table(df, newname)
|
||||
update_parent_field(df, newname)
|
||||
|
||||
|
||||
def update_single(f, new):
|
||||
"""update in tabSingles"""
|
||||
frappe.db.begin()
|
||||
frappe.db.sql(
|
||||
"""update tabSingles set field=%s where doctype=%s and field=%s""",
|
||||
(new, f["parent"], f["fieldname"]),
|
||||
)
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
def update_table(f, new):
|
||||
"""update table"""
|
||||
query = get_change_column_query(f, new)
|
||||
if query:
|
||||
frappe.db.sql(query)
|
||||
|
||||
|
||||
def update_parent_field(f, new):
|
||||
"""update 'parentfield' in tables"""
|
||||
if f["fieldtype"] in frappe.model.table_fields:
|
||||
frappe.db.begin()
|
||||
frappe.db.sql(
|
||||
"""update `tab{}` set parentfield={} where parentfield={}""".format(f["options"], "%s", "%s"),
|
||||
(new, f["fieldname"]),
|
||||
)
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
def get_change_column_query(f, new):
|
||||
"""generate change fieldname query"""
|
||||
desc = frappe.db.sql("desc `tab%s`" % f["parent"])
|
||||
for d in desc:
|
||||
if d[0] == f["fieldname"]:
|
||||
return "alter table `tab{}` change `{}` `{}` {}".format(f["parent"], f["fieldname"], new, d[1])
|
||||
|
||||
|
||||
def supports_translation(fieldtype):
|
||||
return fieldtype in ["Data", "Select", "Text", "Small Text", "Text Editor"]
|
||||
|
|
|
|||
|
|
@ -178,6 +178,7 @@ class Document(BaseDocument):
|
|||
"*",
|
||||
as_dict=True,
|
||||
order_by="idx asc",
|
||||
for_update=self.flags.for_update,
|
||||
)
|
||||
or []
|
||||
)
|
||||
|
|
|
|||
|
|
@ -220,3 +220,4 @@ frappe.patches.v14_0.add_manage_subscriptions_in_navbar_settings
|
|||
frappe.patches.v14_0.update_attachment_comment
|
||||
frappe.patches.v15_0.set_contact_full_name
|
||||
execute:frappe.delete_doc("Page", "activity", force=1)
|
||||
frappe.patches.v14_0.disable_email_accounts_with_oauth
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ import frappe
|
|||
|
||||
def execute():
|
||||
frappe.reload_doc("core", "doctype", "system_settings")
|
||||
frappe.db.set_value("System Settings", None, "allow_login_after_fail", 60)
|
||||
frappe.db.set_single_value("System Settings", "allow_login_after_fail", 60)
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ def execute():
|
|||
frappe.reload_doctype("Dropbox Settings")
|
||||
check_dropbox_enabled = cint(frappe.db.get_single_value("Dropbox Settings", "enabled"))
|
||||
if check_dropbox_enabled == 1:
|
||||
frappe.db.set_value("Dropbox Settings", None, "file_backup", 1)
|
||||
frappe.db.set_single_value("Dropbox Settings", "file_backup", 1)
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ import frappe
|
|||
|
||||
def execute():
|
||||
frappe.reload_doc("core", "doctype", "system_settings", force=1)
|
||||
frappe.db.set_value("System Settings", None, "password_reset_limit", 3)
|
||||
frappe.db.set_single_value("System Settings", "password_reset_limit", 3)
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@ def execute():
|
|||
frappe.reload_doctype("System Settings")
|
||||
# setting first_day_of_the_week value as "Monday" to avoid breaking change
|
||||
# because before the configuration was introduced, system used to consider "Monday" as start of the week
|
||||
frappe.db.set_value("System Settings", "System Settings", "first_day_of_the_week", "Monday")
|
||||
frappe.db.set_single_value("System Settings", "first_day_of_the_week", "Monday")
|
||||
|
|
|
|||
36
frappe/patches/v14_0/disable_email_accounts_with_oauth.py
Normal file
36
frappe/patches/v14_0/disable_email_accounts_with_oauth.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import frappe
|
||||
from frappe.desk.doctype.notification_log.notification_log import make_notification_logs
|
||||
|
||||
|
||||
def execute():
|
||||
if not frappe.get_value("Email Account", {"auth_method": "OAuth"}):
|
||||
return
|
||||
|
||||
# Setting awaiting password to 1 for email accounts where Oauth is enabled.
|
||||
# This is done so that people can resetup their email accounts with connected app mechanism.
|
||||
frappe.db.set_value("Email Account", {"auth_method": "OAuth"}, "awaiting_password", 1)
|
||||
|
||||
message = "Email Accounts with auth method as OAuth have been disabled.\
|
||||
Please re-setup your OAuth based email accounts with the connected app mechanism to re-enable them."
|
||||
|
||||
if sysmanagers := get_system_managers():
|
||||
make_notification_logs(
|
||||
{
|
||||
"type": "Alert",
|
||||
"subject": frappe._(message),
|
||||
},
|
||||
sysmanagers,
|
||||
)
|
||||
|
||||
|
||||
def get_system_managers():
|
||||
user_doctype = frappe.qb.DocType("User").as_("user")
|
||||
user_role_doctype = frappe.qb.DocType("Has Role").as_("user_role")
|
||||
return (
|
||||
frappe.qb.from_(user_doctype)
|
||||
.from_(user_role_doctype)
|
||||
.select(user_doctype.email)
|
||||
.where(user_role_doctype.role == "System Manager")
|
||||
.where(user_doctype.enabled == 1)
|
||||
.where(user_role_doctype.parent == user_doctype.name)
|
||||
).run(pluck=True)
|
||||
|
|
@ -2,8 +2,7 @@ import frappe
|
|||
|
||||
|
||||
def execute():
|
||||
frappe.db.set_value(
|
||||
"System Settings",
|
||||
frappe.db.set_single_value(
|
||||
"System Settings",
|
||||
{"document_share_key_expiry": 30, "allow_older_web_view_links": 1},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@ import frappe
|
|||
|
||||
def execute():
|
||||
days = frappe.db.get_single_value("Website Settings", "auto_account_deletion")
|
||||
frappe.db.set_value("Website Settings", None, "auto_account_deletion", days * 24)
|
||||
frappe.db.set_single_value("Website Settings", "auto_account_deletion", days * 24)
|
||||
|
|
|
|||
|
|
@ -27,28 +27,12 @@ rights = (
|
|||
)
|
||||
|
||||
|
||||
def check_admin_or_system_manager(user=None):
|
||||
from frappe.utils.commands import warn
|
||||
|
||||
warn(
|
||||
"The function check_admin_or_system_manager will be deprecated in version 15."
|
||||
'Please use frappe.only_for("System Manager") instead.',
|
||||
category=PendingDeprecationWarning,
|
||||
)
|
||||
|
||||
if not user:
|
||||
user = frappe.session.user
|
||||
|
||||
if ("System Manager" not in frappe.get_roles(user)) and (user != "Administrator"):
|
||||
frappe.throw(_("Not permitted"), frappe.PermissionError)
|
||||
|
||||
|
||||
def print_has_permission_check_logs(func):
|
||||
def inner(*args, **kwargs):
|
||||
frappe.flags["has_permission_check_logs"] = []
|
||||
result = func(*args, **kwargs)
|
||||
self_perm_check = True if not kwargs.get("user") else kwargs.get("user") == frappe.session.user
|
||||
raise_exception = False if kwargs.get("raise_exception") is False else True
|
||||
raise_exception = kwargs.get("raise_exception", True)
|
||||
|
||||
# print only if access denied
|
||||
# and if user is checking his own permission
|
||||
|
|
|
|||
|
|
@ -48,6 +48,16 @@
|
|||
<path d="M9 3L13 5.99999L9 9" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-unhide">
|
||||
<path stroke="none" fill-rule="evenodd" clip-rule="evenodd" d="M2.10756 9.53547C1.93501 9.82126 1.93501 10.1787 2.10756 10.4645C3.75635 13.1955 6.60531 15 9.84351 15C13.0817 15 15.9307 13.1955 17.5795 10.4645C17.752 10.1787 17.752 9.82127 17.5795 9.53548C15.9307 6.80451 13.0817 5 9.84351 5C6.60531 5 3.75635 6.8045 2.10756 9.53547ZM10 13C11.6569 13 13 11.6569 13 10C13 8.34315 11.6569 7 10 7C8.34315 7 7 8.34315 7 10C7 11.6569 8.34315 13 10 13Z" fill="var(--icon-stroke)"/>
|
||||
<circle cx="10" cy="10" r="1" stroke="none" fill="var(--icon-stroke)"/>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-hide">
|
||||
<rect stroke="none" x="3.02185" y="3.89151" width="1.26078" height="18.4481" rx="0.630391" transform="rotate(-45 3.02185 3.89151)" fill="var(--icon-stroke)"/>
|
||||
<path stroke="none" fill-rule="evenodd" clip-rule="evenodd" d="M5.02016 6.99831C4.84611 6.82426 4.57032 6.80165 4.37821 6.95554C3.49472 7.66323 2.73193 8.53749 2.12941 9.53547C1.95686 9.82126 1.95686 10.1787 2.12941 10.4645C3.7782 13.1955 6.62716 15 9.86536 15C10.5301 15 11.1784 14.924 11.8032 14.7795C12.1655 14.6957 12.2727 14.2508 12.0098 13.9879L11.1052 13.0833C10.9747 12.9529 10.7837 12.9083 10.6027 12.9438C10.4148 12.9807 10.2206 13 10.0219 13C8.365 13 7.02185 11.6569 7.02185 10C7.02185 9.80128 7.04117 9.60707 7.07804 9.41915C7.11355 9.23815 7.06896 9.04711 6.93853 8.91668L5.02016 6.99831ZM12.1967 12.8433C11.9793 12.6259 12.011 12.2666 12.2202 12.0414C12.7176 11.506 13.0219 10.7885 13.0219 10C13.0219 8.34315 11.6787 7 10.0219 7C9.23334 7 8.51587 7.30421 7.98043 7.80167C7.75522 8.0109 7.3959 8.04255 7.17854 7.82518L5.98518 6.63183C5.75274 6.39939 5.80413 6.00935 6.10001 5.86613C7.24996 5.3095 8.52428 5 9.86536 5C13.1036 5 15.9525 6.80451 17.6013 9.53548C17.7739 9.82127 17.7739 10.1787 17.6013 10.4645C16.6787 11.9927 15.3803 13.2307 13.8482 14.0249C13.6613 14.1218 13.4343 14.0809 13.2854 13.932L12.1967 12.8433Z" fill="var(--icon-stroke)"/>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-sidebar-collapse">
|
||||
<path d="M12 6L6 12L12 18" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18 6L12 12L18 18" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 111 KiB |
|
|
@ -1,6 +1,7 @@
|
|||
<script setup>
|
||||
import draggable from "vuedraggable";
|
||||
import Field from "./Field.vue";
|
||||
import EditableInput from "./EditableInput.vue";
|
||||
import { ref } from "vue";
|
||||
import { useStore } from "../store";
|
||||
import { move_children_to_parent } from "../utils";
|
||||
|
|
@ -55,6 +56,7 @@ function remove_column() {
|
|||
|
||||
// remove column
|
||||
columns.splice(index, 1);
|
||||
store.selected_field = null;
|
||||
}
|
||||
|
||||
function move_columns_to_section() {
|
||||
|
|
@ -74,25 +76,43 @@ function move_columns_to_section() {
|
|||
@mouseover.stop="hovered = true"
|
||||
@mouseout.stop="hovered = false"
|
||||
>
|
||||
<div class="column-actions" :hidden="store.read_only">
|
||||
<button
|
||||
v-if="section.columns.indexOf(column)"
|
||||
class="btn btn-xs btn-icon"
|
||||
:title="__('Move the current column & the following columns to a new section')"
|
||||
@click="move_columns_to_section"
|
||||
>
|
||||
<div v-html="frappe.utils.icon('move', 'sm')"></div>
|
||||
</button>
|
||||
<button class="btn btn-xs btn-icon" :title="__('Add Column')" @click="add_column">
|
||||
<div v-html="frappe.utils.icon('add', 'sm')"></div>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-xs btn-icon"
|
||||
:title="__('Remove Column')"
|
||||
@click="remove_column"
|
||||
>
|
||||
<div v-html="frappe.utils.icon('remove', 'sm')"></div>
|
||||
</button>
|
||||
<div
|
||||
:class="[
|
||||
'column-header',
|
||||
column.df.label ? 'has-label' : '',
|
||||
]"
|
||||
:hidden="!column.df.label && store.read_only"
|
||||
>
|
||||
<div class="column-label">
|
||||
<EditableInput
|
||||
:text="column.df.label"
|
||||
:placeholder="__('Column Title')"
|
||||
v-model="column.df.label"
|
||||
/>
|
||||
</div>
|
||||
<div class="column-actions">
|
||||
<button
|
||||
v-if="section.columns.indexOf(column)"
|
||||
class="btn btn-xs btn-icon"
|
||||
:title="__('Move the current column & the following columns to a new section')"
|
||||
@click="move_columns_to_section"
|
||||
>
|
||||
<div v-html="frappe.utils.icon('move', 'sm')"></div>
|
||||
</button>
|
||||
<button class="btn btn-xs btn-icon" :title="__('Add Column')" @click="add_column">
|
||||
<div v-html="frappe.utils.icon('add', 'sm')"></div>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-xs btn-icon"
|
||||
:title="__('Remove Column')"
|
||||
@click.stop="remove_column"
|
||||
>
|
||||
<div v-html="frappe.utils.icon('remove', 'sm')"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="column.df.description" class="column-description">
|
||||
{{ column.df.description }}
|
||||
</div>
|
||||
<draggable
|
||||
class="column-container"
|
||||
|
|
@ -140,7 +160,7 @@ function move_columns_to_section() {
|
|||
}
|
||||
|
||||
&.selected {
|
||||
.column-actions {
|
||||
.column-header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
|
@ -149,6 +169,48 @@ function move_columns_to_section() {
|
|||
}
|
||||
}
|
||||
|
||||
.column-header {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-left: 0.3rem;
|
||||
|
||||
&.has-label {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.column-label {
|
||||
:deep(span) {
|
||||
font-weight: 600;
|
||||
color: var(--heading-color);
|
||||
}
|
||||
}
|
||||
|
||||
.column-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.btn.btn-icon {
|
||||
padding: 2px;
|
||||
box-shadow: none;
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background-color: var(--fg-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.column-description {
|
||||
margin-bottom: 10px;
|
||||
margin-left: 0.3rem;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
|
@ -162,22 +224,5 @@ function move_columns_to_section() {
|
|||
min-height: 2rem;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.column-actions {
|
||||
display: none;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: 0.5rem;
|
||||
|
||||
.btn.btn-icon {
|
||||
padding: 2px;
|
||||
box-shadow: none;
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background-color: var(--fg-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -53,11 +53,11 @@ function focus_on_label() {
|
|||
@blur="editing = false"
|
||||
@click.stop
|
||||
/>
|
||||
<span v-else-if="text">{{ text }}</span>
|
||||
<span v-else-if="text" v-html="text" ></span>
|
||||
<i v-else class="text-muted">
|
||||
{{ empty_label }}
|
||||
</i>
|
||||
<span class="hidden-span" ref="hidden_text">{{ text }}</span>
|
||||
<span class="hidden-span" ref="hidden_text" v-html="text"></span>
|
||||
<span class="hidden-span" ref="hidden_placeholder">{{ placeholder }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import EditableInput from "./EditableInput.vue";
|
||||
import { ref, computed } from "vue";
|
||||
import { useStore } from "../store";
|
||||
import { move_children_to_parent } from "../utils";
|
||||
import { move_children_to_parent, clone_field } from "../utils";
|
||||
|
||||
let props = defineProps(["column", "field"]);
|
||||
let store = useStore();
|
||||
|
|
@ -19,6 +19,7 @@ function remove_field() {
|
|||
}
|
||||
let index = props.column.fields.indexOf(props.field);
|
||||
props.column.fields.splice(index, 1);
|
||||
store.selected_field = null;
|
||||
}
|
||||
|
||||
function move_fields_to_column() {
|
||||
|
|
@ -27,6 +28,20 @@ function move_fields_to_column() {
|
|||
);
|
||||
move_children_to_parent(props, "column", "field", current_section);
|
||||
}
|
||||
|
||||
function duplicate_field() {
|
||||
let duplicate_field = clone_field(props.field);
|
||||
|
||||
if (duplicate_field.df.label) {
|
||||
duplicate_field.df.label = duplicate_field.df.label + " Copy";
|
||||
}
|
||||
duplicate_field.df.fieldname = "";
|
||||
|
||||
// push duplicate_field after props.field in the same column
|
||||
let index = props.column.fields.indexOf(props.field);
|
||||
props.column.fields.splice(index + 1, 0, duplicate_field);
|
||||
store.selected_field = duplicate_field.df;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -48,13 +63,16 @@ function move_fields_to_column() {
|
|||
:data-fieldtype="field.df.fieldtype"
|
||||
>
|
||||
<template #label>
|
||||
<EditableInput
|
||||
:class="{ reqd: field.df.reqd }"
|
||||
:text="field.df.label"
|
||||
:placeholder="__('Label')"
|
||||
:empty_label="`${__('No Label')} (${field.df.fieldtype})`"
|
||||
v-model="field.df.label"
|
||||
/>
|
||||
<div class="field-label">
|
||||
<EditableInput
|
||||
:text="field.df.label"
|
||||
:placeholder="__('Label')"
|
||||
:empty_label="`${__('No Label')} (${field.df.fieldtype})`"
|
||||
v-model="field.df.label"
|
||||
/>
|
||||
<div class="reqd-asterisk" v-if="field.df.reqd">*</div>
|
||||
<div class="help-icon" v-if="field.df.documentation_url" v-html="frappe.utils.icon('help', 'sm')"></div>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<div class="field-actions" :hidden="store.read_only">
|
||||
|
|
@ -75,7 +93,10 @@ function move_fields_to_column() {
|
|||
>
|
||||
<div v-html="frappe.utils.icon('move', 'sm')"></div>
|
||||
</button>
|
||||
<button class="btn btn-xs btn-icon" @click="remove_field">
|
||||
<button class="btn btn-xs btn-icon" @click.stop="duplicate_field">
|
||||
<div v-html="frappe.utils.icon('duplicate', 'sm')"></div>
|
||||
</button>
|
||||
<button class="btn btn-xs btn-icon" @click.stop="remove_field">
|
||||
<div v-html="frappe.utils.icon('remove', 'sm')"></div>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -106,12 +127,30 @@ function move_fields_to_column() {
|
|||
}
|
||||
}
|
||||
|
||||
:deep(.form-control:read-only:focus) {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:deep(.field-controls) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.3rem;
|
||||
|
||||
.field-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.reqd-asterisk {
|
||||
margin-left: 3px;
|
||||
color: var(--red-400);
|
||||
}
|
||||
.help-icon {
|
||||
margin-left: 3px;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.field-actions {
|
||||
flex: none;
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ let docfield_df = computed(() => {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (df.fieldname === "reqd" && store.selected_field.fieldtype === "Check") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (df.fieldname === "options") {
|
||||
df.fieldtype = "Small Text";
|
||||
df.options = "";
|
||||
|
|
|
|||
|
|
@ -3,15 +3,11 @@ import SearchBox from "./SearchBox.vue";
|
|||
import draggable from "vuedraggable";
|
||||
import { ref, computed } from "vue";
|
||||
import { useStore } from "../store";
|
||||
import { clone_field } from "../utils";
|
||||
|
||||
let store = useStore();
|
||||
let search_text = ref("");
|
||||
|
||||
function clone_field(field) {
|
||||
field.df.name = frappe.utils.get_random(8);
|
||||
return JSON.parse(JSON.stringify(field));
|
||||
}
|
||||
|
||||
let fields = computed(() => {
|
||||
let fields = frappe.model.all_fieldtypes
|
||||
.filter(df => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import Sidebar from "./Sidebar.vue";
|
|||
import Tabs from "./Tabs.vue";
|
||||
import { computed, onMounted, watch, ref } from "vue";
|
||||
import { useStore } from "../store";
|
||||
import { onClickOutside } from "@vueuse/core";
|
||||
import { onClickOutside, useMagicKeys, whenever } from "@vueuse/core";
|
||||
|
||||
let store = useStore();
|
||||
|
||||
|
|
@ -14,6 +14,14 @@ let should_render = computed(() => {
|
|||
let container = ref(null);
|
||||
onClickOutside(container, () => store.selected_field = null);
|
||||
|
||||
// cmd/ctrl + s to save the form
|
||||
const { meta_s, ctrl_s } = useMagicKeys();
|
||||
whenever(() => meta_s.value || ctrl_s.value, () => {
|
||||
if (store.dirty) {
|
||||
store.save_changes();
|
||||
}
|
||||
});
|
||||
|
||||
function setup_change_doctype_dialog() {
|
||||
store.page.$title_area.on("click", () => {
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
|
|
@ -109,7 +117,7 @@ onMounted(() => {
|
|||
font-size: var(--text-sm);
|
||||
cursor: pointer;
|
||||
|
||||
&:has(.drop-it-here) {
|
||||
&:not(.hovered) {
|
||||
position: relative;
|
||||
background-color: transparent;
|
||||
height: 60px;
|
||||
|
|
@ -169,10 +177,6 @@ onMounted(() => {
|
|||
}
|
||||
}
|
||||
|
||||
.reqd::after {
|
||||
content: " *";
|
||||
color: var(--red-400);
|
||||
}
|
||||
.description,
|
||||
.time-zone {
|
||||
font-size: var(--text-sm);
|
||||
|
|
@ -210,6 +214,10 @@ onMounted(() => {
|
|||
}
|
||||
}
|
||||
|
||||
.section-description {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.section-columns {
|
||||
margin-top: 8px;
|
||||
|
||||
|
|
@ -219,6 +227,14 @@ onMounted(() => {
|
|||
padding-right: 15px;
|
||||
margin: 0;
|
||||
|
||||
.column-header {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.column-description {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin: 0;
|
||||
margin-bottom: 1rem;
|
||||
|
|
@ -252,7 +268,7 @@ onMounted(() => {
|
|||
}
|
||||
}
|
||||
|
||||
.form-main:not(:has(.tab-header)) :deep(.tab-contents) {
|
||||
.form-main > :deep(div:first-child:not(.tab-header)) {
|
||||
max-height: calc(100vh - 160px);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ function remove_section() {
|
|||
|
||||
// remove section
|
||||
sections.splice(index, 1);
|
||||
store.selected_field = null;
|
||||
}
|
||||
|
||||
function select_section() {
|
||||
|
|
@ -122,12 +123,13 @@ function move_sections_to_tab() {
|
|||
<button
|
||||
class="btn btn-xs btn-section"
|
||||
:title="__('Remove section')"
|
||||
@click="remove_section"
|
||||
@click.stop="remove_section"
|
||||
>
|
||||
<div v-html="frappe.utils.icon('remove', 'sm')"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="section.df.description" class="section-description">{{ section.df.description }}</div>
|
||||
<div class="section-columns" :class="{ hidden: section.df.collapsible && collapsed }">
|
||||
<draggable
|
||||
class="section-columns-container"
|
||||
|
|
@ -206,6 +208,7 @@ function move_sections_to_tab() {
|
|||
|
||||
:deep(span) {
|
||||
font-weight: 600;
|
||||
color: var(--heading-color);
|
||||
}
|
||||
|
||||
.collapse-indicator {
|
||||
|
|
@ -228,6 +231,12 @@ function move_sections_to_tab() {
|
|||
}
|
||||
}
|
||||
|
||||
.section-description {
|
||||
margin-bottom: 10px;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.section-columns-container {
|
||||
display: flex;
|
||||
min-height: 2rem;
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ if (props.df.fieldtype === "Icon") {
|
|||
type="text"
|
||||
:style="{ height: df.fieldtype == 'Table MultiSelect' ? '42px' : '' }"
|
||||
:placeholder="placeholder"
|
||||
disabled
|
||||
readonly
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
|
|
@ -58,7 +58,7 @@ if (props.df.fieldtype === "Icon") {
|
|||
class="mt-2 form-control"
|
||||
type="text"
|
||||
:style="{ height: '110px' }"
|
||||
disabled
|
||||
readonly
|
||||
/>
|
||||
|
||||
<!-- description -->
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ watch(
|
|||
</div>
|
||||
|
||||
<!-- link input -->
|
||||
<input class="form-control" type="text" disabled />
|
||||
<input class="form-control" type="text" readonly />
|
||||
|
||||
<!-- description -->
|
||||
<div v-if="df.description" class="mt-2 description" v-html="df.description" />
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ watch(() => props.df.options, () => {
|
|||
|
||||
<!-- select input -->
|
||||
<div class="select-input">
|
||||
<input class="form-control" disabled />
|
||||
<input class="form-control" readonly />
|
||||
<div class="select-icon" v-html="frappe.utils.icon('select', 'sm')"></div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,10 +8,13 @@ let store = useStore();
|
|||
let props = defineProps(["df", "value", "modelValue"]);
|
||||
let emit = defineEmits(["update:modelValue"]);
|
||||
let slots = useSlots();
|
||||
let height = "300px";
|
||||
if (props.df.fieldtype == "Small Text") {
|
||||
height = "150px";
|
||||
}
|
||||
|
||||
let height = computed(() => {
|
||||
if (props.df.fieldtype == "Small Text") {
|
||||
return "150px";
|
||||
}
|
||||
return "300px";
|
||||
});
|
||||
|
||||
let doctype = ref("");
|
||||
let fieldname = ref("");
|
||||
|
|
@ -110,7 +113,7 @@ watch([() => doctype.value, () => fieldname.value], ([doctype_value, fieldname_v
|
|||
:style="{ height: height, maxHeight: df.max_height ?? '' }"
|
||||
class="form-control"
|
||||
type="text"
|
||||
disabled
|
||||
readonly
|
||||
/>
|
||||
<textarea
|
||||
v-else
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ onMounted(() => {
|
|||
:deep(.quill) {
|
||||
.ql-toolbar {
|
||||
pointer-events: none;
|
||||
|
||||
.ql-formats {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
.ql-container p {
|
||||
cursor: pointer;
|
||||
|
|
|
|||
|
|
@ -45,18 +45,26 @@ class FormBuilder {
|
|||
this.store.read_only = this.store.preview;
|
||||
this.read_only = true;
|
||||
});
|
||||
this.customize_form_btn = this.page.add_button(__("For Customize Form"), () => {
|
||||
frappe.set_route("form-builder", this.doctype, "customize");
|
||||
});
|
||||
this.doctype_form_btn = this.page.add_button(__("For DocType Form"), () => {
|
||||
frappe.set_route("form-builder", this.doctype);
|
||||
});
|
||||
|
||||
this.reset_changes_btn = this.page.add_button(__("Reset Changes"), () => {
|
||||
this.store.reset_changes();
|
||||
});
|
||||
|
||||
this.go_to_doctype_btn = this.page.add_menu_item(__("Go to Doctype"), () =>
|
||||
this.go_to_doctype_list_btn = this.page.add_button(
|
||||
__("Go to {0} List", [__(this.doctype)]),
|
||||
() => {
|
||||
window.open(`/app/${frappe.router.slug(this.doctype)}`);
|
||||
}
|
||||
);
|
||||
|
||||
this.customize_form_btn = this.page.add_menu_item(__("Switch to Customize Form"), () => {
|
||||
frappe.set_route("form-builder", this.doctype, "customize");
|
||||
});
|
||||
this.doctype_form_btn = this.page.add_menu_item(__("Switch to DocType Form"), () => {
|
||||
frappe.set_route("form-builder", this.doctype);
|
||||
});
|
||||
|
||||
this.go_to_doctype_btn = this.page.add_menu_item(__("Go to DocType"), () =>
|
||||
frappe.set_route("Form", "DocType", this.doctype)
|
||||
);
|
||||
this.go_to_customize_form_btn = this.page.add_menu_item(__("Go to Customize Form"), () =>
|
||||
|
|
@ -121,9 +129,7 @@ class FormBuilder {
|
|||
? __("Go to {0}", [__(this.doctype)])
|
||||
: __("Go to {0} List", [__(this.doctype)]);
|
||||
|
||||
this.page.add_menu_item(label, () => {
|
||||
window.open(`/app/${frappe.router.slug(this.doctype)}`);
|
||||
});
|
||||
this.go_to_doctype_list_btn.text(label);
|
||||
}
|
||||
|
||||
// toggle preview btn text
|
||||
|
|
|
|||
|
|
@ -226,8 +226,13 @@ export const useStore = defineStore("form-builder-store", {
|
|||
}
|
||||
|
||||
section.columns.forEach((column, k) => {
|
||||
// do not consider first column
|
||||
if (k > 0 || column.fields.length == 0) {
|
||||
// do not consider first column if label is not set
|
||||
if (
|
||||
(k == 0 &&
|
||||
this.is_df_updated(column.df, this.get_df("Column Break"))) ||
|
||||
k > 0 ||
|
||||
column.fields.length == 0
|
||||
) {
|
||||
idx++;
|
||||
column.df.idx = idx;
|
||||
fields.push(column.df);
|
||||
|
|
|
|||
|
|
@ -279,7 +279,7 @@ export function scrub_field_names(fields) {
|
|||
if (d.fieldtype) {
|
||||
if (!d.fieldname) {
|
||||
if (d.label) {
|
||||
d.fieldname = d.label.trim().toLowerCase().replace(" ", "_");
|
||||
d.fieldname = d.label.trim().toLowerCase().replaceAll(" ", "_");
|
||||
if (d.fieldname.endsWith("?")) {
|
||||
d.fieldname = d.fieldname.slice(0, -1);
|
||||
}
|
||||
|
|
@ -295,7 +295,7 @@ export function scrub_field_names(fields) {
|
|||
}
|
||||
} else {
|
||||
d.fieldname =
|
||||
d.fieldtype.toLowerCase().replace(" ", "_") +
|
||||
d.fieldtype.toLowerCase().replaceAll(" ", "_") +
|
||||
"_" +
|
||||
frappe.utils.get_random(4);
|
||||
}
|
||||
|
|
@ -318,3 +318,9 @@ export function scrub_field_names(fields) {
|
|||
|
||||
return fields;
|
||||
}
|
||||
|
||||
export function clone_field(field) {
|
||||
let cloned_field = JSON.parse(JSON.stringify(field));
|
||||
cloned_field.df.name = frappe.utils.get_random(8);
|
||||
return cloned_field;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -168,7 +168,6 @@ frappe.Application = class Application {
|
|||
}
|
||||
|
||||
set_route() {
|
||||
frappe.flags.setting_original_route = true;
|
||||
if (frappe.boot && localStorage.getItem("session_last_route")) {
|
||||
frappe.set_route(localStorage.getItem("session_last_route"));
|
||||
localStorage.removeItem("session_last_route");
|
||||
|
|
@ -176,7 +175,6 @@ frappe.Application = class Application {
|
|||
// route to home page
|
||||
frappe.router.route();
|
||||
}
|
||||
frappe.after_ajax(() => (frappe.flags.setting_original_route = false));
|
||||
frappe.router.on("change", () => {
|
||||
$(".tooltip").hide();
|
||||
});
|
||||
|
|
@ -449,6 +447,7 @@ frappe.Application = class Application {
|
|||
}
|
||||
},
|
||||
});
|
||||
dialog.get_field("password").disable_password_checks();
|
||||
dialog.set_primary_action(__("Login"), () => {
|
||||
dialog.set_message(__("Authenticating..."));
|
||||
frappe.call({
|
||||
|
|
|
|||
|
|
@ -19,12 +19,20 @@ export default class Column {
|
|||
|
||||
this.form = this.wrapper.find("form").on("submit", () => false);
|
||||
|
||||
if (this.df.description) {
|
||||
$(`
|
||||
<p class="col-sm-12 form-column-description">
|
||||
${__(this.df.description)}
|
||||
</p>
|
||||
`).prependTo(this.wrapper);
|
||||
}
|
||||
|
||||
if (this.df.label) {
|
||||
$(`
|
||||
<label class="control-label">
|
||||
<label class="column-label">
|
||||
${__(this.df.label)}
|
||||
</label>
|
||||
`).appendTo(this.wrapper);
|
||||
`).prependTo(this.wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ frappe.ui.form.ControlPassword = class ControlPassword extends frappe.ui.form.Co
|
|||
static input_type = "password";
|
||||
make() {
|
||||
super.make();
|
||||
this.enable_password_checks = true;
|
||||
}
|
||||
make_input() {
|
||||
var me = this;
|
||||
|
|
@ -23,7 +24,15 @@ frappe.ui.form.ControlPassword = class ControlPassword extends frappe.ui.form.Co
|
|||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
disable_password_checks() {
|
||||
this.enable_password_checks = false;
|
||||
}
|
||||
|
||||
get_password_strength(value) {
|
||||
if (!this.enable_password_checks) {
|
||||
return;
|
||||
}
|
||||
var me = this;
|
||||
frappe.call({
|
||||
type: "POST",
|
||||
|
|
@ -32,13 +41,9 @@ frappe.ui.form.ControlPassword = class ControlPassword extends frappe.ui.form.Co
|
|||
new_password: value || "",
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message && r.message.entropy) {
|
||||
var score = r.message.score,
|
||||
feedback = r.message.feedback;
|
||||
|
||||
feedback.crack_time_display = r.message.crack_time_display;
|
||||
|
||||
var indicators = ["grey", "red", "orange", "yellow", "green"];
|
||||
if (r.message) {
|
||||
let score = r.message.score;
|
||||
var indicators = ["red", "red", "orange", "yellow", "green"];
|
||||
me.set_strength_indicator(indicators[score]);
|
||||
}
|
||||
},
|
||||
|
|
@ -47,6 +52,6 @@ frappe.ui.form.ControlPassword = class ControlPassword extends frappe.ui.form.Co
|
|||
set_strength_indicator(color) {
|
||||
var message = __("Include symbols, numbers and capital letters in the password");
|
||||
this.indicator.removeClass().addClass("password-strength-indicator indicator " + color);
|
||||
this.message.html(message).removeClass("hidden");
|
||||
this.message.html(message).toggleClass("hidden", color == "green");
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -407,7 +407,7 @@ export default class Grid {
|
|||
this.toggle_checkboxes(this.display_status !== "Read");
|
||||
|
||||
// sortable
|
||||
if (this.frm && this.is_sortable() && !this.sortable_setup_done) {
|
||||
if (this.is_sortable() && !this.sortable_setup_done) {
|
||||
this.make_sortable($rows);
|
||||
this.sortable_setup_done = true;
|
||||
}
|
||||
|
|
@ -553,17 +553,18 @@ export default class Grid {
|
|||
let idx = $(event.item).closest(".grid-row").attr("data-idx") - 1;
|
||||
let doc = this.data[idx % this.grid_pagination.page_length];
|
||||
this.renumber_based_on_dom();
|
||||
this.frm.script_manager.trigger(
|
||||
this.df.fieldname + "_move",
|
||||
this.df.options,
|
||||
doc.name
|
||||
);
|
||||
this.frm &&
|
||||
this.frm.script_manager.trigger(
|
||||
this.df.fieldname + "_move",
|
||||
this.df.options,
|
||||
doc.name
|
||||
);
|
||||
this.refresh();
|
||||
this.frm.dirty();
|
||||
this.frm && this.frm.dirty();
|
||||
},
|
||||
});
|
||||
|
||||
$(this.frm.wrapper).trigger("grid-make-sortable", [this.frm]);
|
||||
this.frm && $(this.frm.wrapper).trigger("grid-make-sortable", [this.frm]);
|
||||
}
|
||||
|
||||
get_data(filter_field) {
|
||||
|
|
@ -825,11 +826,11 @@ export default class Grid {
|
|||
let $item = $(item);
|
||||
let index =
|
||||
(this.grid_pagination.page_index - 1) * this.grid_pagination.page_length + i;
|
||||
let d = locals[this.doctype][$item.attr("data-name")];
|
||||
let d = this.grid_rows_by_docname[$item.attr("data-name")].doc;
|
||||
d.idx = index + 1;
|
||||
$item.attr("data-idx", d.idx);
|
||||
|
||||
this.frm.doc[this.df.fieldname][index] = d;
|
||||
if (this.frm) this.frm.doc[this.df.fieldname][index] = d;
|
||||
this.data[index] = d;
|
||||
this.grid_rows[index] = this.grid_rows_by_docname[d.name];
|
||||
});
|
||||
|
|
|
|||
|
|
@ -213,7 +213,12 @@ frappe.ui.form.ScriptManager = class ScriptManager {
|
|||
df.read_only == 1 ||
|
||||
df.is_virtual == 1;
|
||||
|
||||
if (is_read_only_field && df.fetch_from && df.fetch_from.indexOf(".") != -1) {
|
||||
if (
|
||||
is_read_only_field &&
|
||||
df.fetch_from &&
|
||||
(!df.fetch_if_empty || (df.fetch_if_empty && !me.frm.doc[df.fieldname])) &&
|
||||
df.fetch_from.indexOf(".") != -1
|
||||
) {
|
||||
var parts = df.fetch_from.split(".");
|
||||
me.frm.add_fetch(parts[0], parts[1], df.fieldname, df.parent);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export default class Tab {
|
|||
set_active() {
|
||||
this.tab_link.find(".nav-link").tab("show");
|
||||
this.wrapper.addClass("show");
|
||||
this.frm.active_tab = this;
|
||||
this.frm?.set_active_tab?.(this);
|
||||
}
|
||||
|
||||
is_active() {
|
||||
|
|
|
|||
|
|
@ -191,12 +191,16 @@ frappe.views.ListViewSelect = class ListViewSelect {
|
|||
);
|
||||
});
|
||||
|
||||
this.page.add_custom_menu_item(
|
||||
kanban_switcher,
|
||||
__("Create New Kanban Board"),
|
||||
() => frappe.views.KanbanView.show_kanban_dialog(this.doctype),
|
||||
true
|
||||
);
|
||||
let perms = this.list_view.board_perms;
|
||||
let can_create = perms ? perms.create : true;
|
||||
if (can_create) {
|
||||
this.page.add_custom_menu_item(
|
||||
kanban_switcher,
|
||||
__("Create New Kanban Board"),
|
||||
() => frappe.views.KanbanView.show_kanban_dialog(this.doctype),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get_page_name() {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue