Merge branch 'develop' of github.com:frappe/frappe into move-cypress

This commit is contained in:
Gavin D'souza 2020-10-12 17:32:16 +05:30
commit 40ee03c21d
310 changed files with 49958 additions and 9396 deletions

48
.github/helper/documentation.py vendored Normal file
View file

@ -0,0 +1,48 @@
import sys
import requests
from urllib.parse import urlparse
docs_repos = [
"frappe_docs",
"erpnext_documentation",
"erpnext_com",
"frappe_io",
]
def uri_validator(x):
result = urlparse(x)
return all([result.scheme, result.netloc, result.path])
def docs_link_exists(body):
for line in body.splitlines():
for word in line.split():
if word.startswith('http') and uri_validator(word):
parsed_url = urlparse(word)
if parsed_url.netloc == "github.com":
_, org, repo, _type, ref = parsed_url.path.split('/')
if org == "frappe" and repo in docs_repos:
return True
if __name__ == "__main__":
pr = sys.argv[1]
response = requests.get("https://api.github.com/repos/frappe/frappe/pulls/{}".format(pr))
if response.ok:
payload = response.json()
title = payload.get("title", "").lower()
head_sha = payload.get("head", {}).get("sha")
body = payload.get("body", "").lower()
if title.startswith("feat") and head_sha and "no-docs" not in body:
if docs_link_exists(body):
print("Documentation Link Found. You're Awesome! 🎉")
else:
print("Documentation Link Not Found! ⚠️")
sys.exit(1)
else:
print("Skipping documentation checks... 🏃")

View file

@ -3,7 +3,7 @@ import sys
errors_encounter = 0
pattern = re.compile(r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)")
start_pattern = re.compile(r"_{1,2}\([\"']{1,3}")
start_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}")
# skip first argument
files = sys.argv[1:]

24
.github/workflows/docs-checker.yml vendored Normal file
View file

@ -0,0 +1,24 @@
name: 'Documentation Required'
on:
pull_request:
types: [ opened, synchronize, reopened, edited ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: 'Setup Environment'
uses: actions/setup-python@v2
with:
python-version: 3.6
- name: 'Clone repo'
uses: actions/checkout@v2
- name: Validate Docs
env:
PR_NUMBER: ${{ github.event.number }}
run: |
pip install requests --quiet
python $GITHUB_WORKSPACE/.github/helper/documentation.py $PR_NUMBER

View file

@ -0,0 +1,43 @@
name: Build and Publish Assets for Development
on:
push:
branches: [ develop ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
path: 'frappe'
- uses: actions/setup-node@v1
with:
python-version: '12.x'
- uses: actions/setup-python@v2
with:
python-version: '3.6'
- name: Set up bench for current push
run: |
npm install -g yarn
pip3 install -U frappe-bench
bench init frappe-bench --no-procfile --no-backups --skip-assets --skip-redis-config-generation --python $(which python) --frappe-path $GITHUB_WORKSPACE/frappe
cd frappe-bench && bench build
- name: Package assets
run: |
mkdir -p $GITHUB_WORKSPACE/build
tar -cvpzf $GITHUB_WORKSPACE/build/$GITHUB_SHA.tar.gz ./frappe-bench/sites/assets/js ./frappe-bench/sites/assets/css
- name: Publish assets to S3
uses: jakejarvis/s3-sync-action@master
with:
args: --acl public-read
env:
AWS_S3_BUCKET: 'assets.frappeframework.com'
AWS_ACCESS_KEY_ID: ${{ secrets.S3_ASSETS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_ASSETS_SECRET_ACCESS_KEY }}
AWS_S3_ENDPOINT: 'http://s3.fr-par.scw.cloud'
AWS_REGION: 'fr-par'
SOURCE_DIR: '$GITHUB_WORKSPACE/build'

View file

@ -0,0 +1,47 @@
name: Build and Publish Assets built for Releases
on:
release:
types: [ created ]
env:
GITHUB_TOKEN: ${{ github.token }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
path: 'frappe'
- uses: actions/setup-node@v1
with:
python-version: '12.x'
- uses: actions/setup-python@v2
with:
python-version: '3.6'
- name: Set up bench for current push
run: |
npm install -g yarn
pip3 install -U frappe-bench
bench init frappe-bench --no-procfile --no-backups --skip-assets --skip-redis-config-generation --python $(which python) --frappe-path $GITHUB_WORKSPACE/frappe
cd frappe-bench && bench build
- name: Package assets
run: |
mkdir -p $GITHUB_WORKSPACE/build
tar -cvpzf $GITHUB_WORKSPACE/build/assets.tar.gz ./frappe-bench/sites/assets/js ./frappe-bench/sites/assets/css
- name: Get release
id: get_release
uses: bruceadams/get-release@v1.2.0
- name: Upload built Assets to Release
uses: actions/upload-release-asset@v1.0.2
with:
upload_url: ${{ steps.get_release.outputs.upload_url }}
asset_path: build/assets.tar.gz
asset_name: assets.tar.gz
asset_content_type: application/octet-stream

View file

@ -19,4 +19,4 @@ jobs:
run: |
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
python $GITHUB_WORKSPACE/.github/frappe_linter/translation.py $files
python $GITHUB_WORKSPACE/.github/helper/translation.py $files

34
.snyk
View file

@ -65,3 +65,37 @@ patch:
patched: '2020-04-30T23:02:32.330Z'
- quill-image-resize > lodash:
patched: '2020-08-24T23:06:37.710Z'
- node-sass > lodash:
patched: '2020-09-15T23:06:41.931Z'
- node-sass > sass-graph > lodash:
patched: '2020-09-15T23:06:41.931Z'
- node-sass > gaze > globule > lodash:
patched: '2020-09-15T23:06:41.931Z'
- snyk > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > @snyk/snyk-cocoapods-plugin > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-cpp-plugin > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-go-plugin > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-gradle-plugin > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-docker-plugin > snyk-nodejs-lockfile-parser > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-mvn-plugin > @snyk/java-call-graph-builder > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > @snyk/snyk-cocoapods-plugin > @snyk/cocoapods-lockfile-parser > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-php-plugin > @snyk/cli-interface > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-gradle-plugin > @snyk/cli-interface > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-mvn-plugin > @snyk/cli-interface > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-nodejs-lockfile-parser > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-go-plugin > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'

View file

@ -59,15 +59,18 @@ context('Recorder', () => {
cy.get('.title-text').should('contain', 'DocType');
cy.get('.list-count').should('contain', '20 of ');
cy.visit('/desk#recorder');
// temporarily commenting out theses tests as they seem to be
// randomly failing maybe due a backround event
cy.get('.list-row-container span').contains('/api/method/frappe').click();
// cy.visit('/desk#recorder');
cy.location('hash').should('contain', '#recorder/request/');
cy.get('form').should('contain', '/api/method/frappe');
// cy.get('.list-row-container span').contains('/api/method/frappe').click();
cy.get('#page-recorder .primary-action').should('contain', 'Stop').click();
cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click();
cy.location('hash').should('eq', '#recorder');
// cy.location('hash').should('contain', '#recorder/request/');
// cy.get('form').should('contain', '/api/method/frappe');
// cy.get('#page-recorder .primary-action').should('contain', 'Stop').click();
// cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click();
// cy.location('hash').should('eq', '#recorder');
});
});

View file

@ -10,7 +10,7 @@ from six import iteritems, binary_type, text_type, string_types, PY2
from werkzeug.local import Local, release_local
import os, sys, importlib, inspect, json
from past.builtins import cmp
import click
from faker import Faker
# public
@ -226,12 +226,20 @@ def get_site_config(sites_path=None, site_path=None):
if sites_path:
common_site_config = os.path.join(sites_path, "common_site_config.json")
if os.path.exists(common_site_config):
config.update(get_file_json(common_site_config))
try:
config.update(get_file_json(common_site_config))
except Exception as error:
click.secho("common_site_config.json is invalid", fg="red")
print(error)
if site_path:
site_config = os.path.join(site_path, "site_config.json")
if os.path.exists(site_config):
config.update(get_file_json(site_config))
try:
config.update(get_file_json(site_config))
except Exception as error:
click.secho("{0}/site_config.json is invalid".format(local.site), fg="red")
print(error)
elif local.site and not local.flags.new_site:
raise IncorrectSitePath("{0} does not exist".format(local.site))
@ -514,12 +522,15 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message
whitelisted = []
guest_methods = []
xss_safe_methods = []
def whitelist(allow_guest=False, xss_safe=False):
allowed_http_methods_for_whitelisted_func = {}
def whitelist(allow_guest=False, xss_safe=False, methods=None):
"""
Decorator for whitelisting a function and making it accessible via HTTP.
Standard request will be `/api/method/[path.to.method]`
:param allow_guest: Allow non logged-in user to access this method.
:param methods: Allowed http method to access the method.
Use as:
@ -527,10 +538,16 @@ def whitelist(allow_guest=False, xss_safe=False):
def myfunc(param1, param2):
pass
"""
if not methods:
methods = ['GET', 'POST', 'PUT', 'DELETE']
def innerfn(fn):
global whitelisted, guest_methods, xss_safe_methods
global whitelisted, guest_methods, xss_safe_methods, allowed_http_methods_for_whitelisted_func
whitelisted.append(fn)
allowed_http_methods_for_whitelisted_func[fn] = methods
if allow_guest:
guest_methods.append(fn)

View file

@ -193,7 +193,8 @@ def handle_exception(e):
else:
traceback = "<pre>" + sanitize_html(frappe.get_traceback()) + "</pre>"
if frappe.local.flags.disable_traceback:
# disable traceback in production if flag is set
if frappe.local.flags.disable_traceback and not frappe.local.dev_server:
traceback = ""
frappe.respond_as_web_page("Server Error",

View file

@ -2,6 +2,9 @@
// For license information, please see license.txt
frappe.ui.form.on('Assignment Rule', {
onload: (frm) => {
frm.trigger('set_due_date_field_options');
},
refresh: function(frm) {
// refresh description
frm.events.rule(frm);
@ -12,5 +15,25 @@ frappe.ui.form.on('Assignment Rule', {
} else {
frm.get_field('rule').set_description(__('Assign to the one who has the least assignments'));
}
},
document_type: (frm) => {
frm.trigger('set_due_date_field_options');
},
set_due_date_field_options: (frm) => {
let doctype = frm.doc.document_type;
let datetime_fields = [];
if (doctype) {
frappe.model.with_doctype(doctype, () => {
frappe.get_meta(doctype).fields.map((df) => {
if (['Date', 'Datetime'].includes(df.fieldtype)) {
datetime_fields.push({ label: df.label, value: df.fieldname });
}
});
if (datetime_fields) {
frm.set_df_property('due_date_based_on', 'options', datetime_fields);
}
frm.set_df_property('due_date_based_on', 'hidden', !datetime_fields.length);
});
}
}
});

View file

@ -1,4 +1,5 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2019-02-28 17:12:18.815830",
@ -8,6 +9,7 @@
"engine": "InnoDB",
"field_order": [
"document_type",
"due_date_based_on",
"priority",
"disabled",
"column_break_4",
@ -129,9 +131,17 @@
"label": "Assignment Days",
"options": "Assignment Rule Day",
"reqd": 1
},
{
"depends_on": "document_type",
"fieldname": "due_date_based_on",
"fieldtype": "Select",
"label": "Due Date Based On"
}
],
"modified": "2019-09-25 14:52:12.214514",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-10-08 06:48:54.019735",
"modified_by": "Administrator",
"module": "Automation",
"name": "Assignment Rule",

View file

@ -19,14 +19,14 @@ class AssignmentRule(Document):
repeated_days = get_repeated(assignment_days)
frappe.throw(_("Assignment Day {0} has been repeated.").format(frappe.bold(repeated_days)))
def on_update(self): # pylint: disable=no-self-use
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.document_type)
def on_update(self):
clear_assignment_rule_cache(self)
def after_rename(self, old, new, merge): # pylint: disable=no-self-use
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.document_type)
def after_rename(self, old, new, merge):
clear_assignment_rule_cache(self)
def on_trash(self): # pylint: disable=no-self-use
frappe.cache_manager.clear_doctype_map('Assignment Rule', self.document_type)
def on_trash(self):
clear_assignment_rule_cache(self)
def apply_unassign(self, doc, assignments):
if (self.unassign_condition and
@ -53,7 +53,8 @@ class AssignmentRule(Document):
name = doc.get('name'),
description = frappe.render_template(self.description, doc),
assignment_rule = self.name,
notify = True
notify = True,
date = doc.get(self.due_date_based_on) if self.due_date_based_on else None
))
# set for reference in round robin
@ -188,7 +189,7 @@ def apply(doc, method=None, doctype=None, name=None):
# multiple auto assigns
for d in assignment_rules:
assignment_rule_docs.append(frappe.get_doc('Assignment Rule', d.get('name')))
assignment_rule_docs.append(frappe.get_cached_doc('Assignment Rule', d.get('name')))
if not assignment_rule_docs:
return
@ -237,6 +238,32 @@ def apply(doc, method=None, doctype=None, name=None):
break
assignment_rule.close_assignments(doc)
def update_due_date(doc, state=None):
# called from hook
assignment_rules = frappe.cache_manager.get_doctype_map('Assignment Rule', 'due_date_rules_for_' + doc.doctype, dict(
document_type = doc.doctype,
disabled = 0,
due_date_based_on = ['is', 'set']
))
for rule in assignment_rules:
rule_doc = frappe.get_cached_doc('Assignment Rule', rule.get('name'))
due_date_field = rule_doc.due_date_based_on
if doc.meta.has_field(due_date_field) and \
doc.has_value_changed(due_date_field) and rule.get('name'):
assignment_todos = frappe.get_all('ToDo', {
'assignment_rule': rule.get('name'),
'status': 'Open'
})
for todo in assignment_todos:
todo_doc = frappe.get_doc('ToDo', todo.name)
todo_doc.date = doc.get(due_date_field)
todo_doc.flags.updater_reference = {
'doctype': 'Assignment Rule',
'docname': rule.get('name'),
'label': _('via Assignment Rule')
}
todo_doc.save(ignore_permissions=True)
def get_assignment_rules():
return [d.document_type for d in frappe.db.get_all('Assignment Rule', fields=['document_type'], filters=dict(disabled = 0))]
@ -250,3 +277,7 @@ def get_repeated(values):
if value not in diff:
diff.append(str(value))
return " ".join(diff)
def clear_assignment_rule_cache(rule):
frappe.cache_manager.clear_doctype_map('Assignment Rule', rule.document_type)
frappe.cache_manager.clear_doctype_map('Assignment Rule', 'due_date_rules_for_' + rule.document_type)

View file

@ -20,6 +20,7 @@ class TestAutoAssign(unittest.TestCase):
dict(day = 'Friday'),
dict(day = 'Saturday'),
]
self.days = days
self.assignment_rule = get_assignment_rule([days, days])
clear_assignments()
@ -180,6 +181,45 @@ class TestAutoAssign(unittest.TestCase):
status = 'Open'
), 'owner'), ['test3@example.com'])
def test_assignment_rule_condition(self):
frappe.db.sql("DELETE FROM `tabAssignment Rule`")
# Add expiry_date custom field
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
df = dict(fieldname='expiry_date', label='Expiry Date', fieldtype='Date')
create_custom_field('Note', df)
assignment_rule = frappe.get_doc(dict(
name = 'Assignment with Due Date',
doctype = 'Assignment Rule',
document_type = 'Note',
assign_condition = 'public == 0',
due_date_based_on = 'expiry_date',
assignment_days = self.days,
users = [
dict(user = 'test@example.com'),
]
)).insert()
expiry_date = frappe.utils.add_days(frappe.utils.nowdate(), 2)
note = make_note({'expiry_date': expiry_date})
todo = frappe.get_all('ToDo', filters=dict(
reference_type = 'Note',
reference_name = note.name,
status = 'Open'
))[0]
todo = frappe.get_doc('ToDo', todo.name)
self.assertEqual(frappe.utils.get_date_str(todo.date), expiry_date)
# due date should be updated if the reference doc's date is updated.
note.expiry_date = frappe.utils.add_days(expiry_date, 2)
note.save()
todo.reload()
self.assertEqual(frappe.utils.get_date_str(todo.date), note.expiry_date)
assignment_rule.delete()
def clear_assignments():
frappe.db.sql("delete from tabToDo where reference_type = 'Note'")

View file

@ -146,7 +146,7 @@ class AutoRepeat(Document):
def make_new_document(self):
reference_doc = frappe.get_doc(self.reference_doctype, self.reference_document)
new_doc = frappe.copy_doc(reference_doc)
new_doc = frappe.copy_doc(reference_doc, ignore_no_copy = False)
self.update_doc(new_doc, reference_doc)
new_doc.insert(ignore_permissions = True)

View file

