Merge branch 'develop' of github.com:frappe/frappe into move-cypress
This commit is contained in:
commit
40ee03c21d
310 changed files with 49958 additions and 9396 deletions
48
.github/helper/documentation.py
vendored
Normal file
48
.github/helper/documentation.py
vendored
Normal 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... 🏃")
|
||||
|
|
@ -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
24
.github/workflows/docs-checker.yml
vendored
Normal 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
|
||||
43
.github/workflows/publish-assets-develop.yml
vendored
Normal file
43
.github/workflows/publish-assets-develop.yml
vendored
Normal 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'
|
||||
47
.github/workflows/publish-assets-releases.yml
vendored
Normal file
47
.github/workflows/publish-assets-releases.yml
vendored
Normal 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
|
||||
|
||||
2
.github/workflows/translation_linter.yml
vendored
2
.github/workflows/translation_linter.yml
vendored
|
|
@ -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
34
.snyk
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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'")
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
232
frappe/build.py
232
frappe/build.py
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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=[
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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.
|
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
0
frappe/core/doctype/document_naming_rule/__init__.py
Normal file
0
frappe/core/doctype/document_naming_rule/__init__.py
Normal 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');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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) {
|
||||
|
||||
// }
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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": "",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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', '')
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 = """
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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') :
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
9
frappe/patches/v12_0/set_default_password_reset_limit.py
Normal file
9
frappe/patches/v12_0/set_default_password_reset_limit.py
Normal 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)
|
||||
16
frappe/patches/v13_0/web_template_set_module.py
Normal file
16
frappe/patches/v13_0/web_template_set_module.py
Normal 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()
|
||||
|
|
@ -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"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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')},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>×</b>"
|
||||
action: {
|
||||
secondary: {
|
||||
label: '<b>×</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
Loading…
Add table
Reference in a new issue