@ -11,24 +11,141 @@ import warnings
import tempfile
from distutils.spawn import find_executable
from six import iteritems, text_type
import frappe
from frappe.utils.minify import JavascriptMinify
import click
from requests import get
from six import iteritems, text_type
from six.moves.urllib.parse import urlparse
timestamps = {}
app_paths = None
sites_path = os.path.abspath(os.getcwd())
def download_file(url, prefix):
filename = urlparse(url).path.split("/")[-1]
local_filename = os.path.join(prefix, filename)
with get(url, stream=True, allow_redirects=True) as r:
r.raise_for_status()
with open(local_filename, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
return local_filename
def build_missing_files():
# check which files dont exist yet from the build.json and tell build.js to build only those!
missing_assets = []
current_asset_files = []
for type in ["css", "js"]:
current_asset_files.extend(
[
"{0}/{1}".format(type, name)
for name in os.listdir(os.path.join(sites_path, "assets", type))
]
)
with open(os.path.join(sites_path, "assets", "frappe", "build.json")) as f:
all_asset_files = json.load(f).keys()
for asset in all_asset_files:
if asset.replace("concat:", "") not in current_asset_files:
missing_assets.append(asset)
if missing_assets:
from subprocess import check_call
from shlex import split
click.secho("\nBuilding missing assets...\n", fg="yellow")
command = split(
"node rollup/build.js --files {0} --no-concat".format(",".join(missing_assets))
)
check_call(command, cwd=os.path.join("..", "apps", "frappe"))
def get_assets_link(frappe_head):
from subprocess import getoutput
from requests import head
tag = getoutput(
"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*"
" refs/tags/,,' -e 's/\^{}//'"
% frappe_head
)
if tag:
# if tag exists, download assets from github release
url = "https://github.com/frappe/frappe/releases/download/{0}/assets.tar.gz".format(tag)
else:
url = "http://assets.frappeframework.com/{0}.tar.gz".format(frappe_head)
if not head(url):
raise ValueError("URL {0} doesn't exist".format(url))
return url
def download_frappe_assets(verbose=True):
"""Downloads and sets up Frappe assets if they exist based on the current
commit HEAD.
Returns True if correctly setup else returns False.
"""
from simple_chalk import green
from subprocess import getoutput
from tempfile import mkdtemp
assets_setup = False
frappe_head = getoutput("cd ../apps/frappe && git rev-parse HEAD")
if frappe_head:
try:
url = get_assets_link(frappe_head)
click.secho("Retreiving assets...", fg="yellow")
prefix = mkdtemp(prefix="frappe-assets-", suffix=frappe_head)
assets_archive = download_file(url, prefix)
print("\n{0} Downloaded Frappe assets from {1}".format(green(''), url))
if assets_archive:
import tarfile
click.secho("\nExtracting assets...\n", fg="yellow")
with tarfile.open(assets_archive) as tar:
for file in tar:
if not file.isdir():
dest = "." + file.name.replace("./frappe-bench/sites", "")
show = dest.replace("./assets/", "")
tar.makefile(file, dest)
print("{0} Restored {1}".format(green(''), show))
build_missing_files()
return True
else:
raise
except Exception:
# TODO: log traceback in bench.log
click.secho("An Error occurred while downloading assets...", fg="red")
assets_setup = False
finally:
try:
shutil.rmtree(os.path.dirname(assets_archive))
except Exception:
pass
return assets_setup
def symlink(target, link_name, overwrite=False):
'''
"""
Create a symbolic link named link_name pointing to target.
If link_name exists then FileExistsError is raised, unless overwrite=True.
When trying to overwrite a directory, IsADirectoryError is raised.
Source: https://stackoverflow.com/a/55742015/10309266
'''
"""
if not overwrite:
return os.symlink(target, link_name)
@ -76,27 +193,28 @@ def setup():
def get_node_pacman():
pacmans = ['yarn', 'npm']
for exec_ in pacmans:
exec_ = find_executable(exec_)
if exec_:
return exec_
raise ValueError('No Node.js Package Manager found.')
exec_ = find_executable("yarn")
if exec_:
return exec_
raise ValueError("Yarn not found")
def bundle(no_compress, app=None, make_copy=False, restore=False, verbose=False):
def bundle(no_compress, app=None, make_copy=False, restore=False, verbose=False, skip_frappe=False):
"""concat / minify js files"""
setup()
make_asset_dirs(make_copy=make_copy, restore=restore)
pacman = get_node_pacman()
mode = 'build' if no_compress else 'production'
command = '{pacman} run {mode}'.format(pacman=pacman, mode=mode)
mode = "build" if no_compress else "production"
command = "{pacman} run {mode}".format(pacman=pacman, mode=mode)
if app:
command += ' --app {app}'.format(app=app)
command += " --app {app}".format(app=app)
frappe_app_path = os.path.abspath(os.path.join(app_paths[0], '..'))
if skip_frappe:
command += " --skip_frappe"
frappe_app_path = os.path.abspath(os.path.join(app_paths[0], ".."))
check_yarn()
frappe.commands.popen(command, cwd=frappe_app_path)
@ -107,22 +225,22 @@ def watch(no_compress):
pacman = get_node_pacman()
frappe_app_path = os.path.abspath(os.path.join(app_paths[0], '..'))
frappe_app_path = os.path.abspath(os.path.join(app_paths[0], ".."))
check_yarn()
frappe_app_path = frappe.get_app_path('frappe', '..')
frappe.commands.popen('{pacman} run watch'.format(pacman=pacman), cwd=frappe_app_path)
frappe_app_path = frappe.get_app_path("frappe", "..")
frappe.commands.popen("{pacman} run watch".format(pacman=pacman), cwd=frappe_app_path)
def check_yarn():
if not find_executable('yarn'):
print('Please install yarn using below command and try again.\nnpm install -g yarn')
if not find_executable("yarn"):
print("Please install yarn using below command and try again.\nnpm install -g yarn")
def make_asset_dirs(make_copy=False, restore=False):
# don't even think of making assets_path absolute - rm -rf ahead.
assets_path = os.path.join(frappe.local.sites_path, "assets")
for dir_path in [os.path.join(assets_path, 'js'), os.path.join(assets_path, 'css')]:
for dir_path in [os.path.join(assets_path, "js"), os.path.join(assets_path, "css")]:
if not os.path.exists(dir_path):
os.makedirs(dir_path)
@ -131,24 +249,27 @@ def make_asset_dirs(make_copy=False, restore=False):
app_base_path = os.path.abspath(os.path.dirname(pymodule.__file__))
symlinks = []
app_public_path = os.path.join(app_base_path, 'public')
app_public_path = os.path.join(app_base_path, "public")
# app/public > assets/app
symlinks.append([app_public_path, os.path.join(assets_path, app_name)])
# app/node_modules > assets/app/node_modules
if os.path.exists(os.path.abspath(app_public_path)):
symlinks.append([os.path.join(app_base_path, '..', 'node_modules'), os.path.join(
assets_path, app_name, 'node_modules')])
symlinks.append(
[
os.path.join(app_base_path, "..", "node_modules"),
os.path.join(assets_path, app_name, "node_modules"),
]
)
app_doc_path = None
if os.path.isdir(os.path.join(app_base_path, 'docs')):
app_doc_path = os.path.join(app_base_path, 'docs')
if os.path.isdir(os.path.join(app_base_path, "docs")):
app_doc_path = os.path.join(app_base_path, "docs")
elif os.path.isdir(os.path.join(app_base_path, 'www', 'docs')):
app_doc_path = os.path.join(app_base_path, 'www', 'docs')
elif os.path.isdir(os.path.join(app_base_path, "www", "docs")):
app_doc_path = os.path.join(app_base_path, "www", "docs")
if app_doc_path:
symlinks.append([app_doc_path, os.path.join(
assets_path, app_name + '_docs')])
symlinks.append([app_doc_path, os.path.join(assets_path, app_name + "_docs")])
for source, target in symlinks:
source = os.path.abspath(source)
@ -162,7 +283,7 @@ def make_asset_dirs(make_copy=False, restore=False):
shutil.copytree(source, target)
elif make_copy:
if os.path.exists(target):
warnings.warn('Target {target} already exists.'.format(target=target))
warnings.warn("Target {target} already exists.".format(target=target))
else:
shutil.copytree(source, target)
else:
@ -174,7 +295,7 @@ def make_asset_dirs(make_copy=False, restore=False):
try:
symlink(source, target, overwrite=True)
except OSError:
print('Cannot link {} to {}'.format(source, target))
print("Cannot link {} to {}".format(source, target))
else:
# warnings.warn('Source {source} does not exist.'.format(source = source))
pass
@ -193,7 +314,7 @@ def get_build_maps():
build_maps = {}
for app_path in app_paths:
path = os.path.join(app_path, 'public', 'build.json')
path = os.path.join(app_path, "public", "build.json")
if os.path.exists(path):
with open(path) as f:
try:
@ -202,8 +323,7 @@ def get_build_maps():
source_paths = []
for source in sources:
if isinstance(source, list):
s = frappe.get_pymodule_path(
source[0], *source[1].split("/"))
s = frappe.get_pymodule_path(source[0], *source[1].split("/"))
else:
s = os.path.join(app_path, source)
source_paths.append(s)
@ -211,36 +331,42 @@ def get_build_maps():
build_maps[target] = source_paths
except ValueError as e:
print(path)
print('JSON syntax error {0}'.format(str(e)))
print("JSON syntax error {0}".format(str(e)))
return build_maps
def pack(target, sources, no_compress, verbose):
from six import StringIO
outtype, outtxt = target.split(".")[-1], ''
outtype, outtxt = target.split(".")[-1], ""
jsm = JavascriptMinify()
for f in sources:
suffix = None
if ':' in f:
f, suffix = f.split(':')
if ":" in f:
f, suffix = f.split(":")
if not os.path.exists(f) or os.path.isdir(f):
print("did not find " + f)
continue
timestamps[f] = os.path.getmtime(f)
try:
with open(f, 'r') as sourcefile:
data = text_type(sourcefile.read(), 'utf-8', errors='ignore')
with open(f, "r") as sourcefile:
data = text_type(sourcefile.read(), "utf-8", errors="ignore")
extn = f.rsplit(".", 1)[1]
if outtype == "js" and extn == "js" and (not no_compress) and suffix != "concat" and (".min." not in f):
tmpin, tmpout = StringIO(data.encode('utf-8')), StringIO()
if (
outtype == "js"
and extn == "js"
and (not no_compress)
and suffix != "concat"
and (".min." not in f)
):
tmpin, tmpout = StringIO(data.encode("utf-8")), StringIO()
jsm.minify(tmpin, tmpout)
minified = tmpout.getvalue()
if minified:
outtxt += text_type(minified or '', 'utf-8').strip('\n') + ';'
outtxt += text_type(minified or "", "utf-8").strip("\n") + ";"
if verbose:
print("{0}: {1}k".format(f, int(len(minified) / 1024)))
@ -248,27 +374,27 @@ def pack(target, sources, no_compress, verbose):
# add to frappe.templates
outtxt += html_to_js_template(f, data)
else:
outtxt += ('\n/*\n *\t%s\n */' % f)
outtxt += '\n' + data + '\n'
outtxt += "\n/*\n *\t%s\n */" % f
outtxt += "\n" + data + "\n"
except Exception:
print("--Error in:" + f + "--")
print(frappe.get_traceback())
with open(target, 'w') as f:
with open(target, "w") as f:
f.write(outtxt.encode("utf-8"))
print("Wrote %s - %sk" % (target, str(int(os.path.getsize(target)/1024))))
print("Wrote %s - %sk" % (target, str(int(os.path.getsize(target) / 1024))))
def html_to_js_template(path, content):
'''returns HTML template content as Javascript code, adding it to `frappe.templates`'''
"""returns HTML template content as Javascript code, adding it to `frappe.templates`"""
return """frappe.templates["{key}"] = '{content}';\n""".format(
key=path.rsplit("/", 1)[-1][:-5], content=scrub_html_template(content))
def scrub_html_template(content):
'''Returns HTML content with removed whitespace and comments'''
"""Returns HTML content with removed whitespace and comments"""
# remove whitespace to a single space
content = re.sub("\s+", " ", content)
@ -281,12 +407,12 @@ def scrub_html_template(content):
def files_dirty():
for target, sources in iteritems(get_build_maps()):
for f in sources:
if ':' in f:
f, suffix = f.split(':')
if ":" in f:
f, suffix = f.split(":")
if not os.path.exists(f) or os.path.isdir(f):
continue
if os.path.getmtime(f) != timestamps.get(f):
print(f + ' dirty')
print(f + " dirty")
return True
else:
return False

View file

@ -4,7 +4,6 @@
from __future__ import unicode_literals
import frappe, json
import frappe.defaults
from frappe.model.document import Document
from frappe.desk.notifications import (delete_notification_count_for,
clear_notifications)

View file

@ -62,11 +62,11 @@
"label": "URLs"
}
],
"modified": "2019-11-07 13:21:19.395927",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Chat",
"name": "Chat Message",
"owner": "arjun@gmail.com",
"owner": "Administrator",
"permissions": [
{
"create": 1,

View file

@ -107,7 +107,7 @@ def get_single_value(doctype, field):
value = frappe.db.get_single_value(doctype, field)
return value
@frappe.whitelist()
@frappe.whitelist(methods=['POST', 'PUT'])
def set_value(doctype, name, fieldname, value=None):
'''Set a value using get_doc, group of values
@ -142,7 +142,7 @@ def set_value(doctype, name, fieldname, value=None):
return doc.as_dict()
@frappe.whitelist()
@frappe.whitelist(methods=['POST', 'PUT'])
def insert(doc=None):
'''Insert a document
@ -160,7 +160,7 @@ def insert(doc=None):
doc = frappe.get_doc(doc).insert()
return doc.as_dict()
@frappe.whitelist()
@frappe.whitelist(methods=['POST', 'PUT'])
def insert_many(docs=None):
'''Insert multiple documents
@ -186,7 +186,7 @@ def insert_many(docs=None):
return out
@frappe.whitelist()
@frappe.whitelist(methods=['POST', 'PUT'])
def save(doc):
'''Update (save) an existing document
@ -199,7 +199,7 @@ def save(doc):
return doc.as_dict()
@frappe.whitelist()
@frappe.whitelist(methods=['POST', 'PUT'])
def rename_doc(doctype, old_name, new_name, merge=False):
'''Rename document
@ -209,7 +209,7 @@ def rename_doc(doctype, old_name, new_name, merge=False):
new_name = frappe.rename_doc(doctype, old_name, new_name, merge=merge)
return new_name
@frappe.whitelist()
@frappe.whitelist(methods=['POST', 'PUT'])
def submit(doc):
'''Submit a document
@ -222,7 +222,7 @@ def submit(doc):
return doc.as_dict()
@frappe.whitelist()
@frappe.whitelist(methods=['POST', 'PUT'])
def cancel(doctype, name):
'''Cancel a document
@ -233,7 +233,7 @@ def cancel(doctype, name):
return wrapper.as_dict()
@frappe.whitelist()
@frappe.whitelist(methods=['DELETE', 'POST'])
def delete(doctype, name):
'''Delete a remote document
@ -241,13 +241,13 @@ def delete(doctype, name):
:param name: name of the document to be deleted'''
frappe.delete_doc(doctype, name, ignore_missing=False)
@frappe.whitelist()
@frappe.whitelist(methods=['POST', 'PUT'])
def set_default(key, value, parent=None):
"""set a user default value"""
frappe.db.set_default(key, value, parent or frappe.session.user)
frappe.clear_cache(user=frappe.session.user)
@frappe.whitelist()
@frappe.whitelist(methods=['POST', 'PUT'])
def make_width_property_setter(doc):
'''Set width Property Setter
@ -257,7 +257,7 @@ def make_width_property_setter(doc):
if doc["doctype"]=="Property Setter" and doc["property"]=="width":
frappe.get_doc(doc).insert(ignore_permissions = True)
@frappe.whitelist()
@frappe.whitelist(methods=['POST', 'PUT'])
def bulk_update(docs):
'''Bulk update documents
@ -333,7 +333,7 @@ def get_time_zone():
'''Returns default time zone'''
return {"time_zone": frappe.defaults.get_defaults().get("time_zone")}
@frappe.whitelist()
@frappe.whitelist(methods=['POST', 'PUT'])
def attach_file(filename=None, filedata=None, doctype=None, docname=None, folder=None, decode_base64=False, is_private=None, docfield=None):
'''Attach a file to Document (POST)

View file

@ -1,10 +1,5 @@
# imports - standard imports
import atexit
import compileall
import hashlib
import os
import re
import shutil
import sys
# imports - third party imports
@ -12,11 +7,8 @@ import click
# imports - module imports
import frappe
from frappe import _
from frappe.commands import get_site, pass_context
from frappe.commands.scheduler import _is_scheduler_enabled
from frappe.exceptions import SiteNotSpecifiedError
from frappe.installer import update_site_config
from frappe.utils import get_site_path, touch_file
@ -65,8 +57,10 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
sys.exit(1)
if not db_name:
import hashlib
db_name = '_' + hashlib.sha1(site.encode()).hexdigest()[:16]
from frappe.commands.scheduler import _is_scheduler_enabled
from frappe.installer import install_db, make_site_dirs
from frappe.installer import install_app as _install_app
import frappe.utils.scheduler
@ -74,6 +68,7 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
frappe.init(site=site)
try:
# enable scheduler post install?
enable_scheduler = _is_scheduler_enabled()
except Exception:
@ -108,11 +103,11 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
@click.option('--install-app', multiple=True, help='Install app after installation')
@click.option('--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')
@click.option('--force', is_flag=True, default=False, help='Use a bit of force to get the job done')
@click.option('--force', is_flag=True, default=False, help='Ignore the site downgrade warning, if applicable')
@pass_context
def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None):
"Restore site database from an sql file"
from frappe.installer import extract_sql_gzip, extract_tar_files, is_downgrade
from frappe.installer import extract_sql_gzip, extract_files, is_downgrade
force = context.force or force
# Extract the gzip file if user has passed *.sql.gz file instead of *.sql file
@ -148,12 +143,12 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
# Extract public and/or private files to the restored site, if user has given the path
if with_public_files:
with_public_files = os.path.join(base_path, with_public_files)
public = extract_tar_files(site, with_public_files, 'public')
public = extract_files(site, with_public_files, 'public')
os.remove(public)
if with_private_files:
with_private_files = os.path.join(base_path, with_private_files)
private = extract_tar_files(site, with_private_files, 'private')
private = extract_files(site, with_private_files, 'private')
os.remove(private)
# Removing temporarily created file
@ -272,12 +267,13 @@ def disable_user(context, email):
@click.command('migrate')
@click.option('--rebuild-website', help="Rebuild webpages after migration")
@click.option('--skip-failing', is_flag=True, help="Skip patches that fail to run")
@click.option('--skip-search-index', is_flag=True, help="Skip search indexing for web documents")
@pass_context
def migrate(context, rebuild_website=False, skip_failing=False, skip_search_index=False):
def migrate(context, skip_failing=False, skip_search_index=False):
"Run patches, sync schema and rebuild files/translations"
import compileall
import re
from frappe.migrate import migrate
for site in context.sites:
@ -287,7 +283,6 @@ def migrate(context, rebuild_website=False, skip_failing=False, skip_search_inde
try:
migrate(
context.verbose,
rebuild_website=rebuild_website,
skip_failing=skip_failing,
skip_search_index=skip_search_index
)
@ -388,35 +383,34 @@ def use(site, sites_path='.'):
@click.command('backup')
@click.option('--with-files', default=False, is_flag=True, help="Take backup with files")
@click.option('--verbose', default=False, is_flag=True)
@click.option('--backup-path', default=None, help="Set path for saving all the files in this operation")
@click.option('--backup-path-db', default=None, help="Set path for saving database file")
@click.option('--backup-path-files', default=None, help="Set path for saving public file")
@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('--verbose', default=False, is_flag=True, help="Add verbosity")
@click.option('--compress', default=False, is_flag=True, help="Compress private and public files")
@pass_context
def backup(context, with_files=False, backup_path_db=None, backup_path_files=None,
backup_path_private_files=None, quiet=False, verbose=False):
def backup(context, with_files=False, backup_path=None, backup_path_db=None, backup_path_files=None,
backup_path_private_files=None, backup_path_conf=None, verbose=False, compress=False):
"Backup"
from frappe.utils.backups import scheduled_backup
verbose = verbose or context.verbose
exit_code = 0
for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True, verbose=verbose)
except Exception as e:
if verbose:
print("Backup failed for {0}. Database or site_config.json may be corrupted".format(site))
odb = scheduled_backup(ignore_files=not with_files, backup_path=backup_path, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, backup_path_conf=backup_path_conf, force=True, verbose=verbose, compress=compress)
except Exception:
click.secho("Backup failed for Site {0}. Database or site_config.json may be corrupted".format(site), fg="red")
exit_code = 1
continue
if verbose:
from frappe.utils import now
summary_title = "Backup Summary at {0}".format(now())
print(summary_title + "\n" + "-" * len(summary_title))
print("Database backup:", odb.backup_path_db)
if with_files:
print("Public files: ", odb.backup_path_files)
print("Private files: ", odb.backup_path_private_files)
odb.print_summary()
click.secho("Backup for Site {0} has been successfully completed{1}".format(site, " with files" if with_files else ""), fg="green")
frappe.destroy()
if not context.sites:
raise SiteNotSpecifiedError

View file

@ -1,17 +1,17 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, absolute_import, print_function
import click
import json, os, sys, subprocess
import json
import os
import subprocess
import sys
from distutils.spawn import find_executable
import click
import frappe
from frappe.commands import pass_context, get_site
from frappe.commands import get_site, pass_context
from frappe.exceptions import SiteNotSpecifiedError
from frappe.utils import update_progress_bar, get_bench_path
from frappe.utils.response import json_handler
from coverage import Coverage
import cProfile, pstats
from six import StringIO
from frappe.utils import get_bench_path, update_progress_bar
@click.command('build')
@ -19,14 +19,22 @@ from six import StringIO
@click.option('--make-copy', is_flag=True, default=False, help='Copy the files instead of symlinking')
@click.option('--restore', is_flag=True, default=False, help='Copy the files instead of symlinking with force')
@click.option('--verbose', is_flag=True, default=False, help='Verbose')
def build(app=None, make_copy=False, restore = False, verbose=False):
@click.option('--force', is_flag=True, default=False, help='Force build assets instead of downloading available')
def build(app=None, make_copy=False, restore=False, verbose=False, force=False):
"Minify + concatenate JS and CSS files, build translations"
import frappe.build
import frappe
frappe.init('')
# don't minify in developer_mode for faster builds
no_compress = frappe.local.conf.developer_mode or False
frappe.build.bundle(no_compress, app=app, make_copy=make_copy, restore = restore, verbose=verbose)
# dont try downloading assets if force used, app specified or running via CI
if not (force or app or os.environ.get('CI')):
# skip building frappe if assets exist remotely
skip_frappe = frappe.build.download_frappe_assets(verbose=verbose)
else:
skip_frappe = False
frappe.build.bundle(no_compress, app=app, make_copy=make_copy, restore=restore, verbose=verbose, skip_frappe=skip_frappe)
@click.command('watch')
@ -152,6 +160,7 @@ def execute(context, method, args=None, kwargs=None, profile=False):
kwargs = {}
if profile:
import cProfile
pr = cProfile.Profile()
pr.enable()
@ -161,6 +170,9 @@ def execute(context, method, args=None, kwargs=None, profile=False):
ret = frappe.safe_eval(method + "(*args, **kwargs)", eval_globals=globals(), eval_locals=locals())
if profile:
import pstats
from six import StringIO
pr.disable()
s = StringIO()
pstats.Stats(pr, stream=s).sort_stats('cumulative').print_stats(.5)
@ -171,6 +183,7 @@ def execute(context, method, args=None, kwargs=None, profile=False):
finally:
frappe.destroy()
if ret:
from frappe.utils.response import json_handler
print(json.dumps(ret, default=json_handler))
if not context.sites:
@ -292,8 +305,6 @@ def import_doc(context, path, force=False):
@click.option('--submit-after-import', default=False, is_flag=True, help='Submit document after importing it')
@click.option('--ignore-encoding-errors', default=False, is_flag=True, help='Ignore encoding errors while coverting to unicode')
@click.option('--no-email', default=True, is_flag=True, help='Send email if applicable')
@pass_context
def import_csv(context, path, only_insert=False, submit_after_import=False, ignore_encoding_errors=False, no_email=True):
"Import CSV using data import"
@ -424,7 +435,7 @@ def jupyter(context):
os.mkdir(jupyter_notebooks_path)
bin_path = os.path.abspath('../env/bin')
print('''
Stating Jupyter notebook
Starting Jupyter notebook
Run the following in your first cell to connect notebook to frappe
```
import frappe
@ -496,6 +507,8 @@ def run_tests(context, app=None, module=None, doctype=None, test=(),
frappe.flags.skip_test_records = skip_test_records
if coverage:
from coverage import Coverage
# Generate coverage report only for app that is being tested
source_path = os.path.join(get_bench_path(), 'apps', app or 'frappe')
cov = Coverage(source=[source_path], omit=[

View file

@ -2,7 +2,7 @@
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"allow_rename": 1,
"autoname": "field:salutation",
"beta": 0,
"creation": "2017-04-10 12:17:58.071915",
@ -53,7 +53,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-04-10 12:55:18.855578",
"modified": "2020-09-14 12:55:18.855578",
"modified_by": "Administrator",
"module": "Contacts",
"name": "Salutation",
@ -129,4 +129,4 @@
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}
}

View file

@ -8,7 +8,7 @@ from frappe import _
import frappe.permissions
import re, csv, os
from frappe.utils.csvutils import UnicodeWriter
from frappe.utils import cstr, formatdate, format_datetime, parse_json, cint
from frappe.utils import cstr, formatdate, format_datetime, parse_json, cint, format_duration
from frappe.core.doctype.data_import_legacy.importer import get_data_keys
from six import string_types
from frappe.core.doctype.access_log.access_log import make_access_log
@ -330,6 +330,8 @@ class DataExporter:
value = formatdate(value)
elif fieldtype == "Datetime":
value = format_datetime(value)
elif fieldtype == "Duration":
value = format_duration(value, df.hide_days)
row[_column_start_end.start + i + 1] = value

View file

@ -8,6 +8,7 @@ from frappe.model import (
no_value_fields,
table_fields as table_fieldtypes,
)
from frappe.utils import flt, format_duration
from frappe.utils.csvutils import build_csv_response
from frappe.utils.xlsxutils import build_xlsx_response
@ -146,8 +147,13 @@ class Exporter:
if df.parent == doctype:
if df.is_child_table_field and df.child_table_df.fieldname != parentfield:
continue
row[i] = doc.get(df.fieldname, "")
value = doc.get(df.fieldname, None)
if df.fieldtype == "Duration":
value = flt(value or 0)
value = format_duration(value, df.hide_days)
row[i] = value
return rows
def get_data_as_docs(self):

View file

@ -1,5 +1,5 @@
Title ,Description ,Number ,another_number ,ID (Table Field 1) ,Child Title (Table Field 1) ,Child Description (Table Field 1) ,Child 2 Title (Table Field 2) ,Child 2 Date (Table Field 2) ,Child 2 Number (Table Field 2) ,Child Title (Table Field 1 Again) ,Child Date (Table Field 1 Again) ,Child Number (Table Field 1 Again) ,table_field_1_again.child_another_number
Test ,test description ,1 ,2 ,"" ,child title ,child description ,child title ,14-08-2019 ,4 ,child title again ,22-09-2020 ,5 , 7
, , , , ,child title 2 ,child description 2 ,title child ,30-10-2019 ,5 ,child title again 2 ,22-09-2021 , ,
Test 2 ,test description 2 ,1 ,2 , ,child mandatory title , ,title child man , , ,child mandatory again , , ,
Test 3 ,test description 3 ,4 ,5 ,"" ,child title asdf ,child description asdf ,child title asdf adsf ,15-08-2019 ,6 ,child title again asdf ,22-09-2022 ,9 , 71
Title ,Description ,Number ,Duration,another_number ,ID (Table Field 1),Child Title (Table Field 1),Child Description (Table Field 1),Child 2 Title (Table Field 2),Child 2 Date (Table Field 2),Child 2 Number (Table Field 2),Child Title (Table Field 1 Again),Child Date (Table Field 1 Again),Child Number (Table Field 1 Again),table_field_1_again.child_another_number
Test ,test description ,1,3h,2, ,child title ,child description ,child title ,14-08-2019,4,child title again ,22-09-2020,5,7
, , ,, , ,child title 2,child description 2,title child ,30-10-2019,5,child title again 2,22-09-2021, ,
Test 2,test description 2,1,4d 3h,2, ,child mandatory title , ,title child man , , ,child mandatory again , , ,
Test 3,test description 3,4,5d 5h 45m,5, ,child title asdf ,child description asdf ,child title asdf adsf ,15-08-2019,6,child title again asdf ,22-09-2022,9,71
Can't render this file because it contains an unexpected character in line 2 and column 54.

View file

@ -9,7 +9,7 @@ import timeit
import json
from datetime import datetime, date
from frappe import _
from frappe.utils import cint, flt, update_progress_bar, cstr
from frappe.utils import cint, flt, update_progress_bar, cstr, duration_to_seconds
from frappe.utils.csvutils import read_csv_content, get_csv_content_from_google_sheets
from frappe.utils.xlsxutils import (
read_xlsx_file_from_attached_file,
@ -664,6 +664,20 @@ class Row:
}
)
return
elif df.fieldtype == "Duration":
import re
is_valid_duration = re.match("^(?:(\d+d)?((^|\s)\d+h)?((^|\s)\d+m)?((^|\s)\d+s)?)$", value)
if not is_valid_duration:
self.warnings.append(
{
"row": self.row_number,
"col": col.column_number,
"field": df_as_json(df),
"message": _("Value {0} must be in the valid duration format: d h m s").format(
frappe.bold(value)
)
}
)
return value
@ -692,6 +706,8 @@ class Row:
value = flt(value)
elif df.fieldtype in ["Date", "Datetime"]:
value = self.get_date(value, col)
elif df.fieldtype == "Duration":
value = duration_to_seconds(value)
return value

View file

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils import getdate
from frappe.utils import getdate, format_duration
doctype_name = 'DocType for Import'
@ -24,6 +24,7 @@ class TestImporter(unittest.TestCase):
self.assertEqual(doc1.description, 'test description')
self.assertEqual(doc1.number, 1)
self.assertEqual(format_duration(doc1.duration), '3h')
self.assertEqual(doc1.table_field_1[0].child_title, 'child title')
self.assertEqual(doc1.table_field_1[0].child_description, 'child description')
@ -40,7 +41,10 @@ class TestImporter(unittest.TestCase):
self.assertEqual(doc1.table_field_1_again[1].child_date, getdate('2021-09-22'))
self.assertEqual(doc2.description, 'test description 2')
self.assertEqual(format_duration(doc2.duration), '4d 3h')
self.assertEqual(doc3.another_number, 5)
self.assertEqual(format_duration(doc3.duration), '5d 5h 45m')
def test_data_import_preview(self):
import_file = get_import_file('sample_import_file')
@ -48,7 +52,7 @@ class TestImporter(unittest.TestCase):
preview = data_import.get_preview_from_template()
self.assertEqual(len(preview.data), 4)
self.assertEqual(len(preview.columns), 15)
self.assertEqual(len(preview.columns), 16)
def test_data_import_without_mandatory_values(self):
import_file = get_import_file('sample_import_file_without_mandatory')
@ -146,6 +150,7 @@ def create_doctype_if_not_exists(doctype_name, force=False):
{'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'},
{'label': 'Description', 'fieldname': 'description', 'fieldtype': 'Small Text'},
{'label': 'Date', 'fieldname': 'date', 'fieldtype': 'Date'},
{'label': 'Duration', 'fieldname': 'duration', 'fieldtype': 'Duration'},
{'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'},
{'label': 'Number', 'fieldname': 'another_number', 'fieldtype': 'Int'},
{'label': 'Table Field 1', 'fieldname': 'table_field_1', 'fieldtype': 'Table', 'options': table_1_name},

View file

@ -15,7 +15,7 @@ from frappe import _
from frappe.utils.csvutils import getlink
from frappe.utils.dateutils import parse_date
from frappe.utils import cint, cstr, flt, getdate, get_datetime, get_url, get_absolute_url
from frappe.utils import cint, cstr, flt, getdate, get_datetime, get_url, get_absolute_url, duration_to_seconds
from six import string_types
@ -164,7 +164,8 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False,
d[fieldname] = get_datetime(_date + " " + _time)
else:
d[fieldname] = None
elif fieldtype == "Duration":
d[fieldname] = duration_to_seconds(cstr(d[fieldname]))
elif fieldtype in ("Image", "Attach Image", "Attach"):
# added file to attachments list
attachments.append(d[fieldname])

View file

@ -490,7 +490,7 @@
"collapsible_depends_on": "links",
"fieldname": "links_section",
"fieldtype": "Section Break",
"label": "Links Section"
"label": "Linked Documents"
},
{
"fieldname": "links",
@ -609,7 +609,7 @@
"link_fieldname": "reference_doctype"
}
],
"modified": "2020-08-06 12:59:32.369095",
"modified": "2020-09-24 13:13:58.227153",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",

View file

@ -99,6 +99,10 @@ class DocType(Document):
if self.default_print_format and not self.custom:
frappe.throw(_('Standard DocType cannot have default print format, use Customize Form'))
if frappe.conf.get('developer_mode'):
self.owner = 'Administrator'
self.modified_by = 'Administrator'
def set_default_in_list_view(self):
'''Set default in-list-view for first 4 mandatory fields'''
if not [d.fieldname for d in self.fields if d.in_list_view]:
@ -234,6 +238,8 @@ class DocType(Document):
if not autoname and self.get("fields", {"fieldname":"naming_series"}):
self.autoname = "naming_series:"
elif self.autoname == "naming_series:" and not self.get("fields", {"fieldname":"naming_series"}):
frappe.throw(_("Invalid fieldname '{0}' in autoname").format(self.autoname))
# validate field name if autoname field:fieldname is used
# Create unique index on autoname field automatically.
@ -634,13 +640,15 @@ class DocType(Document):
if not name:
name = self.name
flags = {"flags": re.ASCII} if six.PY3 else {}
# a DocType name should not start or end with an empty space
if re.match("^[ \t\n\r]+|[ \t\n\r]+$", name, **flags):
frappe.throw(_("DocType's name should not start or end with whitespace"), frappe.NameError)
# a DocType's name should not start with a number or underscore
# and should only contain letters, numbers and underscore
if six.PY2:
is_a_valid_name = re.match("^(?![\W])[^\d_\s][\w ]+$", name)
else:
is_a_valid_name = re.match("^(?![\W])[^\d_\s][\w ]+$", name, flags = re.ASCII)
if not is_a_valid_name:
if not re.match("^(?![\W])[^\d_\s][\w ]+$", name, **flags):
frappe.throw(_("DocType's name should start with a letter and it can only consist of letters, numbers, spaces and underscores"), frappe.NameError)
@ -762,7 +770,7 @@ def validate_fields(meta):
if not d.get("__islocal") and frappe.db.has_column(d.parent, d.fieldname):
has_non_unique_values = frappe.db.sql("""select `{fieldname}`, count(*)
from `tab{doctype}` where ifnull({fieldname}, '') != ''
from `tab{doctype}` where ifnull(`{fieldname}`, '') != ''
group by `{fieldname}` having count(*) > 1 limit 1""".format(
doctype=d.parent, fieldname=d.fieldname))

View file

@ -0,0 +1,23 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Document Naming Rule', {
refresh: function(frm) {
frm.trigger('document_type');
},
document_type: (frm) => {
// update the select field options with fieldnames
if (frm.doc.document_type) {
frappe.model.with_doctype(frm.doc.document_type, () => {
let fieldnames = frappe.get_meta(frm.doc.document_type).fields
.filter((d) => {
return frappe.model.no_value_type.indexOf(d.fieldtype) === -1;
}).map((d) => {
return {label: `${d.label} (${d.fieldname})`, value: d.fieldname};
});
frappe.meta.get_docfield('Document Naming Rule Condition', 'field', frm.doc.name).options = fieldnames;
frm.refresh_field('conditions');
});
}
}
});

View file

@ -0,0 +1,104 @@
{
"actions": [],
"creation": "2020-09-07 12:48:48.334318",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"document_type",
"disabled",
"priority",
"section_break_3",
"conditions",
"naming_section",
"prefix",
"prefix_digits",
"counter"
],
"fields": [
{
"fieldname": "document_type",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Document Type",
"options": "DocType"
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
},
{
"fieldname": "prefix",
"fieldtype": "Data",
"label": "Prefix",
"mandatory_depends_on": "eval:doc.naming_by===\"Numbered\""
},
{
"fieldname": "counter",
"fieldtype": "Int",
"label": "Counter",
"read_only": 1
},
{
"default": "5",
"description": "Example: 00001",
"fieldname": "prefix_digits",
"fieldtype": "Int",
"label": "Digits",
"mandatory_depends_on": "eval:doc.naming_by===\"Numbered\""
},
{
"fieldname": "naming_section",
"fieldtype": "Section Break",
"label": "Naming"
},
{
"collapsible": 1,
"collapsible_depends_on": "conditions",
"fieldname": "section_break_3",
"fieldtype": "Section Break",
"label": "Rule Conditions"
},
{
"fieldname": "conditions",
"fieldtype": "Table",
"label": "Conditions",
"options": "Document Naming Rule Condition"
},
{
"description": "Rules with higher priority will be applied first.",
"fieldname": "priority",
"fieldtype": "Int",
"label": "Priority"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-09-21 10:23:34.401539",
"modified_by": "Administrator",
"module": "Core",
"name": "Document Naming Rule",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "document_type",
"track_changes": 1
}

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils.data import evaluate_filters
class DocumentNamingRule(Document):
def apply(self, doc):
'''
Apply naming rules for the given document. Will set `name` if the rule is matched.
'''
if self.conditions:
if not evaluate_filters(doc, [(d.field, d.condition, d.value) for d in self.conditions]):
return
counter = frappe.db.get_value(self.doctype, self.name, 'counter', for_update=True) or 0
doc.name = self.prefix + ('%0'+str(self.prefix_digits)+'d') % (counter + 1)
frappe.db.set_value(self.doctype, self.name, 'counter', counter + 1)

View file

@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
class TestDocumentNamingRule(unittest.TestCase):
def test_naming_rule_by_series(self):
naming_rule = frappe.get_doc(dict(
doctype = 'Document Naming Rule',
document_type = 'ToDo',
prefix = 'test-todo-',
prefix_digits = 5
)).insert()
todo = frappe.get_doc(dict(
doctype = 'ToDo',
description = 'Is this my name ' + frappe.generate_hash()
)).insert()
self.assertEqual(todo.name, 'test-todo-00001')
naming_rule.delete()
todo.delete()
def test_naming_rule_by_condition(self):
naming_rule = frappe.get_doc(dict(
doctype = 'Document Naming Rule',
document_type = 'ToDo',
prefix = 'test-high-',
prefix_digits = 5,
priority = 10,
conditions = [dict(
field = 'priority',
condition = '=',
value = 'High'
)]
)).insert()
# another rule
naming_rule_1 = frappe.copy_doc(naming_rule)
naming_rule_1.prefix = 'test-medium-'
naming_rule_1.conditions[0].value = 'Medium'
naming_rule_1.insert()
# default rule with low priority - should not get applied for rules
# with higher priority
naming_rule_2 = frappe.copy_doc(naming_rule)
naming_rule_2.prefix = 'test-low-'
naming_rule_2.priority = 0
naming_rule_2.conditions = []
naming_rule_2.insert()
todo = frappe.get_doc(dict(
doctype = 'ToDo',
priority = 'High',
description = 'Is this my name ' + frappe.generate_hash()
)).insert()
todo_1 = frappe.get_doc(dict(
doctype = 'ToDo',
priority = 'Medium',
description = 'Is this my name ' + frappe.generate_hash()
)).insert()
todo_2 = frappe.get_doc(dict(
doctype = 'ToDo',
priority = 'Low',
description = 'Is this my name ' + frappe.generate_hash()
)).insert()
try:
self.assertEqual(todo.name, 'test-high-00001')
self.assertEqual(todo_1.name, 'test-medium-00001')
self.assertEqual(todo_2.name, 'test-low-00001')
finally:
naming_rule.delete()
naming_rule_1.delete()
naming_rule_2.delete()
todo.delete()
todo_1.delete()
todo_2.delete()

View file

@ -0,0 +1,8 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Document Naming Rule Condition', {
// refresh: function(frm) {
// }
});

View file

@ -0,0 +1,49 @@
{
"actions": [],
"creation": "2020-09-08 10:17:54.366279",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"field",
"condition",
"value"
],
"fields": [
{
"fieldname": "field",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Field",
"reqd": 1
},
{
"fieldname": "condition",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Condition",
"options": "=\n!=\n>\n<\n>=\n<=",
"reqd": 1
},
{
"fieldname": "value",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Value",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-09-08 10:19:56.192949",
"modified_by": "Administrator",
"module": "Core",
"name": "Document Naming Rule Condition",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class DocumentNamingRuleCondition(Document):
pass

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestDocumentNamingRuleCondition(unittest.TestCase):
pass

View file

@ -17,11 +17,11 @@
"unique": 1
}
],
"modified": "2019-06-30 13:24:13.732202",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Core",
"name": "Domain",
"owner": "makarand@erpnext.com",
"owner": "Administrator",
"permissions": [
{
"create": 1,

View file

@ -54,12 +54,12 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2017-05-04 11:05:54.750351",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Core",
"name": "Has Domain",
"name_case": "",
"owner": "makarand@erpnext.com",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,

View file

@ -31,7 +31,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Fieldtype",
"options": "Check\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nInt\nLink\nSelect\nTime",
"options": "Check\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nInt\nLink\nSelect\nTime",
"reqd": 1
},
{
@ -48,7 +48,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-08-17 14:32:17.174796",
"modified": "2020-09-03 10:52:03.895817",
"modified_by": "Administrator",
"module": "Core",
"name": "Report Column",

View file

@ -40,6 +40,7 @@
"password_settings",
"logout_on_password_reset",
"force_user_to_reset_password",
"password_reset_limit",
"column_break_31",
"enable_password_policy",
"minimum_password_score",
@ -415,6 +416,13 @@
"fieldtype": "Int",
"label": "Run Jobs only Daily if Inactive For (Days)"
},
{
"default": "3",
"description": "Hourly rate limit for generating password reset links",
"fieldname": "password_reset_limit",
"fieldtype": "Int",
"label": "Password Reset Link Generation Limit"
},
{
"default": "1",
"fieldname": "logout_on_password_reset",

View file

@ -19,6 +19,7 @@ class TestUser(unittest.TestCase):
# 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)
def test_user_type(self):
new_user = frappe.get_doc(dict(doctype='User', email='test-for-type@example.com',
@ -222,6 +223,19 @@ class TestUser(unittest.TestCase):
self.assertEqual(extract_mentions(comment)[0], "test_user@example.com")
self.assertEqual(extract_mentions(comment)[1], "test.again@example1.com")
def test_rate_limiting_for_reset_password(self):
from frappe.utils.password import delete_password_reset_cache
delete_password_reset_cache()
frappe.db.set_value("System Settings", "System Settings", "password_reset_limit", 1)
user = frappe.get_doc("User", "testperm@example.com")
link = user.reset_password()
self.assertRegex(link, "\/update-password\?key=[A-Za-z0-9]*")
self.assertRaises(frappe.ValidationError, user.reset_password, False)
def delete_contact(user):
frappe.db.sql("DELETE FROM `tabContact` WHERE `email_id`= %s", user)
frappe.db.sql("DELETE FROM `tabContact Email` WHERE `email_id`= %s", user)

View file

@ -13,15 +13,16 @@ from frappe.utils.user import get_system_managers
from bs4 import BeautifulSoup
import frappe.permissions
import frappe.share
import re
import json
from frappe.website.utils import is_signup_enabled
from frappe.utils.background_jobs import enqueue
STANDARD_USERS = ("Guest", "Administrator")
class MaxUsersReachedError(frappe.ValidationError): pass
class MaxUsersReachedError(frappe.ValidationError):
pass
class User(Document):
__new_password = None
@ -225,6 +226,11 @@ class User(Document):
def reset_password(self, send_email=False, password_expired=False):
from frappe.utils import random_string, get_url
rate_limit = frappe.db.get_single_value("System Settings", "password_reset_limit")
if rate_limit:
check_password_reset_limit(self.name, rate_limit)
key = random_string(32)
self.db_set("reset_password_key", key)
@ -236,6 +242,7 @@ class User(Document):
if send_email:
self.password_reset_mail(link)
update_password_reset_limit(self.name)
return link
def get_other_system_managers(self):
@ -1110,3 +1117,16 @@ def generate_keys(user):
return {"api_secret": api_secret}
frappe.throw(frappe._("Not Permitted"), frappe.PermissionError)
def update_password_reset_limit(user):
generated_link_count = get_generated_link_count(user)
generated_link_count += 1
frappe.cache().hset("password_reset_link_count", user, generated_link_count)
def check_password_reset_limit(user, rate_limit):
generated_link_count = get_generated_link_count(user)
if generated_link_count >= rate_limit:
frappe.throw(_("You have reached the hourly limit for generating password reset links. Please try again later."))
def get_generated_link_count(user):
return cint(frappe.cache().hget("password_reset_link_count", user)) or 0

View file

@ -30,7 +30,7 @@ frappe.ui.form.on('Data Migration Connector', {
frm.set_value('connector_type', 'Custom');
frm.set_value('python_module', r.message);
frm.save();
frappe.show_alert(__(`New module created ${r.message}`));
frappe.show_alert(__("New module created {0}", [r.message]));
d.hide();
}
});

View file

@ -186,8 +186,8 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-07-28 15:49:54.019073",
"modified_by": "cave@aperture.com",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Data Migration",
"name": "Data Migration Plan",
"name_case": "",

View file

@ -800,12 +800,12 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-07-30 07:02:26.980372",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Data Migration",
"name": "Data Migration Run",
"name_case": "",
"owner": "faris@erpnext.com",
"owner": "Administrator",
"permissions": [
{
"amend": 0,

View file

@ -53,11 +53,11 @@
}
],
"links": [],
"modified": "2020-06-15 11:24:57.639430",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Desk",
"name": "Calendar View",
"owner": "faris@erpnext.com",
"owner": "Administrator",
"permissions": [
{
"create": 1,

View file

@ -169,7 +169,7 @@ frappe.ui.form.on('Dashboard Chart', {
frm.field_options = frappe.report_utils.get_field_options_from_report(data.columns, data);
frm.set_df_property('x_field', 'options', frm.field_options.non_numeric_fields);
if (!frm.field_options.numeric_fields.length) {
frappe.msgprint(__(`Report has no numeric fields, please change the Report Name`));
frappe.msgprint(__("Report has no numeric fields, please change the Report Name"));
} else {
let y_field_df = frappe.meta.get_docfield('Dashboard Chart Field', 'y_field', frm.doc.name);
y_field_df.options = frm.field_options.numeric_fields;

View file

@ -120,8 +120,8 @@
"hide_toolbar": 1,
"in_create": 1,
"links": [],
"modified": "2020-05-31 22:31:12.886950",
"modified_by": "umair@erpnext.com",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Desk",
"name": "Notification Log",
"owner": "Administrator",

View file

@ -207,7 +207,7 @@ frappe.ui.form.on('Number Card', {
frm.field_options = frappe.report_utils.get_field_options_from_report(data.columns, data);
frm.set_df_property('report_field', 'options', frm.field_options.numeric_fields);
if (!frm.field_options.numeric_fields.length) {
frappe.msgprint(__(`Report has no numeric fields, please change the Report Name`));
frappe.msgprint(__("Report has no numeric fields, please change the Report Name"));
}
} else {
frappe.msgprint(__('Report has no data, please modify the filters or change the Report Name'));

View file

@ -169,16 +169,14 @@ def get_comments(doctype, doc_name, frequency, user):
return timeline
def is_document_followed(doctype, doc_name, user):
docs = frappe.get_all(
return frappe.db.exists(
"Document Follow",
filters={
{
"ref_doctype": doctype,
"ref_docname": doc_name,
"user": user
},
limit=1
}
)
return len(docs)
@frappe.whitelist()
def get_follow_users(doctype, doc_name):

View file

@ -23,6 +23,8 @@ def savedocs(doc, action):
# update recent documents
run_onload(doc)
send_updated_docs(doc)
frappe.msgprint(frappe._("Saved"), indicator='green', alert=True)
except Exception:
frappe.errprint(frappe.utils.get_traceback())
raise
@ -36,6 +38,7 @@ def cancel(doctype=None, name=None, workflow_state_fieldname=None, workflow_stat
doc.set(workflow_state_fieldname, workflow_state)
doc.cancel()
send_updated_docs(doc)
frappe.msgprint(frappe._("Cancelled"), indicator='red', alert=True)
except Exception:
frappe.errprint(frappe.utils.get_traceback())

View file

@ -360,11 +360,12 @@ class UserProfile {
this.get_user_rank().then(() => {
this.get_user_points().then(() => {
let html = $(__(`<p class="user-energy-points text-muted">${__('Energy Points: ')}<span class="rank">{0}</span></p>
<p class="user-energy-points text-muted">${__('Review Points: ')}<span class="rank">{1}</span></p>
<p class="user-energy-points text-muted">${__('Rank: ')}<span class="rank">{2}</span></p>
<p class="user-energy-points text-muted">${__('Monthly Rank: ')}<span class="rank">{3}</span></p>
`, [this.energy_points, this.review_points, this.rank, this.month_rank]));
let html = $(`
<p class="user-energy-points text-muted">${__('Energy Points:')} <span class="rank">${this.energy_points}</span></p>
<p class="user-energy-points text-muted">${__('Review Points:')} <span class="rank">${this.review_points}</span></p>
<p class="user-energy-points text-muted">${__('Rank:')} <span class="rank">${this.rank}</span></p>
<p class="user-energy-points text-muted">${__('Monthly Rank:')} <span class="rank">${this.month_rank}</span></p>
`);
$profile_details.append(html);
});

View file

@ -4,25 +4,33 @@
from __future__ import unicode_literals
import frappe
import os, json
import os
import json
from frappe import _
from frappe.modules import scrub, get_module_path
from frappe.utils import flt, cint, get_html_format, get_url_to_form
from frappe.utils import (
flt,
cint,
get_html_format,
get_url_to_form,
gzip_decompress,
format_duration,
)
from frappe.model.utils import render_include
from frappe.translate import send_translations
import frappe.desk.reportview
from frappe.permissions import get_role_permissions
from six import string_types, iteritems
from datetime import timedelta
from frappe.utils import gzip_decompress
from frappe.core.utils import ljust_list
def get_report_doc(report_name):
doc = frappe.get_doc("Report", report_name)
doc.custom_columns = []
if doc.report_type == 'Custom Report':
if doc.report_type == "Custom Report":
custom_report_doc = doc
reference_report = custom_report_doc.reference_report
doc = frappe.get_doc("Report", reference_report)
@ -31,11 +39,18 @@ def get_report_doc(report_name):
doc.is_custom_report = True
if not doc.is_permitted():
frappe.throw(_("You don't have access to Report: {0}").format(report_name), frappe.PermissionError)
frappe.throw(
_("You don't have access to Report: {0}").format(report_name),
frappe.PermissionError,
)
if not frappe.has_permission(doc.ref_doctype, "report"):
frappe.throw(_("You don't have permission to get a report on: {0}").format(doc.ref_doctype),
frappe.PermissionError)
frappe.throw(
_("You don't have permission to get a report on: {0}").format(
doc.ref_doctype
),
frappe.PermissionError,
)
if doc.disabled:
frappe.throw(_("Report {0} is disabled").format(report_name))
@ -55,11 +70,10 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
if report.report_type == "Query Report":
res = report.execute_query_report(filters)
elif report.report_type == 'Script Report':
elif report.report_type == "Script Report":
res = report.execute_script_report(filters)
columns, result, message, chart, report_summary, skip_total_row = \
ljust_list(res, 6)
columns, result, message, chart, report_summary, skip_total_row = ljust_list(res, 6)
if report.custom_columns:
# Original query columns, needed to reorder data as per custom columns
@ -67,7 +81,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
# Reordered columns
columns = json.loads(report.custom_columns)
result = reorder_data_for_custom_columns(columns, query_columns, result, report.report_type)
result = reorder_data_for_custom_columns(columns, query_columns, result)
result = add_data_to_custom_columns(columns, result)
@ -75,7 +89,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
result = add_data_to_custom_columns(custom_columns, result)
for custom_column in custom_columns:
columns.insert(custom_column['insert_after_index'] + 1, custom_column)
columns.insert(custom_column["insert_after_index"] + 1, custom_column)
if result:
result = get_filtered_data(report.ref_doctype, columns, result, user)
@ -91,17 +105,19 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
"report_summary": report_summary,
"skip_total_row": skip_total_row or 0,
"status": None,
"execution_time": frappe.cache().hget('report_execution_time', report.name) or 0
"execution_time": frappe.cache().hget("report_execution_time", report.name)
or 0,
}
@frappe.whitelist()
def background_enqueue_run(report_name, filters=None, user=None):
"""run reports in background"""
if not user:
user = frappe.session.user
report = get_report_doc(report_name)
track_instance = \
frappe.get_doc({
track_instance = frappe.get_doc(
{
"doctype": "Prepared Report",
"report_name": report_name,
# This looks like an insanity but, without this it'd be very hard to find Prepared Reports matching given condition
@ -111,21 +127,24 @@ def background_enqueue_run(report_name, filters=None, user=None):
"report_type": report.report_type,
"query": report.query,
"module": report.module,
})
}
)
track_instance.insert(ignore_permissions=True)
frappe.db.commit()
track_instance.enqueue_report()
return {
"name": track_instance.name,
"redirect_url": get_url_to_form("Prepared Report", track_instance.name)
"redirect_url": get_url_to_form("Prepared Report", track_instance.name),
}
@frappe.whitelist()
def get_script(report_name):
report = get_report_doc(report_name)
module = report.module or frappe.db.get_value("DocType", report.ref_doctype, "module")
module = report.module or frappe.db.get_value(
"DocType", report.ref_doctype, "module"
)
module_path = get_module_path(module)
report_folder = os.path.join(module_path, "report", scrub(report.name))
script_path = os.path.join(report_folder, scrub(report.name) + ".js")
@ -151,24 +170,38 @@ def get_script(report_name):
return {
"script": render_include(script),
"html_format": html_format,
"execution_time": frappe.cache().hget('report_execution_time', report_name) or 0
"execution_time": frappe.cache().hget("report_execution_time", report_name)
or 0,
}
@frappe.whitelist()
@frappe.read_only()
def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None):
def run(
report_name,
filters=None,
user=None,
ignore_prepared_report=False,
custom_columns=None,
):
report = get_report_doc(report_name)
if not user:
user = frappe.session.user
if not frappe.has_permission(report.ref_doctype, "report"):
frappe.msgprint(_("Must have report permission to access this report."),
raise_exception=True)
frappe.msgprint(
_("Must have report permission to access this report."),
raise_exception=True,
)
result = None
if report.prepared_report and not report.disable_prepared_report and not ignore_prepared_report:
if (
report.prepared_report
and not report.disable_prepared_report
and not ignore_prepared_report
and not custom_columns
):
if filters:
if isinstance(filters, string_types):
filters = json.loads(filters)
@ -181,10 +214,13 @@ def run(report_name, filters=None, user=None, ignore_prepared_report=False, cust
else:
result = generate_report_result(report, filters, user, custom_columns)
result["add_total_row"] = report.add_total_row and not result.get('skip_total_row', False)
result["add_total_row"] = report.add_total_row and not result.get(
"skip_total_row", False
)
return result
def add_data_to_custom_columns(columns, result):
custom_fields_data = get_data_for_custom_report(columns)
@ -196,44 +232,42 @@ def add_data_to_custom_columns(columns, result):
if isinstance(row, list):
for idx, column in enumerate(columns):
if column.get('link_field'):
row_obj[column['fieldname']] = None
if column.get("link_field"):
row_obj[column["fieldname"]] = None
row.insert(idx, None)
else:
row_obj[column['fieldname']] = row[idx]
row_obj[column["fieldname"]] = row[idx]
data.append(row_obj)
else:
data.append(row)
for row in data:
for column in columns:
if column.get('link_field'):
fieldname = column['fieldname']
key = (column['doctype'], fieldname)
link_field = column['link_field']
row[fieldname] = custom_fields_data.get(key, {}).get(row.get(link_field))
if column.get("link_field"):
fieldname = column["fieldname"]
key = (column["doctype"], fieldname)
link_field = column["link_field"]
row[fieldname] = custom_fields_data.get(key, {}).get(
row.get(link_field)
)
return data
def reorder_data_for_custom_columns(custom_columns, columns, result, report_type):
def reorder_data_for_custom_columns(custom_columns, columns, result):
if not result:
return []
if report_type == 'Query Report':
# Assume list result for query reports
# Query report columns exclusively use Label
custom_column_labels = [col["label"] for col in custom_columns]
original_column_labels = [col.split(":")[0] for col in columns]
return get_columns_from_list(custom_column_labels, original_column_labels, result)
custom_column_names = [col["fieldname"] for col in custom_columns]
columns = [get_column_as_dict(col) for col in columns]
if isinstance(result[0], list) or isinstance(result[0], tuple):
# If the result is a list of lists
original_column_names = [col["fieldname"] for col in columns]
custom_column_names = [col["label"] for col in custom_columns]
original_column_names = [col["label"] for col in columns]
return get_columns_from_list(custom_column_names, original_column_names, result)
else:
# If the result is a list of dicts
return get_columns_from_dict(custom_column_names, result)
# columns do not need to be reordered if result is a list of dicts
return result
def get_columns_from_list(columns, target_columns, result):
reordered_result = []
@ -251,20 +285,6 @@ def get_columns_from_list(columns, target_columns, result):
return reordered_result
def get_columns_from_dict(columns, result):
reordered_result = []
for res in result:
r = {}
for col_name in columns:
try:
r[col_name] = res[col_name]
except KeyError:
pass
reordered_result.append(r)
return reordered_result
def get_prepared_report_result(report, filters, dn="", user=None):
latest_report_data = {}
@ -274,14 +294,15 @@ def get_prepared_report_result(report, filters, dn="", user=None):
doc = frappe.get_doc("Prepared Report", dn)
else:
# Only look for completed prepared reports with given filters.
doc_list = frappe.get_all("Prepared Report",
doc_list = frappe.get_all(
"Prepared Report",
filters={
"status": "Completed",
"filters": json.dumps(filters),
"owner": user,
"report_name": report.get('custom_report') or report.get('report_name')
"report_name": report.get("custom_report") or report.get("report_name"),
},
order_by = 'creation desc'
order_by="creation desc",
)
if doc_list:
@ -291,11 +312,15 @@ def get_prepared_report_result(report, filters, dn="", user=None):
if doc:
try:
# Prepared Report data is stored in a GZip compressed JSON file
attached_file_name = frappe.db.get_value("File", {"attached_to_doctype": doc.doctype, "attached_to_name":doc.name}, "name")
attached_file = frappe.get_doc('File', attached_file_name)
attached_file_name = frappe.db.get_value(
"File",
{"attached_to_doctype": doc.doctype, "attached_to_name": doc.name},
"name",
)
attached_file = frappe.get_doc("File", attached_file_name)
compressed_content = attached_file.get_content()
uncompressed_content = gzip_decompress(compressed_content)
data = json.loads(uncompressed_content)
data = json.loads(uncompressed_content.decode("utf-8"))
if data:
columns = json.loads(doc.columns) if doc.columns else data[0]
@ -303,23 +328,18 @@ def get_prepared_report_result(report, filters, dn="", user=None):
if isinstance(column, dict) and column.get("label"):
column["label"] = _(column["label"])
latest_report_data = {
"columns": columns,
"result": data
}
latest_report_data = {"columns": columns, "result": data}
except Exception:
frappe.log_error(frappe.get_traceback())
frappe.delete_doc("Prepared Report", doc.name)
frappe.db.commit()
doc = None
latest_report_data.update({
"prepared_report": True,
"doc": doc
})
latest_report_data.update({"prepared_report": True, "doc": doc})
return latest_report_data
@frappe.whitelist()
def export_query():
"""export from query reports"""
@ -335,8 +355,8 @@ def export_query():
if isinstance(data.get("report_name"), string_types):
report_name = data["report_name"]
frappe.permissions.can_export(
frappe.get_cached_value('Report', report_name, 'ref_doctype'),
raise_exception=True
frappe.get_cached_value("Report", report_name, "ref_doctype"),
raise_exception=True,
)
if isinstance(data.get("file_format_type"), string_types):
file_format_type = data["file_format_type"]
@ -353,19 +373,50 @@ def export_query():
data = run(report_name, filters, custom_columns=custom_columns)
data = frappe._dict(data)
if not data.columns:
frappe.respond_as_web_page(_("No data to export"),
_("You can try changing the filters of your report."))
frappe.respond_as_web_page(
_("No data to export"),
_("You can try changing the filters of your report."),
)
return
columns = get_columns_dict(data.columns)
from frappe.utils.xlsxutils import make_xlsx
data["result"] = handle_duration_fieldtype_values(
data.get("result"), data.get("columns")
)
xlsx_data = build_xlsx_data(columns, data, visible_idx, include_indentation)
xlsx_file = make_xlsx(xlsx_data, "Query Report")
frappe.response['filename'] = report_name + '.xlsx'
frappe.response['filecontent'] = xlsx_file.getvalue()
frappe.response['type'] = 'binary'
frappe.response["filename"] = report_name + ".xlsx"
frappe.response["filecontent"] = xlsx_file.getvalue()
frappe.response["type"] = "binary"
def handle_duration_fieldtype_values(result, columns):
for i, col in enumerate(columns):
fieldtype = None
if isinstance(col, string_types):
col = col.split(":")
if len(col) > 1:
if col[1]:
fieldtype = col[1]
if "/" in fieldtype:
fieldtype, options = fieldtype.split("/")
else:
fieldtype = "Data"
else:
fieldtype = col.get("fieldtype")
if fieldtype == "Duration":
for entry in range(0, len(result)):
val_in_seconds = result[entry][i]
if val_in_seconds:
duration_val = format_duration(val_in_seconds)
result[entry][i] = duration_val
return result
def build_xlsx_data(columns, data, visible_idx, include_indentation):
@ -384,12 +435,14 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation):
if isinstance(row, dict) and row:
for idx in range(len(data.columns)):
label = columns[idx]["label"]
fieldname = columns[idx]["fieldname"]
cell_value = row.get(fieldname, row.get(label, ""))
if cint(include_indentation) and 'indent' in row and idx == 0:
cell_value = (' ' * cint(row['indent'])) + cell_value
row_data.append(cell_value)
# check if column is not hidden
if not columns[idx].get("hidden"):
label = columns[idx]["label"]
fieldname = columns[idx]["fieldname"]
cell_value = row.get(fieldname, row.get(label, ""))
if cint(include_indentation) and "indent" in row and idx == 0:
cell_value = (" " * cint(row["indent"])) + cell_value
row_data.append(cell_value)
else:
row_data = row
@ -397,8 +450,9 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation):
return result
def add_total_row(result, columns, meta = None):
total_row = [""]*len(columns)
def add_total_row(result, columns, meta=None):
total_row = [""] * len(columns)
has_percent = []
for i, col in enumerate(columns):
fieldtype, options, fieldname = None, None, None
@ -424,10 +478,13 @@ def add_total_row(result, columns, meta = None):
options = col.get("options")
for row in result:
if i >= len(row): continue
if i >= len(row):
continue
cell = row.get(fieldname) if isinstance(row, dict) else row[i]
if fieldtype in ["Currency", "Int", "Float", "Percent"] and flt(cell):
if fieldtype in ["Currency", "Int", "Float", "Percent", "Duration"] and flt(
cell
):
total_row[i] = flt(total_row[i]) + flt(cell)
if fieldtype == "Percent" and i not in has_percent:
@ -435,12 +492,15 @@ def add_total_row(result, columns, meta = None):
if fieldtype == "Time" and cell:
if not total_row[i]:
total_row[i]=timedelta(hours=0,minutes=0,seconds=0)
total_row[i] = total_row[i] + cell
total_row[i] = timedelta(hours=0, minutes=0, seconds=0)
total_row[i] = total_row[i] + cell
if fieldtype=="Link" and options == "Currency":
total_row[i] = result[0].get(fieldname) if isinstance(result[0], dict) else result[0][i]
if fieldtype == "Link" and options == "Currency":
total_row[i] = (
result[0].get(fieldname)
if isinstance(result[0], dict)
else result[0][i]
)
for i in has_percent:
total_row[i] = flt(total_row[i]) / len(result)
@ -459,35 +519,44 @@ def add_total_row(result, columns, meta = None):
result.append(total_row)
return result
@frappe.whitelist()
def get_data_for_custom_field(doctype, field):
if not frappe.has_permission(doctype, "read"):
frappe.throw(_("Not Permitted"), frappe.PermissionError)
value_map = frappe._dict(frappe.get_all(doctype,
fields=["name", field],
as_list=1))
value_map = frappe._dict(frappe.get_all(doctype, fields=["name", field], as_list=1))
return value_map
def get_data_for_custom_report(columns):
doc_field_value_map = {}
for column in columns:
if column.get('link_field'):
fieldname = column.get('fieldname')
doctype = column.get('doctype')
doc_field_value_map[(doctype, fieldname)] = get_data_for_custom_field(doctype, fieldname)
if column.get("link_field"):
fieldname = column.get("fieldname")
doctype = column.get("doctype")
doc_field_value_map[(doctype, fieldname)] = get_data_for_custom_field(
doctype, fieldname
)
return doc_field_value_map
@frappe.whitelist()
def save_report(reference_report, report_name, columns):
report_doc = get_report_doc(reference_report)
docname = frappe.db.exists("Report",
{'report_name': report_name, 'is_standard': 'No', 'report_type': 'Custom Report'})
docname = frappe.db.exists(
"Report",
{
"report_name": report_name,
"is_standard": "No",
"report_type": "Custom Report",
},
)
if docname:
report = frappe.get_doc("Report", docname)
report.update({"json": columns})
@ -496,15 +565,17 @@ def save_report(reference_report, report_name, columns):
return docname
else:
new_report = frappe.get_doc({
'doctype': 'Report',
'report_name': report_name,
'json': columns,
'ref_doctype': report_doc.ref_doctype,
'is_standard': 'No',
'report_type': 'Custom Report',
'reference_report': reference_report
}).insert(ignore_permissions = True)
new_report = frappe.get_doc(
{
"doctype": "Report",
"report_name": report_name,
"json": columns,
"ref_doctype": report_doc.ref_doctype,
"is_standard": "No",
"report_type": "Custom Report",
"reference_report": reference_report,
}
).insert(ignore_permissions=True)
frappe.msgprint(_("{0} saved successfully").format(new_report.name))
return new_report.name
@ -522,10 +593,22 @@ def get_filtered_data(ref_doctype, columns, data, user):
if match_filters_per_doctype:
for row in data:
# Why linked_doctypes.get(ref_doctype)? because if column is empty, linked_doctypes[ref_doctype] is removed
if linked_doctypes.get(ref_doctype) and shared and row[linked_doctypes[ref_doctype]] in shared:
if (
linked_doctypes.get(ref_doctype)
and shared
and row[linked_doctypes[ref_doctype]] in shared
):
result.append(row)
elif has_match(row, linked_doctypes, match_filters_per_doctype, ref_doctype, if_owner, columns_dict, user):
elif has_match(
row,
linked_doctypes,
match_filters_per_doctype,
ref_doctype,
if_owner,
columns_dict,
user,
):
result.append(row)
else:
result = list(data)
@ -533,17 +616,25 @@ def get_filtered_data(ref_doctype, columns, data, user):
return result
def has_match(row, linked_doctypes, doctype_match_filters, ref_doctype, if_owner, columns_dict, user):
def has_match(
row,
linked_doctypes,
doctype_match_filters,
ref_doctype,
if_owner,
columns_dict,
user,
):
"""Returns True if after evaluating permissions for each linked doctype
- There is an owner match for the ref_doctype
- `and` There is a user permission match for all linked doctypes
- There is an owner match for the ref_doctype
- `and` There is a user permission match for all linked doctypes
Returns True if the row is empty
Returns True if the row is empty
Note:
Each doctype could have multiple conflicting user permission doctypes.
Hence even if one of the sets allows a match, it is true.
This behavior is equivalent to the trickling of user permissions of linked doctypes to the ref doctype.
Note:
Each doctype could have multiple conflicting user permission doctypes.
Hence even if one of the sets allows a match, it is true.
This behavior is equivalent to the trickling of user permissions of linked doctypes to the ref doctype.
"""
resultant_match = True
@ -554,20 +645,22 @@ def has_match(row, linked_doctypes, doctype_match_filters, ref_doctype, if_owner
for doctype, filter_list in doctype_match_filters.items():
matched_for_doctype = False
if doctype==ref_doctype and if_owner:
if doctype == ref_doctype and if_owner:
idx = linked_doctypes.get("User")
if (idx is not None
and row[idx]==user
and columns_dict[idx]==columns_dict.get("owner")):
# owner match is true
matched_for_doctype = True
if (
idx is not None
and row[idx] == user
and columns_dict[idx] == columns_dict.get("owner")
):
# owner match is true
matched_for_doctype = True
if not matched_for_doctype:
for match_filters in filter_list:
match = True
for dt, idx in linked_doctypes.items():
# case handled above
if dt=="User" and columns_dict[idx]==columns_dict.get("owner"):
if dt == "User" and columns_dict[idx] == columns_dict.get("owner"):
continue
cell_value = None
@ -576,7 +669,11 @@ def has_match(row, linked_doctypes, doctype_match_filters, ref_doctype, if_owner
elif isinstance(row, (list, tuple)):
cell_value = row[idx]
if dt in match_filters and cell_value not in match_filters.get(dt) and frappe.db.exists(dt, cell_value):
if (
dt in match_filters
and cell_value not in match_filters.get(dt)
and frappe.db.exists(dt, cell_value)
):
match = False
break
@ -595,6 +692,7 @@ def has_match(row, linked_doctypes, doctype_match_filters, ref_doctype, if_owner
return resultant_match
def get_linked_doctypes(columns, data):
linked_doctypes = {}
@ -602,7 +700,7 @@ def get_linked_doctypes(columns, data):
for idx, col in enumerate(columns):
df = columns_dict[idx]
if df.get("fieldtype")=="Link":
if df.get("fieldtype") == "Link":
if data and isinstance(data[0], (list, tuple)):
linked_doctypes[df["options"]] = idx
else:
@ -631,38 +729,45 @@ def get_linked_doctypes(columns, data):
return linked_doctypes
def get_columns_dict(columns):
"""Returns a dict with column docfield values as dict
The keys for the dict are both idx and fieldname,
so either index or fieldname can be used to search for a column's docfield properties
The keys for the dict are both idx and fieldname,
so either index or fieldname can be used to search for a column's docfield properties
"""
columns_dict = frappe._dict()
for idx, col in enumerate(columns):
col_dict = frappe._dict()
# string
if isinstance(col, string_types):
col = col.split(":")
if len(col) > 1:
if "/" in col[1]:
col_dict["fieldtype"], col_dict["options"] = col[1].split("/")
else:
col_dict["fieldtype"] = col[1]
col_dict["label"] = col[0]
col_dict["fieldname"] = frappe.scrub(col[0])
# dict
else:
col_dict.update(col)
if "fieldname" not in col_dict:
col_dict["fieldname"] = frappe.scrub(col_dict["label"])
col_dict = get_column_as_dict(col)
columns_dict[idx] = col_dict
columns_dict[col_dict["fieldname"]] = col_dict
return columns_dict
def get_column_as_dict(col):
col_dict = frappe._dict()
# string
if isinstance(col, string_types):
col = col.split(":")
if len(col) > 1:
if "/" in col[1]:
col_dict["fieldtype"], col_dict["options"] = col[1].split("/")
else:
col_dict["fieldtype"] = col[1]
col_dict["label"] = col[0]
col_dict["fieldname"] = frappe.scrub(col[0])
# dict
else:
col_dict.update(col)
if "fieldname" not in col_dict:
col_dict["fieldname"] = frappe.scrub(col_dict["label"])
return col_dict
def get_user_match_filters(doctypes, user):
match_filters = {}

View file

@ -11,7 +11,7 @@ from frappe.model.db_query import DatabaseQuery
from frappe import _
from six import string_types, StringIO
from frappe.core.doctype.access_log.access_log import make_access_log
from frappe.utils import cstr
from frappe.utils import cstr, format_duration
@frappe.whitelist()
@ -167,6 +167,8 @@ def export_query():
for i, row in enumerate(ret):
data.append([i+1] + list(row))
data = handle_duration_fieldtype_values(doctype, data, db_query.fields)
if file_format_type == "CSV":
# convert to csv
@ -236,6 +238,29 @@ def get_labels(fields, doctype):
return labels
def handle_duration_fieldtype_values(doctype, data, fields):
for field in fields:
key = field.split(" as ")[0]
if key.startswith(('count(', 'sum(', 'avg(')): continue
if "." in key:
parenttype, fieldname = key.split(".")[0][4:-1], key.split(".")[1].strip("`")
else:
parenttype = doctype
fieldname = field.strip("`")
df = frappe.get_meta(parenttype).get_field(fieldname)
if df and df.fieldtype == 'Duration':
index = fields.index(field) + 1
for i in range(1, len(data)):
val_in_seconds = data[i][index]
if val_in_seconds:
duration_val = format_duration(val_in_seconds, df.hide_days)
data[i][index] = duration_val
return data
@frappe.whitelist()
def delete_items():
"""delete selected items"""

View file

@ -3,23 +3,7 @@
frappe.ui.form.on('Auto Email Report', {
refresh: function(frm) {
if(frm.doc.report_type !== 'Report Builder') {
if(frm.script_setup_for !== frm.doc.report && !frm.doc.__islocal) {
frappe.call({
method:"frappe.desk.query_report.get_script",
args: {
report_name: frm.doc.report
},
callback: function(r) {
frappe.dom.eval(r.message.script || "");
frm.script_setup_for = frm.doc.report;
frm.trigger('show_filters');
}
});
} else {
frm.trigger('show_filters');
}
}
frm.trigger('fetch_report_filters');
if(!frm.is_new()) {
frm.add_custom_button(__('Download'), function() {
var w = window.open(
@ -50,6 +34,27 @@ frappe.ui.form.on('Auto Email Report', {
},
report: function(frm) {
frm.set_value('filters', '');
frm.trigger('fetch_report_filters');
},
fetch_report_filters(frm) {
if (frm.doc.report
&& frm.doc.report_type !== 'Report Builder'
&& frm.script_setup_for !== frm.doc.report
) {
frappe.call({
method: "frappe.desk.query_report.get_script",
args: {
report_name: frm.doc.report
},
callback: function(r) {
frappe.dom.eval(r.message.script || "");
frm.script_setup_for = frm.doc.report;
frm.trigger('show_filters');
}
});
} else {
frm.trigger('show_filters');
}
},
show_filters: function(frm) {
var wrapper = $(frm.get_field('filters_display').wrapper);

View file

@ -1,181 +1,78 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2019-01-09 16:39:23.746535",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"creation": "2019-01-09 16:39:23.746535",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"ref_doctype",
"ref_docname",
"user"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "ref_doctype",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Doctype",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "ref_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Doctype",
"options": "DocType",
"reqd": 1,
"search_index": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "ref_docname",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Document Name",
"length": 0,
"no_copy": 0,
"options": "ref_doctype",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "ref_docname",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Document Name",
"options": "ref_doctype",
"reqd": 1,
"search_index": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "user",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "User",
"length": 0,
"no_copy": 0,
"options": "User",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldname": "user",
"fieldtype": "Link",
"in_list_view": 1,
"label": "User",
"options": "User",
"reqd": 1,
"search_index": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 1,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-02-26 15:43:44.330348",
"modified_by": "Administrator",
"module": "Email",
"name": "Document Follow",
"name_case": "",
"owner": "Administrator",
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-09-17 09:19:28.496453",
"modified_by": "Administrator",
"module": "Email",
"name": "Document Follow",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 1,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0,
"track_views": 0
],
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC"
}

View file

@ -4,12 +4,13 @@
frappe.ui.form.on("Email Queue", {
refresh: function(frm) {
if (["Not Sent","Partially Sent"].indexOf(frm.doc.status)!=-1) {
frm.add_custom_button("Send Now", function() {
let button = frm.add_custom_button("Send Now", function() {
frappe.call({
method: 'frappe.email.doctype.email_queue.email_queue.send_now',
args: {
name: frm.doc.name
},
btn: button,
callback: function() {
frm.reload_doc();
}
@ -18,12 +19,13 @@ frappe.ui.form.on("Email Queue", {
}
if (["Error","Partially Errored"].indexOf(frm.doc.status)!=-1) {
frm.add_custom_button("Retry Sending", function() {
let button = frm.add_custom_button("Retry Sending", function() {
frm.call({
method: "retry_sending",
args: {
name: frm.doc.name
},
btn: button,
callback: function(r) {
if (!r.exc) {
frm.set_value("status", "Not Sent");

View file

@ -102,7 +102,7 @@ frappe.notification = {
<h5>Message Example</h5>
<pre>
Your {{ doc.name }} order of {{ doc.total }} has shipped and should be delivered on {{ doc.date }}. Details : {{doc.customer}}
Your appointment is coming up on {{ doc.date }} at {{ doc.time }}
</pre>`;
} else if (frm.doc.channel === 'Email') {
template = `<h5>Message Example</h5>
@ -166,6 +166,7 @@ frappe.ui.form.on('Notification', {
},
refresh: function(frm) {
frappe.notification.setup_fieldname_select(frm);
frappe.notification.setup_example_message(frm);
frm.get_field('is_standard').toggle(frappe.boot.developer_mode);
frm.trigger('event');
},

View file

@ -67,7 +67,7 @@
},
{
"depends_on": "eval:doc.channel=='Slack'",
"description": "To use Slack Channel, add a <a href=\"\\#Form/Slack Webhook URL\">Slack Webhook URL</a>.",
"description": "To use Slack Channel, add a <a href=\"#List/Slack%20Webhook%20URL/List\">Slack Webhook URL</a>.",
"fieldname": "slack_webhook_url",
"fieldtype": "Link",
"label": "Slack Channel",
@ -269,6 +269,7 @@
"fieldname": "twilio_number",
"fieldtype": "Link",
"label": "Twilio Number",
"mandatory_depends_on": "eval: doc.channel==='WhatsApp'",
"options": "Twilio Number Group"
},
{
@ -290,7 +291,7 @@
"icon": "fa fa-envelope",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-09-01 18:36:22.550891",
"modified": "2020-09-03 10:33:23.084590",
"modified_by": "Administrator",
"module": "Email",
"name": "Notification",

View file

@ -43,6 +43,7 @@ class Notification(Document):
self.validate_forbidden_types()
self.validate_condition()
self.validate_standard()
self.validate_twilio_settings()
frappe.cache().hdel('notifications', self.document_type)
def on_update(self):
@ -69,6 +70,11 @@ def get_context(context):
if self.is_standard and not frappe.conf.developer_mode:
frappe.throw(_('Cannot edit Standard Notification. To edit, please disable this and duplicate it'))
def validate_twilio_settings(self):
if self.enabled and self.channel == "WhatsApp" \
and not frappe.db.get_single_value("Twilio Settings", "enabled"):
frappe.throw(_("Please enable Twilio settings to send WhatsApp messages"))
def validate_condition(self):
temp_doc = frappe.new_doc(self.document_type)
if self.condition:
@ -149,7 +155,12 @@ def get_context(context):
allow_update = False
try:
if allow_update and not doc.flags.in_notification_update:
doc.set(self.set_property_after_alert, self.property_value)
fieldname = self.set_property_after_alert
value = self.property_value
if doc.meta.get_field(fieldname).fieldtype in frappe.model.numeric_fieldtypes:
value = frappe.utils.cint(value)
doc.set(fieldname, value)
doc.flags.updater_reference = {
'doctype': self.doctype,
'docname': self.name,
@ -167,9 +178,14 @@ def get_context(context):
subject = frappe.render_template(self.subject, context)
attachments = self.get_attachment(doc)
recipients, cc, bcc = self.get_list_of_recipients(doc, context)
users = recipients + cc + bcc
if not users:
return
notification_doc = {
'type': 'Alert',
'document_type': doc.doctype,
@ -274,8 +290,6 @@ def get_context(context):
if self.send_to_all_assignees:
recipients = recipients + get_assignees(doc)
if not recipients and not cc and not bcc:
return None, None, None
return list(set(recipients)), list(set(cc)), list(set(bcc))
def get_receiver_list(self, doc, context):
@ -425,4 +439,4 @@ def get_assignees(doc):
recipients = [d.owner for d in assignees]
return recipients
return recipients

View file

@ -207,7 +207,7 @@ class EMail:
def set_in_reply_to(self, in_reply_to):
"""Used to send the Message-Id of a received email back as In-Reply-To"""
self.msg_root["In-Reply-To"] = in_reply_to.replace("\r", "").replace("\n", "")
self.set_header('In-Reply-To', in_reply_to)
def make(self):
"""build into msg_root"""
@ -234,7 +234,10 @@ class EMail:
if key in self.msg_root:
del self.msg_root[key]
self.msg_root[key] = value
try:
self.msg_root[key] = value
except ValueError:
self.msg_root[key] = sanitize_email_header(value)
def as_string(self):
"""validate, build message and convert to string"""
@ -458,3 +461,6 @@ def get_header(header=None):
})
return email_header
def sanitize_email_header(str):
return str.replace('\r', '').replace('\n', '')

View file

@ -65,16 +65,21 @@ def execute_cmd(cmd, from_async=False):
method = method.queue
is_whitelisted(method)
is_valid_http_method(method)
return frappe.call(method, **frappe.form_dict)
def is_valid_http_method(method):
http_method = frappe.local.request.method
if http_method not in frappe.allowed_http_methods_for_whitelisted_func[method]:
frappe.throw(_("Not permitted"), frappe.PermissionError)
def is_whitelisted(method):
# check if whitelisted
if frappe.session['user'] == 'Guest':
if (method not in frappe.guest_methods):
frappe.msgprint(_("Not permitted"))
raise frappe.PermissionError('Not Allowed, {0}'.format(method))
frappe.throw(_("Not permitted"), frappe.PermissionError)
if method not in frappe.xss_safe_methods:
# strictly sanitize form_dict
@ -85,8 +90,7 @@ def is_whitelisted(method):
else:
if not method in frappe.whitelisted:
frappe.msgprint(_("Not permitted"))
raise frappe.PermissionError('Not Allowed, {0}'.format(method))
frappe.throw(_("Not permitted"), frappe.PermissionError)
@frappe.whitelist(allow_guest=True)
def version():

View file

@ -43,6 +43,11 @@ app_include_css = [
"assets/css/report.min.css",
]
doctype_js = {
"Web Page": "public/js/frappe/utils/web_template.js",
"Website Settings": "public/js/frappe/utils/web_template.js"
}
web_include_js = [
"website_script.js"
]
@ -138,6 +143,7 @@ doc_events = {
"frappe.automation.doctype.milestone_tracker.milestone_tracker.evaluate_milestone",
"frappe.core.doctype.file.file.attach_files_to_document",
"frappe.event_streaming.doctype.event_update_log.event_update_log.notify_consumers",
"frappe.automation.doctype.assignment_rule.assignment_rule.update_due_date",
],
"after_rename": "frappe.desk.notifications.clear_doctype_notifications",
"on_cancel": [
@ -196,7 +202,8 @@ scheduler_events = {
"frappe.deferred_insert.save_to_db",
"frappe.desk.form.document_follow.send_hourly_updates",
"frappe.integrations.doctype.google_calendar.google_calendar.sync",
"frappe.email.doctype.newsletter.newsletter.send_scheduled_email"
"frappe.email.doctype.newsletter.newsletter.send_scheduled_email",
"frappe.utils.password.delete_password_reset_cache"
],
"daily": [
"frappe.email.queue.clear_outbox",
@ -276,6 +283,7 @@ setup_wizard_exception = [
]
before_migrate = ['frappe.patches.v11_0.sync_user_permission_doctype_before_migrate.execute']
after_migrate = ['frappe.website.doctype.website_theme.website_theme.after_migrate']
otp_methods = ['OTP App','Email','SMS']
user_privacy_documents = [

View file

@ -1,29 +1,17 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
# called from wnf.py
# lib/wnf.py --install [rootpassword] [dbname] [source]
import json
import os
from __future__ import unicode_literals, print_function
from six.moves import input
import os, json, subprocess, shutil
import click
import frappe
import frappe.database
import importlib
from frappe import _
from frappe.model.sync import sync_for
from frappe.utils.fixtures import sync_fixtures
from frappe.website import render
from frappe.modules.utils import sync_customizations
from frappe.database import setup_database
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
def install_db(root_login="root", root_password=None, db_name=None, source_sql=None,
admin_password=None, verbose=True, force=0, site_config=None, reinstall=False,
db_password=None, db_type=None, db_host=None, db_port=None, no_mariadb_socket=False):
import frappe.database
from frappe.database import setup_database
if not db_type:
db_type = frappe.conf.db_type or 'mariadb'
@ -45,7 +33,13 @@ def install_db(root_login="root", root_password=None, db_name=None, source_sql=N
frappe.flags.in_install_db = False
def install_app(name, verbose=False, set_as_patched=True):
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
from frappe.utils.fixtures import sync_fixtures
from frappe.model.sync import sync_for
from frappe.modules.utils import sync_customizations
frappe.flags.in_install = name
frappe.flags.ignore_in_install = False
@ -65,7 +59,7 @@ def install_app(name, verbose=False, set_as_patched=True):
raise Exception("App not in apps.txt")
if name in installed_apps:
frappe.msgprint(_("App {0} already installed").format(name))
frappe.msgprint(frappe._("App {0} already installed").format(name))
return
print("\nInstalling {0}...".format(name))
@ -102,25 +96,31 @@ def install_app(name, verbose=False, set_as_patched=True):
frappe.flags.in_install = False
def add_to_installed_apps(app_name, rebuild_website=True):
installed_apps = frappe.get_installed_apps()
if not app_name in installed_apps:
installed_apps.append(app_name)
frappe.db.set_global("installed_apps", json.dumps(installed_apps))
frappe.db.commit()
post_install(rebuild_website)
if frappe.flags.in_install:
post_install(rebuild_website)
def remove_from_installed_apps(app_name):
installed_apps = frappe.get_installed_apps()
if app_name in installed_apps:
installed_apps.remove(app_name)
frappe.db.set_value("DefaultValue", {"defkey": "installed_apps"}, "defvalue", json.dumps(installed_apps))
frappe.db.set_global("installed_apps", json.dumps(installed_apps))
frappe.get_single("Installed Applications").update_versions()
frappe.db.commit()
if frappe.flags.in_install:
post_install()
def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False):
"""Remove app and all linked to the app's module with the app from a site."""
import click
# dont allow uninstall app if not installed unless forced
if not force:
@ -143,11 +143,12 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
frappe.flags.in_uninstall = True
drop_doctypes = []
# remove modules, doctypes, roles
for module_name in frappe.get_module_list(app_name):
for doctype in frappe.get_list("DocType", filters={"module": module_name},
fields=["name", "issingle"]):
print("removing DocType {0}...".format(doctype.name))
modules = (x.name for x in frappe.get_all("Module Def", filters={"app_name": app_name}))
for module_name in modules:
print("Deleting Module '{0}'".format(module_name))
for doctype in frappe.get_list("DocType", filters={"module": module_name}, fields=["name", "issingle"]):
print("* removing DocType '{0}'...".format(doctype.name))
if not dry_run:
frappe.delete_doc("DocType", doctype.name)
@ -155,35 +156,36 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
if not doctype.issingle:
drop_doctypes.append(doctype.name)
linked_doctypes = frappe.get_all("DocField", filters={"fieldtype": "Link", "options": "Module Def"}, fields=['parent'])
ordered_doctypes = ["Desk Page", "Report", "Page", "Web Form"]
doctypes_with_linked_modules = ordered_doctypes + [doctype.parent for doctype in linked_doctypes if doctype.parent not in ordered_doctypes]
for doctype in doctypes_with_linked_modules:
for record in frappe.get_list(doctype, filters={"module": module_name}):
print("removing {0} {1}...".format(doctype, record.name))
print("* removing {0} '{1}'...".format(doctype, record.name))
if not dry_run:
frappe.delete_doc(doctype, record.name)
print("removing Module {0}...".format(module_name))
print("* removing Module Def '{0}'...".format(module_name))
if not dry_run:
frappe.delete_doc("Module Def", module_name)
remove_from_installed_apps(app_name)
if not dry_run:
# drop tables after a commit
frappe.db.commit()
remove_from_installed_apps(app_name)
for doctype in set(drop_doctypes):
print("* dropping Table for '{0}'...".format(doctype))
frappe.db.sql("drop table `tab{0}`".format(doctype))
frappe.db.commit()
click.secho("Uninstalled App {0} from Site {1}".format(app_name, frappe.local.site), fg="green")
frappe.flags.in_uninstall = False
def post_install(rebuild_website=False):
from frappe.website import render
if rebuild_website:
render.clear_cache()
@ -191,6 +193,7 @@ def post_install(rebuild_website=False):
frappe.db.commit()
frappe.clear_cache()
def set_all_patches_as_completed(app):
patch_path = os.path.join(frappe.get_pymodule_path(app), "patches.txt")
if os.path.exists(patch_path):
@ -201,6 +204,7 @@ def set_all_patches_as_completed(app):
}).insert(ignore_permissions=True)
frappe.db.commit()
def init_singles():
singles = [single['name'] for single in frappe.get_all("DocType", filters={'issingle': True})]
for single in singles:
@ -210,6 +214,7 @@ def init_singles():
doc.flags.ignore_validate=True
doc.save()
def make_conf(db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None):
site = frappe.local.site
make_site_config(db_name, db_password, site_config, db_type=db_type, db_host=db_host, db_port=db_port)
@ -217,6 +222,7 @@ def make_conf(db_name=None, db_password=None, site_config=None, db_type=None, db
frappe.destroy()
frappe.init(site, sites_path=sites_path)
def make_site_config(db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None):
frappe.create_folder(os.path.join(frappe.local.site_path))
site_file = get_site_config_path()
@ -237,6 +243,7 @@ def make_site_config(db_name=None, db_password=None, site_config=None, db_type=N
with open(site_file, "w") as f:
f.write(json.dumps(site_config, indent=1, sort_keys=True))
def update_site_config(key, value, validate=True, site_config_path=None):
"""Update a value in site_config"""
if not site_config_path:
@ -266,9 +273,11 @@ def update_site_config(key, value, validate=True, site_config_path=None):
if hasattr(frappe.local, "conf"):
frappe.local.conf[key] = value
def get_site_config_path():
return os.path.join(frappe.local.site_path, "site_config.json")
def get_conf_params(db_name=None, db_password=None):
if not db_name:
db_name = input("Database Name: ")
@ -281,6 +290,7 @@ def get_conf_params(db_name=None, db_password=None):
return {"db_name": db_name, "db_password": db_password}
def make_site_dirs():
site_public_path = os.path.join(frappe.local.site_path, 'public')
site_private_path = os.path.join(frappe.local.site_path, 'private')
@ -296,6 +306,7 @@ def make_site_dirs():
if not os.path.exists(locks_dir):
os.makedirs(locks_dir)
def add_module_defs(app):
modules = frappe.get_module_list(app)
for module in modules:
@ -304,7 +315,10 @@ def add_module_defs(app):
d.module_name = module
d.save(ignore_permissions=True)
def remove_missing_apps():
import importlib
apps = ('frappe_subscription', 'shopping_cart')
installed_apps = json.loads(frappe.db.get_global("installed_apps") or "[]")
for app in apps:
@ -316,7 +330,10 @@ def remove_missing_apps():
installed_apps.remove(app)
frappe.db.set_global("installed_apps", json.dumps(installed_apps))
def extract_sql_gzip(sql_gz_path):
import subprocess
try:
# dvf - decompress, verbose, force
original_file = sql_gz_path
@ -328,7 +345,10 @@ def extract_sql_gzip(sql_gz_path):
return decompressed_file
def extract_tar_files(site_name, file_path, folder_name):
def extract_files(site_name, file_path, folder_name):
import subprocess
import shutil
# Need to do frappe.init to maintain the site locals
frappe.init(site=site_name)
abs_site_path = os.path.abspath(frappe.get_site_path())
@ -341,7 +361,10 @@ def extract_tar_files(site_name, file_path, folder_name):
tar_path = os.path.join(abs_site_path, tar_name)
try:
subprocess.check_output(['tar', 'xvf', tar_path, '--strip', '2'], cwd=abs_site_path)
if file_path.endswith(".tar"):
subprocess.check_output(['tar', 'xvf', tar_path, '--strip', '2'], cwd=abs_site_path)
elif file_path.endswith(".tgz"):
subprocess.check_output(['tar', 'zxvf', tar_path, '--strip', '2'], cwd=abs_site_path)
except:
raise
finally:
@ -349,6 +372,7 @@ def extract_tar_files(site_name, file_path, folder_name):
return tar_path
def is_downgrade(sql_file_path, verbose=False):
"""checks if input db backup will get downgraded on current bench"""
from semantic_version import Version

View file

@ -97,7 +97,7 @@ def backup_to_dropbox(upload_db_backup=True):
if frappe.flags.create_new_backup:
backup = new_backup(ignore_files=True)
filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db))
site_config = os.path.join(get_backups_path(), os.path.basename(backup.site_config_backup_path))
site_config = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_conf))
else:
filename, site_config = get_latest_backup_file()

View file

@ -97,8 +97,8 @@
"label": "Push to Google Contacts"
}
],
"modified": "2019-09-13 15:53:19.569924",
"modified_by": "himanshu@erpnext.com",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Google Contacts",
"owner": "Administrator",

View file

@ -100,8 +100,8 @@
}
],
"issingle": 1,
"modified": "2019-08-21 17:33:28.516614",
"modified_by": "qwe@qwe.com",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Google Drive",
"owner": "Administrator",

View file

@ -191,7 +191,7 @@ def upload_system_backup_to_google_drive():
backup = new_backup()
file_urls = []
file_urls.append(backup.backup_path_db)
file_urls.append(backup.site_config_backup_path)
file_urls.append(backup.backup_path_conf)
if account.file_backup:
file_urls.append(backup.backup_path_files)

View file

@ -118,7 +118,7 @@ def backup_to_s3():
backup = new_backup(ignore_files=False, backup_path_db=None,
backup_path_files=None, backup_path_private_files=None, force=True)
db_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db))
site_config = os.path.join(get_backups_path(), os.path.basename(backup.site_config_backup_path))
site_config = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_conf))
if backup_files:
files_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_files))
private_files = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_private_files))

View file

@ -5,6 +5,7 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enabled",
"account_sid",
"auth_token",
"column_break_2",
@ -14,12 +15,14 @@
{
"fieldname": "account_sid",
"fieldtype": "Data",
"label": "Account SID"
"label": "Account SID",
"mandatory_depends_on": "eval: doc.enabled"
},
{
"fieldname": "auth_token",
"fieldtype": "Password",
"label": "Auth Token"
"label": "Auth Token",
"mandatory_depends_on": "eval: doc.enabled"
},
{
"fieldname": "column_break_2",
@ -30,11 +33,18 @@
"fieldtype": "Table",
"label": "Twilio Number",
"options": "Twilio Number Group"
},
{
"default": "0",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2020-08-11 15:28:57.860554",
"modified": "2020-09-03 10:17:21.318743",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Twilio Settings",

View file

@ -5,14 +5,16 @@
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from twilio.rest import Client
from frappe import _
from frappe.utils.password import get_decrypted_password
from twilio.rest import Client
from six import string_types
from json import loads
class TwilioSettings(Document):
def validate(self):
self.validate_twilio_credentials()
def on_update(self):
if self.enabled:
self.validate_twilio_credentials()
def validate_twilio_credentials(self):
try:
@ -23,14 +25,15 @@ class TwilioSettings(Document):
frappe.throw(_("Invalid Account SID or Auth Token."))
def send_whatsapp_message(sender, receiver_list, message):
import json
twilio_settings = frappe.get_doc("Twilio Settings")
if not twilio_settings.enabled:
frappe.throw(_("Please enable twilio settings before sending WhatsApp messages"))
if isinstance(receiver_list, string_types):
receiver_list = json.loads(receiver_list)
receiver_list = loads(receiver_list)
if not isinstance(receiver_list, list):
receiver_list = [receiver_list]
twilio_settings = frappe.get_doc("Twilio Settings")
auth_token = get_decrypted_password("Twilio Settings", "Twilio Settings", 'auth_token')
client = Client(twilio_settings.account_sid, auth_token)
args = {

View file

@ -19,7 +19,7 @@ def send_email(success, service_name, doctype, email_field, error_status=None):
return
if success:
if not frappe.db.get_value(doctype, None, "send_email_for_successful_backup"):
if not frappe.db.get_single_value(doctype, "send_email_for_successful_backup"):
return
subject = "Backup Upload Successful"
@ -28,7 +28,6 @@ def send_email(success, service_name, doctype, email_field, error_status=None):
<p>Hi there, this is just to inform you that your backup was successfully uploaded to your {0} bucket. So relax!</p>""".format(
service_name
)
else:
subject = "[Warning] Backup Upload Failed"
message = """

View file

@ -22,8 +22,8 @@ from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
from frappe.search.website_search import build_index_for_all_routes
def migrate(verbose=True, rebuild_website=False, skip_failing=False, skip_search_index=False):
'''Migrate all apps to the latest version, will:
def migrate(verbose=True, skip_failing=False, skip_search_index=False):
'''Migrate all apps to the current version, will:
- run before migrate hooks
- run patches
- sync doctypes (schema)

View file

@ -26,18 +26,16 @@ max_positive_value = {
DOCTYPES_FOR_DOCTYPE = ('DocType', 'DocField', 'DocPerm', 'DocType Action', 'DocType Link')
_classes = {}
def get_controller(doctype):
"""Returns the **class** object of the given DocType.
For `custom` type, returns `frappe.model.document.Document`.
:param doctype: DocType name as string."""
from frappe.model.document import Document
from frappe.utils.nestedset import NestedSet
global _classes
if not doctype in _classes:
def _get_controller():
from frappe.model.document import Document
from frappe.utils.nestedset import NestedSet
module_name, custom = frappe.db.get_value("DocType", doctype, ("module", "custom"), cache=True) \
or ["Core", False]
@ -48,8 +46,17 @@ def get_controller(doctype):
is_tree = False
_class = NestedSet if is_tree else Document
else:
module = load_doctype_module(doctype, module_name)
classname = doctype.replace(" ", "").replace("-", "")
class_overrides = frappe.get_hooks('override_doctype_class')
if class_overrides and class_overrides.get(doctype):
import_path = frappe.get_hooks('override_doctype_class').get(doctype)[-1]
module_path, classname = import_path.rsplit('.', 1)
module = frappe.get_module(module_path)
if not hasattr(module, classname):
raise ImportError('{0}: {1} does not exist in module {2}'.format(doctype, classname, module_path))
else:
module = load_doctype_module(doctype, module_name)
classname = doctype.replace(" ", "").replace("-", "")
if hasattr(module, classname):
_class = getattr(module, classname)
if issubclass(_class, BaseDocument):
@ -58,9 +65,13 @@ def get_controller(doctype):
raise ImportError(doctype)
else:
raise ImportError(doctype)
_classes[doctype] = _class
return _class
return _classes[doctype]
if frappe.local.dev_server:
return _get_controller()
key = '{}:doctype_classes'.format(frappe.local.site)
return frappe.cache().hget(key, doctype, generator=_get_controller, shared=True)
class BaseDocument(object):
ignore_in_getter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns")
@ -335,6 +346,9 @@ class BaseDocument(object):
if frappe.db.is_primary_key_violation(e):
if self.meta.autoname=="hash":
# hash collision? try again
frappe.flags.retry_count = (frappe.flags.retry_count or 0) + 1
if frappe.flags.retry_count > 5 and not frappe.flags.in_test:
raise
self.name = None
self.db_insert()
return

View file

@ -37,7 +37,8 @@ class DatabaseQuery(object):
ignore_permissions=False, user=None, with_comment_count=False,
join='left join', distinct=False, start=None, page_length=None, limit=None,
ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False,
update=None, add_total_row=None, user_settings=None, reference_doctype=None, return_query=False, strict=True):
update=None, add_total_row=None, user_settings=None, reference_doctype=None,
return_query=False, strict=True, pluck=None):
if not ignore_permissions and not frappe.has_permission(self.doctype, "read", user=user):
frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(self.doctype))
raise frappe.PermissionError(self.doctype)
@ -57,7 +58,10 @@ class DatabaseQuery(object):
if fields:
self.fields = fields
else:
self.fields = ["`tab{0}`.`name`".format(self.doctype)]
if pluck:
self.fields = ["`tab{0}`.`{1}`".format(self.doctype, pluck)]
else:
self.fields = ["`tab{0}`.`name`".format(self.doctype)]
if start: limit_start = start
if page_length: limit_page_length = page_length
@ -104,6 +108,9 @@ class DatabaseQuery(object):
self.save_user_settings_fields = save_user_settings_fields
self.update_user_settings()
if pluck:
return [d[pluck] for d in result]
return result
def build_and_run(self):
@ -162,7 +169,18 @@ class DatabaseQuery(object):
self.set_field_tables()
args.fields = ', '.join(self.fields)
fields = []
for field in self.fields:
if field.strip().startswith(("`", "*", '"', "'")) or "(" in field:
fields.append(field)
elif "as" in field.lower().split(" "):
col, _, new = field.split()[-3:]
fields.append("`{0}` as {1}".format(col, new))
else:
fields.append("`{0}`".format(field))
args.fields = ", ".join(fields)
self.set_order_by(args)
@ -391,7 +409,10 @@ class DatabaseQuery(object):
ref_doctype = frappe.get_meta(f.doctype).get_field(f.fieldname).options
result=[]
lft, rgt = frappe.db.get_value(ref_doctype, f.value, ["lft", "rgt"])
lft, rgt = '', ''
if f.value:
lft, rgt = frappe.db.get_value(ref_doctype, f.value, ["lft", "rgt"])
# Get descendants elements of a DocType with a tree structure
if f.operator.lower() in ('descendants of', 'not descendants of') :

View file

@ -7,6 +7,7 @@ from frappe import _
from frappe.utils import now_datetime, cint, cstr
import re
from six import string_types
from frappe.model import log_types
def set_new_name(doc):
@ -35,7 +36,13 @@ def set_new_name(doc):
elif getattr(doc.meta, "issingle", False):
doc.name = doc.doctype
else:
elif getattr(doc.meta, "istable", False):
doc.name = make_autoname("hash", doc.doctype)
if not doc.name:
set_naming_from_document_naming_rule(doc)
if not doc.name:
doc.run_method("autoname")
if not doc.name and autoname:
@ -43,12 +50,15 @@ def set_new_name(doc):
# if the autoname option is 'field:' and no name was derived, we need to
# notify
if autoname.startswith("field:") and not doc.name:
if not doc.name and autoname.startswith("field:"):
fieldname = autoname[6:]
frappe.throw(_("{0} is required").format(doc.meta.get_label(fieldname)))
# at this point, we fall back to name generation with the hash option
if not doc.name or autoname == "hash":
if not doc.name and autoname == "hash":
doc.name = make_autoname("hash", doc.doctype)
if not doc.name:
doc.name = make_autoname("hash", doc.doctype)
doc.name = validate_name(
@ -76,6 +86,23 @@ def set_name_from_naming_options(autoname, doc):
elif "#" in autoname:
doc.name = make_autoname(autoname, doc=doc)
def set_naming_from_document_naming_rule(doc):
'''
Evaluate rules based on "Document Naming Series" doctype
'''
if doc.doctype in log_types:
return
try:
for d in frappe.get_all('Document Naming Rule',
dict(document_type=doc.doctype, disabled=0), order_by='priority desc'):
frappe.get_cached_doc('Document Naming Rule', d.name).apply(doc)
if doc.name:
break
except frappe.db.TableMissingError: # noqa: E722
# not yet bootstrapped
pass
def set_name_by_naming_series(doc):
"""Sets name by the `naming_series` property"""
if not doc.naming_series:

View file

@ -13,7 +13,7 @@ from oauthlib.oauth2.rfc6749.endpoints.token import TokenEndpoint
from oauthlib.oauth2.rfc6749.endpoints.resource import ResourceEndpoint
from oauthlib.oauth2.rfc6749.endpoints.revocation import RevocationEndpoint
from oauthlib.common import Request
from six.moves.urllib.parse import parse_qs, urlparse, unquote
from six.moves.urllib.parse import unquote
def get_url_delimiter(separator_character=" "):
return separator_character
@ -94,19 +94,13 @@ class OAuthWebRequestValidator(RequestValidator):
def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
# Is the client allowed to access the requested scopes?
client_scopes = frappe.db.get_value("OAuth Client", client_id, 'scopes').split(get_url_delimiter())
are_scopes_valid = True
for scp in scopes:
are_scopes_valid = are_scopes_valid and True if scp in client_scopes else False
return are_scopes_valid
allowed_scopes = get_client_scopes(client_id)
return all(scope in allowed_scopes for scope in scopes)
def get_default_scopes(self, client_id, request, *args, **kwargs):
# Scopes a client will authorize for if none are supplied in the
# authorization request.
scopes = frappe.db.get_value("OAuth Client", client_id, 'scopes').split(get_url_delimiter())
scopes = get_client_scopes(client_id)
request.scopes = scopes #Apparently this is possible.
return scopes
@ -440,3 +434,8 @@ def delete_oauth2_data():
frappe.delete_doc("OAuth Bearer Token", token["name"])
if commit_code or commit_token:
frappe.db.commit()
def get_client_scopes(client_id):
scopes_string = frappe.db.get_value("OAuth Client", client_id, "scopes")
return scopes_string.split()

View file

@ -305,7 +305,11 @@ frappe.patches.v12_0.fix_email_id_formatting
frappe.patches.v13_0.add_toggle_width_in_navbar_settings
frappe.patches.v13_0.rename_notification_fields
frappe.patches.v13_0.remove_duplicate_navbar_items
frappe.patches.v12_0.set_default_password_reset_limit
execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True)
frappe.patches.v13_0.set_route_for_blog_category
frappe.patches.v13_0.enable_custom_script
frappe.patches.v13_0.update_newsletter_content_type
frappe.patches.v13_0.delete_event_producer_and_consumer_keys
execute:frappe.db.set_value('Website Settings', 'Website Settings', {'navbar_template': 'Standard Navbar', 'footer_template': 'Standard Footer'})
frappe.patches.v13_0.delete_event_producer_and_consumer_keys
frappe.patches.v13_0.web_template_set_module #2020-10-05

View file

@ -0,0 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
import frappe
def execute():
frappe.reload_doc("core", "doctype", "system_settings", force=1)
frappe.db.set_value('System Settings', None, "password_reset_limit", 3)

View file

@ -0,0 +1,16 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
"""Set default module for standard Web Template, if none."""
frappe.reload_doctype('Web Template')
frappe.reload_doctype('Web Template Field')
standard_templates = frappe.get_list('Web Template', {'standard': 1})
for template in standard_templates:
doc = frappe.get_doc('Web Template', template.name)
if not doc.module:
doc.module = 'Website'
doc.save()

View file

@ -243,6 +243,7 @@
"public/js/frappe/utils/energy_point_utils.js",
"public/js/frappe/utils/dashboard_utils.js",
"public/js/frappe/ui/chart.js",
"public/js/frappe/ui/datatable.js",
"public/js/frappe/ui/driver.js",
"public/js/frappe/barcode_scanner/index.js"
],

View file

@ -52,7 +52,7 @@ frappe.Application = Class.extend({
this.set_favicon();
this.setup_analytics();
this.set_fullwidth_if_enabled();
this.add_browser_class();
this.setup_energy_point_listeners();
frappe.ui.keys.setup();
@ -511,6 +511,16 @@ frappe.Application = Class.extend({
}
},
add_browser_class() {
let browsers = ['Chrome', 'Firefox', 'Safari'];
for (let browser of browsers) {
if (navigator.userAgent.includes(browser)) {
$('html').addClass(browser.toLowerCase());
return;
}
}
},
set_fullwidth_if_enabled() {
frappe.ui.toolbar.set_fullwidth_if_enabled();
},

View file

@ -34,7 +34,7 @@ frappe.dom = {
},
remove_script_and_style: function(txt) {
const evil_tags = ["script", "style", "noscript", "title", "meta", "base", "head"];
const regex = new RegExp(evil_tags.map(tag => `<${tag}>.*<\\/${tag}>`).join('|'));
const regex = new RegExp(evil_tags.map(tag => `<${tag}>.*<\\/${tag}>`).join('|'), 's');
if (!regex.test(txt)) {
// no evil tags found, skip the DOM method entirely!
return txt;

View file

@ -1,14 +1,12 @@
frappe.ui.form.ControlDynamicLink = frappe.ui.form.ControlLink.extend({
get_options: function() {
let options = '';
if(this.df.get_options) {
if (this.df.get_options) {
options = this.df.get_options();
}
else if (this.docname==null && cur_dialog) {
} else if (this.docname==null && cur_dialog) {
//for dialog box
options = cur_dialog.get_value(this.df.options);
}
else if (!cur_frm) {
} else if (!cur_frm) {
const selector = `input[data-fieldname="${this.df.options}"]`;
let input = null;
if (cur_list) {
@ -21,13 +19,12 @@ frappe.ui.form.ControlDynamicLink = frappe.ui.form.ControlLink.extend({
if (input) {
options = input.val();
}
}
else {
} else {
options = frappe.model.get_value(this.df.parent, this.docname, this.df.options);
}
if (frappe.model.is_single(options)) {
frappe.throw(__(`${options.bold()} is not a valid DocType for Dynamic Link`));
frappe.throw(__("{0} is not a valid DocType for Dynamic Link", [options.bold()]));
}
return options;

View file

@ -1,17 +1,17 @@
frappe.ui.form.ControlInt = frappe.ui.form.ControlData.extend({
make: function() {
make: function () {
this._super();
// $(this.label_area).addClass('pull-right');
// $(this.disp_area).addClass('text-right');
},
make_input: function() {
make_input: function () {
var me = this;
this._super();
this.$input
// .addClass("text-right")
.on("focus", function() {
setTimeout(function() {
if(!document.activeElement) return;
.on("focus", function () {
setTimeout(function () {
if (!document.activeElement) return;
document.activeElement.value
= me.validate(document.activeElement.value);
document.activeElement.select();
@ -19,7 +19,10 @@ frappe.ui.form.ControlInt = frappe.ui.form.ControlData.extend({
return false;
});
},
eval_expression: function(value) {
validate: function (value) {
return this.parse(value);
},
eval_expression: function (value) {
if (typeof value === 'string') {
if (value.match(/^[0-9+\-/* ]+$/)) {
// If it is a string containing operators
@ -33,7 +36,7 @@ frappe.ui.form.ControlInt = frappe.ui.form.ControlData.extend({
}
return value;
},
parse: function(value) {
parse: function (value) {
return cint(this.eval_expression(value), null);
}
});

View file

@ -44,5 +44,9 @@ frappe.ui.form.ControlMarkdownEditor = frappe.ui.form.ControlCode.extend({
.then(() => {
this.update_preview();
});
},
set_disp_area(value) {
this.disp_area && $(this.disp_area).text(value);
}
});

View file

@ -1265,7 +1265,7 @@ frappe.ui.form.Form = class FrappeForm {
set_df_property(fieldname, property, value, docname, table_field) {
var df;
if (!docname && !table_field){
if (!docname && !table_field) {
df = this.get_docfield(fieldname);
} else {
var grid = this.fields_dict[table_field].grid,
@ -1273,7 +1273,7 @@ frappe.ui.form.Form = class FrappeForm {
if (fname && fname.length)
df = frappe.meta.get_docfield(fname[0].parent, fieldname, docname);
}
if(df && df[property] != value) {
if (df && df[property] != value) {
df[property] = value;
refresh_field(fieldname, table_field);
}

View file

@ -763,6 +763,13 @@ export default class Grid {
// download
this.setup_download();
const value_formatter_map = {
"Date": val => val ? frappe.datetime.user_to_str(val) : val,
"Int": val => cint(val),
"Check": val => cint(val),
"Float": val => flt(val),
};
// upload
frappe.flags.no_socketio = true;
$(this.wrapper).find(".grid-upload").removeClass('hidden').on("click", () => {
@ -790,16 +797,9 @@ export default class Grid {
var fieldname = fieldnames[ci];
var df = frappe.meta.get_docfield(me.df.options, fieldname);
// convert date formatting
if (df.fieldtype==="Date" && value) {
value = frappe.datetime.user_to_str(value);
}
if (df.fieldtype==="Int" || df.fieldtype==="Check") {
value = cint(value);
}
d[fieldnames[ci]] = value;
d[fieldnames[ci]] = value_formatter_map[df.fieldtype]
? value_formatter_map[df.fieldtype](value)
: value;
});
}
}

View file

@ -393,11 +393,16 @@ export default class GridRow {
// sync get_query
field.get_query = this.grid.get_field(df.fieldname).get_query;
var field_on_change_function = field.df.onchange;
field.df.onchange = function(e) {
field_on_change_function && field_on_change_function(e);
me.grid.grid_rows[this.doc.idx - 1].refresh_field(field.df.fieldname);
};
if (!field.df.onchange_modified) {
var field_on_change_function = field.df.onchange;
field.df.onchange = function(e) {
field_on_change_function && field_on_change_function(e);
me.grid.grid_rows[this.doc.idx - 1].refresh_field(this.df.fieldname);
};
field.df.onchange_modified = true;
}
field.refresh();
if(field.$input) {
field.$input

View file

@ -215,10 +215,6 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
$(btn).prop("disabled", false);
frappe.ui.form.is_saving = false;
if (!r.exc) {
frappe.show_alert({message: __('Saved'), indicator: 'green'});
}
if (r) {
var doc = r.docs && r.docs[0];
if (doc) {

View file

@ -78,7 +78,6 @@ frappe.ui.form.Review = class Review {
}
show_review_dialog() {
const user_options = this.get_involved_users();
const doc_owner = this.frm.doc.owner;
const review_dialog = new frappe.ui.Dialog({
'title': __('Add Review'),
'fields': [{
@ -106,7 +105,7 @@ frappe.ui.form.Review = class Review {
fieldtype: 'Int',
label: __('Points'),
reqd: 1,
description: __(`Currently you have ${this.points.review_points} review points`)
description: __("Currently you have {0} review points", [this.points.review_points])
}, {
fieldtype: 'Small Text',
fieldname: 'reason',
@ -181,7 +180,7 @@ frappe.ui.form.Review = class Review {
trigger: 'hover',
delay: 500,
placement: 'top',
template:`
template: `
<div class="review-popover popover">
<div class="arrow"></div>
<div class="popover-content"></div>

View file

@ -186,7 +186,7 @@ frappe.ui.form.Toolbar = Class.extend({
},
set_indicator: function() {
var indicator = frappe.get_indicator(this.frm.doc);
if (this.frm.save_disabled && [__('Saved'), __('Not Saved')].includes(indicator[0])) {
if (this.frm.save_disabled && indicator && [__('Saved'), __('Not Saved')].includes(indicator[0])) {
return;
}
if(indicator) {
@ -272,12 +272,12 @@ frappe.ui.form.Toolbar = Class.extend({
});
}
if (frappe.user_roles.includes("System Manager") && me.frm.meta.issingle === 0) {
if (frappe.user_roles.includes("System Manager")) {
let is_doctype_form = me.frm.doctype === 'DocType';
let doctype = is_doctype_form ? me.frm.docname : me.frm.doctype;
let is_doctype_custom = is_doctype_form ? me.frm.doc.custom : false;
if (doctype != 'DocType' && !is_doctype_custom) {
if (doctype != 'DocType' && !is_doctype_custom && me.frm.meta.issingle === 0) {
this.page.add_menu_item(__("Customize"), function() {
if (me.frm.meta && me.frm.meta.custom) {
frappe.set_route('Form', 'DocType', doctype);

View file

@ -33,7 +33,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
if (!this.has_permissions()) {
frappe.set_route('');
frappe.msgprint(__(`Not permitted to view ${this.doctype}`));
frappe.msgprint(__("Not permitted to view {0}", [this.doctype]));
return;
}

View file

@ -31,7 +31,7 @@ $.extend(frappe.model, {
{fieldname:'docstatus', fieldtype:'Int', label:__('Document Status')},
],
numeric_fieldtypes: ["Int", "Float", "Currency", "Percent"],
numeric_fieldtypes: ["Int", "Float", "Currency", "Percent", "Duration"],
std_fields_table: [
{fieldname:'parent', fieldtype:'Data', label:__('Parent')},

View file

@ -1,6 +1,6 @@
frappe.provide('frappe.route');
frappe.route_history_queue = [];
const routes_to_skip = ['Form', 'social', 'setup-wizard'];
const routes_to_skip = ['Form', 'social', 'setup-wizard', 'recorder'];
const save_routes = frappe.utils.debounce(() => {
const routes = frappe.route_history_queue;
@ -30,7 +30,6 @@ function is_route_useful(route) {
if (!route[1]) {
return false;
} else if ((route[0] === 'List' && !route[2]) || routes_to_skip.includes(route[0])) {
return false;
} else {
return true;

View file

@ -3,155 +3,139 @@
/**
* @description Converts a canvas, image or a video to a data URL string.
*
*
* @param {HTMLElement} element - canvas, img or video.
* @returns {string} - The data URL string.
*
*
* @example
* frappe._.get_data_uri(video)
* // returns "data:image/pngbase64,..."
*/
frappe._.get_data_uri = element =>
{
const $element = $(element)
const width = $element.width()
const height = $element.height()
frappe._.get_data_uri = element => {
const $element = $(element);
const width = $element.width();
const height = $element.height();
const $canvas = $('<canvas/>')
$canvas[0].width = width
$canvas[0].height = height
const $canvas = $('<canvas/>');
$canvas[0].width = width;
$canvas[0].height = height;
const context = $canvas[0].getContext('2d')
context.drawImage($element[0], 0, 0, width, height)
const data_uri = $canvas[0].toDataURL('image/png')
const context = $canvas[0].getContext('2d');
context.drawImage($element[0], 0, 0, width, height);
return data_uri
}
const data_uri = $canvas[0].toDataURL('image/png');
return data_uri;
};
/**
* @description Frappe's Capture object.
*
*
* @example
* const capture = frappe.ui.Capture()
* capture.show()
*
*
* capture.click((data_uri) => {
* // do stuff
* })
*
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Taking_still_photos
*/
frappe.ui.Capture = class
{
constructor (options = { })
{
this.options = frappe.ui.Capture.OPTIONS
this.set_options(options)
frappe.ui.Capture = class {
constructor(options = {}) {
this.options = frappe.ui.Capture.OPTIONS;
this.set_options(options);
}
set_options (options)
{
this.options = { ...frappe.ui.Capture.OPTIONS, ...options }
return this
set_options(options) {
this.options = { ...frappe.ui.Capture.OPTIONS, ...options };
return this;
}
render ( )
{
return navigator.mediaDevices.getUserMedia({ video: true }).then(stream =>
{
this.dialog = new frappe.ui.Dialog({
title: this.options.title,
render() {
return navigator.mediaDevices.getUserMedia({ video: true }).then(stream => {
this.dialog = new frappe.ui.Dialog({
title: this.options.title,
animate: this.options.animate,
action:
{
secondary:
{
label: "<b>&times</b>"
action: {
secondary: {
label: '<b>&times</b>'
}
}
})
const $e = $(frappe.ui.Capture.TEMPLATE)
const video = $e.find('video')[0]
video.srcObject = stream
video.play()
const $container = $(this.dialog.body)
$container.html($e)
$e.find('.fc-btf').hide()
});
$e.find('.fc-bcp').click(() =>
{
const data_url = frappe._.get_data_uri(video)
$e.find('.fc-p').attr('src', data_url)
const $e = $(frappe.ui.Capture.TEMPLATE);
$e.find('.fc-s').hide()
$e.find('.fc-p').show()
const video = $e.find('video')[0];
video.srcObject = stream;
video.play();
$e.find('.fc-btu').hide()
$e.find('.fc-btf').show()
})
const $container = $(this.dialog.body);
$container.html($e);
$e.find('.fc-br').click(() =>
{
$e.find('.fc-p').hide()
$e.find('.fc-s').show()
$e.find('.fc-btf').hide();
$e.find('.fc-btf').hide()
$e.find('.fc-btu').show()
})
$e.find('.fc-bcp').click(() => {
const data_url = frappe._.get_data_uri(video);
$e.find('.fc-p').attr('src', data_url);
$e.find('.fc-bs').click(() =>
{
const data_url = frappe._.get_data_uri(video)
this.hide()
if (this.callback)
this.callback(data_url)
})
})
$e.find('.fc-s').hide();
$e.find('.fc-p').show();
$e.find('.fc-btu').hide();
$e.find('.fc-btf').show();
});
$e.find('.fc-br').click(() => {
$e.find('.fc-p').hide();
$e.find('.fc-s').show();
$e.find('.fc-btf').hide();
$e.find('.fc-btu').show();
});
$e.find('.fc-bs').click(() => {
const data_url = frappe._.get_data_uri(video);
this.hide();
if (this.callback) this.callback(data_url);
});
});
}
show ( )
{
this.render().then(() =>
{
this.dialog.show()
}).catch(err => {
if ( this.options.error )
{
const alert = `<span class="indicator red"/> ${frappe.ui.Capture.ERR_MESSAGE}`
frappe.show_alert(alert, 3)
}
show() {
this.render()
.then(() => {
this.dialog.show();
})
.catch(err => {
if (this.options.error) {
const alert = `<span class="indicator red"/> ${
frappe.ui.Capture.ERR_MESSAGE
}`;
frappe.show_alert(alert, 3);
}
throw err
})
throw err;
});
}
hide ( )
{
if ( this.dialog )
this.dialog.hide()
hide() {
if (this.dialog) this.dialog.hide();
}
submit (fn)
{
this.callback = fn
submit(fn) {
this.callback = fn;
}
}
frappe.ui.Capture.OPTIONS =
{
title: __(`Camera`),
};
frappe.ui.Capture.OPTIONS = {
title: __("Camera"),
animate: false,
error: false,
}
frappe.ui.Capture.ERR_MESSAGE = __("Unable to load camera.")
frappe.ui.Capture.TEMPLATE =
`
error: false
};
frappe.ui.Capture.ERR_MESSAGE = __('Unable to load camera.');
frappe.ui.Capture.TEMPLATE = `
<div class="frappe-capture">
<div class="panel panel-default">
<img class="fc-p img-responsive"/>
@ -181,14 +165,7 @@ frappe.ui.Capture.TEMPLATE =
<div class="fc-btu">
<div class="row">
<div class="col-md-6">
${
''
// <div class="pull-left">
// <button class="btn btn-default">
// <small>${__('Take Video')}</small>
// </button>
// </div>
}
${''}
</div>
<div class="col-md-6">
<div class="pull-right">
@ -201,4 +178,4 @@ frappe.ui.Capture.TEMPLATE =
</div>
</div>
</div>
`
`;

Some files were not shown because too many files have changed in this diff Show more