Merge branch 'develop' into s3-backup-update
This commit is contained in:
commit
97f6d8f209
264 changed files with 6597 additions and 1895 deletions
|
|
@ -78,6 +78,7 @@
|
|||
"has_common": true,
|
||||
"has_words": true,
|
||||
"validate_email": true,
|
||||
"validate_name": true,
|
||||
"validate_phone": true,
|
||||
"get_number_format": true,
|
||||
"format_number": true,
|
||||
|
|
|
|||
28
.github/frappe_linter/translation.py
vendored
Normal file
28
.github/frappe_linter/translation.py
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import re
|
||||
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}")
|
||||
|
||||
# skip first argument
|
||||
files = sys.argv[1:]
|
||||
for _file in files:
|
||||
if not _file.endswith(('.py', '.js')):
|
||||
continue
|
||||
with open(_file, 'r') as f:
|
||||
print(f'Checking: {_file}')
|
||||
for num, line in enumerate(f, 1):
|
||||
all_matches = start_pattern.finditer(line)
|
||||
if all_matches:
|
||||
for match in all_matches:
|
||||
verify = pattern.search(line)
|
||||
if not verify:
|
||||
errors_encounter += 1
|
||||
print(f'A syntax error has been discovered at line number: {num}')
|
||||
print(f'Syntax error occurred with: {line}')
|
||||
if errors_encounter > 0:
|
||||
print('You can visit "https://frappe.io/docs/user/en/translations" to resolve this error.')
|
||||
assert 1+1 == 3
|
||||
else:
|
||||
print('Good To Go!')
|
||||
22
.github/workflows/translation_linter.yml
vendored
Normal file
22
.github/workflows/translation_linter.yml
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
name: Frappe Linter
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- version-12-hotfix
|
||||
- version-11-hotfix
|
||||
jobs:
|
||||
check_translation:
|
||||
name: Translation Syntax Check
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup python3
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.6
|
||||
- name: Validating Translation Syntax
|
||||
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
|
||||
19
.travis.yml
19
.travis.yml
|
|
@ -7,14 +7,19 @@ addons:
|
|||
- test_site_producer
|
||||
mariadb: 10.3
|
||||
postgresql: 9.5
|
||||
chrome: stable
|
||||
|
||||
git:
|
||||
depth: 1
|
||||
|
||||
cache:
|
||||
- pip
|
||||
- npm
|
||||
- yarn
|
||||
pip: true
|
||||
npm: true
|
||||
yarn: true
|
||||
directories:
|
||||
# we also need to cache folder with Cypress binary
|
||||
# https://docs.cypress.io/guides/guides/continuous-integration.html#Caching
|
||||
- ~/.cache
|
||||
|
||||
matrix:
|
||||
include:
|
||||
|
|
@ -54,10 +59,9 @@ before_install:
|
|||
install:
|
||||
- cd ~
|
||||
- source ./.nvm/nvm.sh
|
||||
- nvm install v8.10.0
|
||||
- nvm install 12
|
||||
|
||||
- git clone https://github.com/frappe/bench --depth 1
|
||||
- pip install -e ./bench
|
||||
- pip install frappe-bench
|
||||
|
||||
- bench init frappe-bench --skip-assets --python $(which python) --frappe-path $TRAVIS_BUILD_DIR
|
||||
|
||||
|
|
@ -99,8 +103,7 @@ install:
|
|||
- if [ $TYPE == "server" ]; then sed -i 's/socketio:/# socketio:/g' Procfile; fi
|
||||
- if [ $TYPE == "server" ]; then sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile; fi
|
||||
|
||||
- if [ $TYPE == "ui" ]; then bench setup requirements --node; fi
|
||||
|
||||
- bench setup requirements --node
|
||||
- bench start &
|
||||
- bench --site test_site reinstall --yes
|
||||
- bench --site test_site_producer reinstall --yes
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
{
|
||||
"baseUrl": "http://test_site_ui:8000",
|
||||
"projectId": "92odwv",
|
||||
"adminPassword": "admin"
|
||||
"adminPassword": "admin",
|
||||
"defaultCommandTimeout": 10000,
|
||||
"pageLoadTimeout": 15000
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
context('Depends On', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
return cy.new_form('Test Depends On');
|
||||
});
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ context('FileUploader', () => {
|
|||
open_upload_dialog();
|
||||
|
||||
cy.get_open_dialog().find('a:contains("web link")').click();
|
||||
cy.get_open_dialog().find('.file-web-link input').type('https://github.com');
|
||||
cy.get_open_dialog().find('.file-web-link input').type('https://github.com', { delay: 100, force: true });
|
||||
cy.server();
|
||||
cy.route('POST', '/api/method/upload_file').as('upload_file');
|
||||
cy.get_open_dialog().find('.btn-primary').click();
|
||||
|
|
|
|||
|
|
@ -6,14 +6,17 @@ context('Form', () => {
|
|||
return frappe.call("frappe.tests.ui_test_helpers.create_contact_records");
|
||||
});
|
||||
});
|
||||
beforeEach(() => {
|
||||
cy.visit('/desk#workspace/Website');
|
||||
});
|
||||
it('create a new form', () => {
|
||||
cy.visit('/desk#Form/ToDo/New ToDo 1');
|
||||
cy.fill_field('description', 'this is a test todo', 'Text Editor').blur();
|
||||
cy.get('.page-title').should('contain', 'Not Saved');
|
||||
cy.server();
|
||||
cy.route({
|
||||
method: 'POST',
|
||||
url: 'api/method/frappe.desk.form.save.savedocs'
|
||||
}).as('form_save');
|
||||
cy.get('.primary-action').click();
|
||||
cy.wait('@form_save').its('status').should('eq', 200);
|
||||
cy.visit('/desk#List/ToDo');
|
||||
cy.location('hash').should('eq', '#List/ToDo/List');
|
||||
cy.get('h1').should('be.visible').and('contain', 'To Do');
|
||||
|
|
@ -41,4 +44,21 @@ context('Form', () => {
|
|||
list_view.filter_area.filter_list.clear_filters();
|
||||
});
|
||||
});
|
||||
it('validates behaviour of Data options validations in child table', () => {
|
||||
// test email validations for set_invalid controller
|
||||
let website_input = 'website.in';
|
||||
let expectBackgroundColor = 'rgb(255, 220, 220)';
|
||||
|
||||
cy.visit('/desk#Form/Contact/New Contact 1');
|
||||
cy.get('.frappe-control[data-fieldname="email_ids"]').as('table');
|
||||
cy.get('@table').find('button.grid-add-row').click();
|
||||
cy.get('.grid-body .rows [data-fieldname="email_id"]').click();
|
||||
cy.get('@table').find('input.input-with-feedback.form-control').as('email_input');
|
||||
cy.get('@email_input').type(website_input, { waitForAnimations: false });
|
||||
cy.fill_field('company_name', 'Test Company');
|
||||
cy.get('@email_input').should($div => {
|
||||
const style = window.getComputedStyle($div[0]);
|
||||
expect(style.backgroundColor).to.equal(expectBackgroundColor);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
|
|||
if (fieldtype === 'Select') {
|
||||
cy.get('@input').select(value);
|
||||
} else {
|
||||
cy.get('@input').type(value, { waitForAnimations: false });
|
||||
cy.get('@input').type(value, { waitForAnimations: false, force: true });
|
||||
}
|
||||
return cy.get('@input');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -219,7 +219,10 @@ class LoginManager:
|
|||
user = frappe.db.get_value("User", filters={"username": user}, fieldname="name") or user
|
||||
|
||||
self.check_if_enabled(user)
|
||||
self.user = self.check_password(user, pwd)
|
||||
if not frappe.form_dict.get('tmp_id'):
|
||||
self.user = self.check_password(user, pwd)
|
||||
else:
|
||||
self.user = user
|
||||
|
||||
def force_user_to_reset_password(self):
|
||||
if not self.user:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
{
|
||||
"hidden": 0,
|
||||
"label": "Tools",
|
||||
"links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]"
|
||||
"links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Video\",\n \"name\": \"Video\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"label": "Tools",
|
||||
"modified": "2020-04-01 11:24:40.804346",
|
||||
"modified": "2020-04-20 18:21:14.152537",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Automation",
|
||||
"name": "Tools",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ from frappe.utils.change_log import get_versions
|
|||
from frappe.translate import get_lang_dict
|
||||
from frappe.email.inbox import get_email_accounts
|
||||
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled
|
||||
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled
|
||||
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
|
||||
from frappe.social.doctype.post.post import frequently_visited_links
|
||||
|
||||
|
|
@ -79,6 +80,7 @@ def get_bootinfo():
|
|||
bootinfo.success_action = get_success_action()
|
||||
bootinfo.update(get_email_accounts(user=frappe.session.user))
|
||||
bootinfo.energy_points_enabled = is_energy_point_enabled()
|
||||
bootinfo.website_tracking_enabled = is_tracking_enabled()
|
||||
bootinfo.points = get_energy_points(frappe.session.user)
|
||||
bootinfo.frequently_visited_links = frequently_visited_links()
|
||||
bootinfo.link_preview_doctypes = get_link_preview_doctypes()
|
||||
|
|
@ -268,4 +270,18 @@ def get_success_action():
|
|||
return frappe.get_all("Success Action", fields=["*"])
|
||||
|
||||
def get_link_preview_doctypes():
|
||||
return [d.name for d in frappe.db.get_all('DocType', {'show_preview_popup': 1})]
|
||||
from frappe.utils import cint
|
||||
|
||||
link_preview_doctypes = [d.name for d in frappe.db.get_all('DocType', {'show_preview_popup': 1})]
|
||||
customizations = frappe.get_all("Property Setter",
|
||||
fields=['doc_type', 'value'],
|
||||
filters={'property': 'show_preview_popup'}
|
||||
)
|
||||
|
||||
for custom in customizations:
|
||||
if not cint(custom.value) and custom.doc_type in link_preview_doctypes:
|
||||
link_preview_doctypes.remove(custom.doc_type)
|
||||
else:
|
||||
link_preview_doctypes.append(custom.doc_type)
|
||||
|
||||
return link_preview_doctypes
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ global_cache_keys = ("app_hooks", "installed_apps",
|
|||
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
|
||||
'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version',
|
||||
'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts',
|
||||
'sitemap_routes')
|
||||
'sitemap_routes', 'db_tables')
|
||||
|
||||
user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
|
||||
"defaults", "user_permissions", "home_page", "linked_with",
|
||||
|
|
|
|||
|
|
@ -1,13 +1,23 @@
|
|||
from __future__ import unicode_literals, absolute_import, print_function
|
||||
# imports - standard imports
|
||||
import atexit
|
||||
import compileall
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
# imports - third party imports
|
||||
import click
|
||||
import hashlib, os, sys, compileall, re
|
||||
|
||||
# imports - module imports
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.commands import pass_context, get_site
|
||||
from frappe.commands import get_site, pass_context
|
||||
from frappe.commands.scheduler import _is_scheduler_enabled
|
||||
from frappe.installer import update_site_config
|
||||
from frappe.utils import touch_file, get_site_path
|
||||
from six import text_type
|
||||
from frappe.utils import get_site_path, touch_file
|
||||
|
||||
|
||||
@click.command('new-site')
|
||||
@click.argument('site')
|
||||
|
|
@ -68,32 +78,33 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
|
|||
|
||||
make_site_dirs()
|
||||
|
||||
installing = None
|
||||
try:
|
||||
installing = touch_file(get_site_path('locks', 'installing.lock'))
|
||||
installing = touch_file(get_site_path('locks', 'installing.lock'))
|
||||
atexit.register(_new_site_cleanup, site, mariadb_root_username, mariadb_root_password)
|
||||
|
||||
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password,
|
||||
db_name=db_name, admin_password=admin_password, verbose=verbose,
|
||||
source_sql=source_sql, force=force, reinstall=reinstall, db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port, no_mariadb_socket=no_mariadb_socket)
|
||||
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, db_name=db_name,
|
||||
admin_password=admin_password, verbose=verbose, source_sql=source_sql, force=force, reinstall=reinstall,
|
||||
db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port, no_mariadb_socket=no_mariadb_socket)
|
||||
apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
|
||||
for app in apps_to_install:
|
||||
_install_app(app, verbose=verbose, set_as_patched=not source_sql)
|
||||
|
||||
apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
|
||||
for app in apps_to_install:
|
||||
_install_app(app, verbose=verbose, set_as_patched=not source_sql)
|
||||
os.remove(installing)
|
||||
|
||||
frappe.utils.scheduler.toggle_scheduler(enable_scheduler)
|
||||
frappe.db.commit()
|
||||
frappe.utils.scheduler.toggle_scheduler(enable_scheduler)
|
||||
frappe.db.commit()
|
||||
|
||||
scheduler_status = "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled"
|
||||
print("*** Scheduler is", scheduler_status, "***")
|
||||
scheduler_status = "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled"
|
||||
print("*** Scheduler is", scheduler_status, "***")
|
||||
|
||||
except frappe.exceptions.ImproperDBConfigurationError:
|
||||
_drop_site(site, mariadb_root_username, mariadb_root_password, force=True)
|
||||
def _new_site_cleanup(site, mariadb_root_username, mariadb_root_password):
|
||||
installing = get_site_path('locks', 'installing.lock')
|
||||
|
||||
finally:
|
||||
if installing and os.path.exists(installing):
|
||||
os.remove(installing)
|
||||
if installing and os.path.exists(installing):
|
||||
if mariadb_root_password:
|
||||
_drop_site(site, mariadb_root_username, mariadb_root_password, force=True)
|
||||
shutil.rmtree(site)
|
||||
|
||||
frappe.destroy()
|
||||
frappe.destroy()
|
||||
|
||||
@click.command('restore')
|
||||
@click.argument('sql-file-path')
|
||||
|
|
@ -317,10 +328,18 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non
|
|||
"Backup"
|
||||
from frappe.utils.backups import scheduled_backup
|
||||
verbose = context.verbose
|
||||
exit_code = 0
|
||||
for site in context.sites:
|
||||
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)
|
||||
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)
|
||||
except Exception as e:
|
||||
if verbose:
|
||||
print("Backup failed for {0}. Database or site_config.json may be corrupted".format(site))
|
||||
exit_code = 1
|
||||
continue
|
||||
|
||||
if verbose:
|
||||
from frappe.utils import now
|
||||
print("database backup taken -", odb.backup_path_db, "- on", now())
|
||||
|
|
@ -329,6 +348,7 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non
|
|||
print("private files backup taken -", odb.backup_path_private_files, "- on", now())
|
||||
|
||||
frappe.destroy()
|
||||
sys.exit(exit_code)
|
||||
|
||||
@click.command('remove-from-installed-apps')
|
||||
@click.argument('app')
|
||||
|
|
|
|||
|
|
@ -522,7 +522,7 @@ def run_ui_tests(context, app, headless=False):
|
|||
password_env = 'CYPRESS_adminPassword={}'.format(admin_password) if admin_password else ''
|
||||
|
||||
# run for headless mode
|
||||
run_or_open = 'run --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open'
|
||||
run_or_open = 'run --browser chrome --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open'
|
||||
command = '{site_env} {password_env} yarn run cypress {run_or_open}'
|
||||
formatted_command = command.format(site_env=site_env, password_env=password_env, run_or_open=run_or_open)
|
||||
frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"label": "Users",
|
||||
"modified": "2020-04-01 11:24:40.767676",
|
||||
"modified": "2020-04-26 22:36:14.311554",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Users",
|
||||
|
|
@ -46,12 +46,12 @@
|
|||
"type": "DocType"
|
||||
},
|
||||
{
|
||||
"label": "permission-manager",
|
||||
"label": "Permission Manager",
|
||||
"link_to": "permission-manager",
|
||||
"type": "Page"
|
||||
},
|
||||
{
|
||||
"label": "user-profile",
|
||||
"label": "User Profile",
|
||||
"link_to": "user-profile",
|
||||
"type": "Page"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,11 @@ class Exporter:
|
|||
return parent_fields
|
||||
|
||||
def get_exportable_children_fields(self):
|
||||
children = [df.options for df in self.meta.fields if df.fieldtype in table_fields]
|
||||
child_table_fields = [df for df in self.meta.fields if df.fieldtype in table_fields]
|
||||
if self.export_fields == "Mandatory":
|
||||
child_table_fields = [df for df in child_table_fields if df.reqd]
|
||||
|
||||
children = [df.options for df in child_table_fields]
|
||||
children_fields = []
|
||||
for child in children:
|
||||
children_fields += self.get_exportable_fields(child)
|
||||
|
|
|
|||
|
|
@ -351,9 +351,9 @@ class Importer:
|
|||
value = cstr(value)
|
||||
|
||||
# convert boolean values to 0 or 1
|
||||
if df.fieldtype == "Check" and value.lower().strip() in ["t", "f", "true", "false"]:
|
||||
if df.fieldtype == "Check" and value.lower().strip() in ["t", "f", "true", "false", "yes", "no", "y", "n"]:
|
||||
value = value.lower().strip()
|
||||
value = 1 if value in ["t", "true"] else 0
|
||||
value = 1 if value in ["t", "true", "y", "yes"] else 0
|
||||
|
||||
if df.fieldtype in ["Int", "Check"]:
|
||||
value = cint(value)
|
||||
|
|
@ -610,7 +610,7 @@ class Importer:
|
|||
"message": msg,
|
||||
}
|
||||
)
|
||||
return False
|
||||
return
|
||||
|
||||
elif df.fieldtype == "Link":
|
||||
d = self.get_missing_link_field_values(df.options)
|
||||
|
|
@ -643,8 +643,10 @@ class Importer:
|
|||
if value in INVALID_VALUES:
|
||||
value = None
|
||||
|
||||
value = validate_value(value, df)
|
||||
if value:
|
||||
if value is not None:
|
||||
value = validate_value(value, df)
|
||||
|
||||
if value is not None:
|
||||
doc[df.fieldname] = self.parse_value(value, df)
|
||||
|
||||
is_table = frappe.get_meta(doctype).istable
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class TestExporter(unittest.TestCase):
|
|||
e = Exporter('Web Page', export_fields='All')
|
||||
csv_array = e.get_csv_array()
|
||||
header = csv_array[0]
|
||||
self.assertEqual(len(header), 24)
|
||||
self.assertEqual(len(header), 36)
|
||||
|
||||
|
||||
def test_exports_selected_fields(self):
|
||||
|
|
|
|||
|
|
@ -337,7 +337,12 @@ frappe.ui.form.on('Data Import Beta', {
|
|||
let message = warnings_by_row[row_number]
|
||||
.map(w => {
|
||||
if (w.field) {
|
||||
return `<li>${w.field.label}: ${w.message}</li>`;
|
||||
let label =
|
||||
w.field.label +
|
||||
(w.field.parent !== frm.doc.reference_doctype
|
||||
? ` (${w.field.parent})`
|
||||
: '');
|
||||
return `<li>${label}: ${w.message}</li>`;
|
||||
}
|
||||
return `<li>${w.message}</li>`;
|
||||
})
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@
|
|||
"label",
|
||||
"fieldtype",
|
||||
"fieldname",
|
||||
"reqd",
|
||||
"precision",
|
||||
"length",
|
||||
"reqd",
|
||||
"search_index",
|
||||
"in_list_view",
|
||||
"in_standard_filter",
|
||||
|
|
@ -453,7 +453,7 @@
|
|||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-15 02:26:03.310781",
|
||||
"modified": "2020-04-19 21:54:13.783908",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocField",
|
||||
|
|
|
|||
|
|
@ -477,7 +477,8 @@ class DocType(Document):
|
|||
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict['fields']))
|
||||
if field_dict:
|
||||
new_field_dicts.append(field_dict[0])
|
||||
remaining_field_names.remove(fieldname)
|
||||
if fieldname in remaining_field_names:
|
||||
remaining_field_names.remove(fieldname)
|
||||
|
||||
for fieldname in remaining_field_names:
|
||||
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict['fields']))
|
||||
|
|
@ -498,7 +499,8 @@ class DocType(Document):
|
|||
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict.get('fields', [])))
|
||||
if field_dict:
|
||||
new_field_dicts.append(field_dict[0])
|
||||
remaining_field_names.remove(fieldname)
|
||||
if fieldname in remaining_field_names:
|
||||
remaining_field_names.remove(fieldname)
|
||||
|
||||
for fieldname in remaining_field_names:
|
||||
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict.get('fields', [])))
|
||||
|
|
@ -710,9 +712,10 @@ def validate_fields(meta):
|
|||
if d.fieldtype == "Currency" and cint(d.width) < 100:
|
||||
frappe.throw(_("Max width for type Currency is 100px in row {0}").format(d.idx))
|
||||
|
||||
def check_in_list_view(d):
|
||||
def check_in_list_view(is_table, d):
|
||||
if d.in_list_view and (d.fieldtype in not_allowed_in_list_view):
|
||||
frappe.throw(_("'In List View' not allowed for type {0} in row {1}").format(d.fieldtype, d.idx))
|
||||
property_label = 'In Grid View' if is_table else 'In List View'
|
||||
frappe.throw(_("'{0}' not allowed for type {1} in row {2}").format(property_label, d.fieldtype, d.idx))
|
||||
|
||||
def check_in_global_search(d):
|
||||
if d.in_global_search and d.fieldtype in no_value_fields:
|
||||
|
|
@ -731,8 +734,11 @@ def validate_fields(meta):
|
|||
d.default = '0'
|
||||
if d.fieldtype == "Check" and d.default not in ('0', '1'):
|
||||
frappe.throw(_("Default for 'Check' type of field must be either '0' or '1'"))
|
||||
if d.fieldtype == "Select" and d.default and (d.default not in d.options.split("\n")):
|
||||
frappe.throw(_("Default for {0} must be an option").format(d.fieldname))
|
||||
if d.fieldtype == "Select" and d.default:
|
||||
if not d.options:
|
||||
frappe.throw(_("Options for {0} must be set before setting the default value.").format(frappe.bold(d.fieldname)))
|
||||
elif d.default not in d.options.split("\n"):
|
||||
frappe.throw(_("Default value for {0} must be in the list of options.").format(frappe.bold(d.fieldname)))
|
||||
|
||||
def check_precision(d):
|
||||
if d.fieldtype in ("Currency", "Float", "Percent") and d.precision is not None and not (1 <= cint(d.precision) <= 6):
|
||||
|
|
@ -893,7 +899,7 @@ def validate_fields(meta):
|
|||
field.fetch_from = field.fetch_from.strip('\n').strip()
|
||||
|
||||
def validate_data_field_type(docfield):
|
||||
if docfield.fieldtype == "Data":
|
||||
if docfield.fieldtype == "Data" and not (docfield.oldfieldtype and docfield.oldfieldtype != "Data"):
|
||||
if docfield.options and (docfield.options not in data_field_options):
|
||||
df_str = frappe.bold(_(docfield.label))
|
||||
text_str = _("{0} is an invalid Data field.").format(df_str) + "<br>" * 2 + _("Only Options allowed for Data field are:") + "<br>"
|
||||
|
|
@ -901,6 +907,16 @@ def validate_fields(meta):
|
|||
|
||||
frappe.msgprint(text_str + df_options_str, title="Invalid Data Field", raise_exception=True)
|
||||
|
||||
def check_child_table_option(docfield):
|
||||
if docfield.fieldtype not in ['Table MultiSelect', 'Table']: return
|
||||
|
||||
doctype = docfield.options
|
||||
meta = frappe.get_meta(doctype)
|
||||
|
||||
if not meta.istable:
|
||||
frappe.throw(_('Option {0} for field {1} is not a child table')
|
||||
.format(frappe.bold(doctype), frappe.bold(docfield.fieldname)), title=_("Invalid Option"))
|
||||
|
||||
|
||||
fields = meta.get("fields")
|
||||
fieldname_list = [d.fieldname for d in fields]
|
||||
|
|
@ -924,11 +940,12 @@ def validate_fields(meta):
|
|||
check_link_table_options(meta.get("name"), d)
|
||||
check_dynamic_link_options(d)
|
||||
check_hidden_and_mandatory(meta.get("name"), d)
|
||||
check_in_list_view(d)
|
||||
check_in_list_view(meta.get('istable'), d)
|
||||
check_in_global_search(d)
|
||||
check_illegal_default(d)
|
||||
check_unique_and_text(meta.get("name"), d)
|
||||
check_illegal_depends_on_conditions(d)
|
||||
check_child_table_option(d)
|
||||
check_table_multiselect_option(d)
|
||||
scrub_options_in_select(d)
|
||||
scrub_fetch_from(d)
|
||||
|
|
|
|||
|
|
@ -1,46 +1,45 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
"""
|
||||
record of files
|
||||
|
||||
naming for same name files: file.gif, file-1.gif, file-2.gif etc
|
||||
"""
|
||||
|
||||
import frappe
|
||||
import json
|
||||
import os
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import base64
|
||||
import re
|
||||
import hashlib
|
||||
import mimetypes
|
||||
import imghdr
|
||||
import io
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import zipfile
|
||||
|
||||
import requests
|
||||
import requests.exceptions
|
||||
import imghdr
|
||||
from PIL import Image, ImageFile, ImageOps
|
||||
from six import PY2, StringIO, string_types, text_type
|
||||
from six.moves.urllib.parse import quote, unquote
|
||||
|
||||
from frappe.utils import get_hook_method, get_files_path, random_string, encode, cstr, call_hook_method, cint
|
||||
from frappe import _
|
||||
from frappe import conf
|
||||
from frappe.utils.nestedset import NestedSet
|
||||
import frappe
|
||||
from frappe import _, conf
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import strip
|
||||
from PIL import Image, ImageOps
|
||||
from six import StringIO, string_types
|
||||
from six.moves.urllib.parse import unquote, quote
|
||||
from six import text_type, PY2
|
||||
import zipfile
|
||||
from frappe.utils import call_hook_method, cint, cstr, encode, get_files_path, get_hook_method, random_string, strip
|
||||
|
||||
|
||||
class MaxFileSizeReachedError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class FolderNotEmpty(frappe.ValidationError): pass
|
||||
class FolderNotEmpty(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
exclude_from_linked_with = True
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
|
||||
|
||||
class File(Document):
|
||||
|
|
@ -608,8 +607,7 @@ def get_local_image(file_url):
|
|||
try:
|
||||
image = Image.open(file_path)
|
||||
except IOError:
|
||||
frappe.msgprint(_("Unable to read file format for {0}").format(file_url))
|
||||
raise
|
||||
frappe.msgprint(_("Unable to read file format for {0}").format(file_url), raise_exception=True)
|
||||
|
||||
content = None
|
||||
|
||||
|
|
@ -698,7 +696,7 @@ def remove_file(fid=None, attached_to_doctype=None, attached_to_name=None, from_
|
|||
|
||||
|
||||
def get_max_file_size():
|
||||
return conf.get('max_file_size') or 10485760
|
||||
return cint(conf.get('max_file_size')) or 10485760
|
||||
|
||||
|
||||
def remove_all(dt, dn, from_delete=False):
|
||||
|
|
@ -715,7 +713,10 @@ def has_permission(doc, ptype=None, user=None):
|
|||
has_access = False
|
||||
user = user or frappe.session.user
|
||||
|
||||
if not doc.is_private or doc.owner == user or user == 'Administrator':
|
||||
if ptype == 'create':
|
||||
has_access = frappe.has_permission('File', 'create', user=user)
|
||||
|
||||
if not doc.is_private or doc.owner in [user, 'Guest'] or user == 'Administrator':
|
||||
has_access = True
|
||||
|
||||
if doc.attached_to_doctype and doc.attached_to_name:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:language_code",
|
||||
"creation": "2014-08-22 16:12:17.249590",
|
||||
|
|
@ -41,7 +42,9 @@
|
|||
}
|
||||
],
|
||||
"icon": "fa fa-globe",
|
||||
"modified": "2019-07-19 16:32:12.652550",
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-16 22:11:33.066852",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Language",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"field_order": [
|
||||
"stopped",
|
||||
"method",
|
||||
"server_script",
|
||||
"frequency",
|
||||
"cron_format",
|
||||
"last_execution",
|
||||
|
|
@ -63,6 +64,14 @@
|
|||
"options": "All\nHourly\nHourly Long\nDaily\nDaily Long\nWeekly\nWeekly Long\nMonthly\nMonthly Long\nCron\nYearly\nAnnual",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "server_script",
|
||||
"fieldtype": "Link",
|
||||
"label": "Server Script",
|
||||
"options": "Server Script",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
|
|
@ -72,7 +81,7 @@
|
|||
"link_fieldname": "scheduled_job_type"
|
||||
}
|
||||
],
|
||||
"modified": "2019-12-09 11:10:21.259929",
|
||||
"modified": "2020-04-05 17:27:33.480562",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Scheduled Job Type",
|
||||
|
|
|
|||
|
|
@ -70,7 +70,12 @@ class ScheduledJobType(Document):
|
|||
self.scheduler_log = None
|
||||
try:
|
||||
self.log_status('Start')
|
||||
frappe.get_attr(self.method)()
|
||||
if self.server_script:
|
||||
script_name = frappe.db.get_value("Server Script", self.server_script)
|
||||
if script_name:
|
||||
frappe.get_doc('Server Script', script_name).execute_scheduled_method()
|
||||
else:
|
||||
frappe.get_attr(self.method)()
|
||||
frappe.db.commit()
|
||||
self.log_status('Complete')
|
||||
except Exception:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,45 @@
|
|||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Server Script', {
|
||||
// refresh: function(frm) {
|
||||
refresh: function(frm) {
|
||||
if(frm.doc.script_type === 'Scheduler Event' && !frm.doc.disabled){
|
||||
frm.add_custom_button('Schedule Script', function() {
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: "Schedule Script Execution",
|
||||
fields: [
|
||||
{
|
||||
fieldname: "event_type",
|
||||
label: __('Select Event Type'),
|
||||
fieldtype: "Select",
|
||||
options: "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long"
|
||||
},
|
||||
],
|
||||
primary_action_label: __('Schedule Script'),
|
||||
primary_action: () => {
|
||||
d.get_primary_btn().attr('disabled', true);
|
||||
var data = d.get_values();
|
||||
d.hide();
|
||||
if(data) {
|
||||
frm.events.schedule_script(frm, data);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
d.show();
|
||||
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
schedule_script(frm, data){
|
||||
frm.call({
|
||||
method: "frappe.core.doctype.server_script.server_script.setup_scheduler_events",
|
||||
args: {
|
||||
'script_name': frm.doc.name,
|
||||
'frequency': data.event_type
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// }
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Script Type",
|
||||
"options": "DocType Event\nAPI",
|
||||
"options": "DocType Event\nScheduler Event\nAPI",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -75,7 +75,7 @@
|
|||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2019-12-17 12:55:07.389775",
|
||||
"modified": "2020-04-06 11:24:38.161555",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Server Script",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ from __future__ import unicode_literals
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.safe_exec import safe_exec
|
||||
from frappe import _
|
||||
|
||||
|
||||
class ServerScript(Document):
|
||||
@staticmethod
|
||||
|
|
@ -31,3 +33,39 @@ class ServerScript(Document):
|
|||
# execute event
|
||||
safe_exec(self.script, None, dict(doc = doc))
|
||||
|
||||
def execute_scheduled_method(self):
|
||||
if self.script_type == 'Scheduler Event':
|
||||
safe_exec(self.script)
|
||||
else:
|
||||
# wrong report type!
|
||||
raise frappe.DoesNotExistError
|
||||
|
||||
@frappe.whitelist()
|
||||
def setup_scheduler_events(script_name, frequency):
|
||||
method = frappe.scrub(script_name) + '_' + frequency.lower()
|
||||
scheduled_script = frappe.db.get_value('Scheduled Job Type',
|
||||
dict(method=method))
|
||||
|
||||
if not scheduled_script:
|
||||
doc = frappe.get_doc(dict(
|
||||
doctype = 'Scheduled Job Type',
|
||||
method = method,
|
||||
frequency = frequency,
|
||||
server_script = script_name
|
||||
))
|
||||
|
||||
doc.insert()
|
||||
|
||||
frappe.msgprint(_('Enabled scheduled execution for script {0}').format(script_name))
|
||||
|
||||
else:
|
||||
doc = frappe.get_doc('Scheduled Job Type', scheduled_script)
|
||||
doc.update(dict(
|
||||
doctype = 'Scheduled Job Type',
|
||||
method = method,
|
||||
frequency = frequency,
|
||||
server_script = script_name
|
||||
))
|
||||
doc.save()
|
||||
|
||||
frappe.msgprint(_('Scheduled execution for script {0} has updated').format(script_name))
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ def get_server_script_map():
|
|||
script_map.setdefault(script.reference_doctype, {}).setdefault(script.doctype_event, []).append(script.name)
|
||||
else:
|
||||
script_map.setdefault('_api', {})[script.api_method] = script.name
|
||||
|
||||
frappe.cache().set_value('server_script_map', script_map)
|
||||
|
||||
return script_map
|
||||
return script_map
|
||||
|
|
@ -97,47 +97,49 @@ frappe.ui.form.on('User', {
|
|||
});
|
||||
}, __("Password"));
|
||||
|
||||
frappe.db.get_single_value("LDAP Settings", "enabled").then((value) => {
|
||||
if (value === 1 && frm.doc.name != "Administrator") {
|
||||
frm.add_custom_button(__("Reset LDAP Password"), function() {
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __("Reset LDAP Password"),
|
||||
fields: [
|
||||
{
|
||||
label: __("New Password"),
|
||||
fieldtype: "Password",
|
||||
fieldname: "new_password",
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __("Confirm New Password"),
|
||||
fieldtype: "Password",
|
||||
fieldname: "confirm_password",
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __("Logout All Sessions"),
|
||||
fieldtype: "Check",
|
||||
fieldname: "logout_sessions"
|
||||
if (frappe.user.has_role("System Manager")) {
|
||||
frappe.db.get_single_value("LDAP Settings", "enabled").then((value) => {
|
||||
if (value === 1 && frm.doc.name != "Administrator") {
|
||||
frm.add_custom_button(__("Reset LDAP Password"), function() {
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __("Reset LDAP Password"),
|
||||
fields: [
|
||||
{
|
||||
label: __("New Password"),
|
||||
fieldtype: "Password",
|
||||
fieldname: "new_password",
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __("Confirm New Password"),
|
||||
fieldtype: "Password",
|
||||
fieldname: "confirm_password",
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __("Logout All Sessions"),
|
||||
fieldtype: "Check",
|
||||
fieldname: "logout_sessions"
|
||||
}
|
||||
],
|
||||
primary_action: (values) => {
|
||||
d.hide();
|
||||
if (values.new_password !== values.confirm_password) {
|
||||
frappe.throw(__("Passwords do not match!"));
|
||||
}
|
||||
frappe.call(
|
||||
"frappe.integrations.doctype.ldap_settings.ldap_settings.reset_password", {
|
||||
user: frm.doc.email,
|
||||
password: values.new_password,
|
||||
logout: values.logout_sessions
|
||||
});
|
||||
}
|
||||
],
|
||||
primary_action: (values) => {
|
||||
d.hide();
|
||||
if (values.new_password !== values.confirm_password) {
|
||||
frappe.throw(__("Passwords do not match!"));
|
||||
}
|
||||
frappe.call(
|
||||
"frappe.integrations.doctype.ldap_settings.ldap_settings.reset_password", {
|
||||
user: frm.doc.email,
|
||||
password: values.new_password,
|
||||
logout: values.logout_sessions
|
||||
});
|
||||
}
|
||||
});
|
||||
d.show();
|
||||
}, __("Password"));
|
||||
}
|
||||
});
|
||||
});
|
||||
d.show();
|
||||
}, __("Password"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
frm.add_custom_button(__("Reset OTP Secret"), function() {
|
||||
frappe.call({
|
||||
|
|
|
|||
|
|
@ -551,6 +551,7 @@ def update_password(new_password, logout_all_sessions=0, key=None, old_password=
|
|||
|
||||
res = _get_user_for_update_password(key, old_password)
|
||||
if res.get('message'):
|
||||
frappe.local.response.http_status_code = 410
|
||||
return res['message']
|
||||
else:
|
||||
user = res['user']
|
||||
|
|
@ -718,7 +719,7 @@ def _get_user_for_update_password(key, old_password):
|
|||
user = frappe.db.get_value("User", {"reset_password_key": key})
|
||||
if not user:
|
||||
return {
|
||||
'message': _("Cannot Update: Incorrect / Expired Link.")
|
||||
'message': _("The Link specified has either been used before or Invalid")
|
||||
}
|
||||
|
||||
elif old_password:
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ import unittest
|
|||
|
||||
class TestUserPermission(unittest.TestCase):
|
||||
def setUp(self):
|
||||
frappe.db.sql("DELETE FROM `tabUser Permission` WHERE `user`='test_bulk_creation_update@example.com'")
|
||||
frappe.db.sql("""DELETE FROM `tabUser Permission`
|
||||
WHERE `user` in ('test_bulk_creation_update@example.com', 'test_user_perm1@example.com')""")
|
||||
|
||||
def test_default_user_permission_validation(self):
|
||||
user = create_user('test_default_permission@example.com')
|
||||
|
|
@ -20,6 +21,26 @@ class TestUserPermission(unittest.TestCase):
|
|||
param = get_params(user, 'User', perm_user.name, is_default=1)
|
||||
self.assertRaises(frappe.ValidationError, add_user_permissions, param)
|
||||
|
||||
def test_default_user_permission(self):
|
||||
frappe.set_user('Administrator')
|
||||
user = create_user('test_user_perm1@example.com', 'Website Manager')
|
||||
for category in ['general', 'public']:
|
||||
if not frappe.db.exists('Blog Category', category):
|
||||
frappe.get_doc({'doctype': 'Blog Category',
|
||||
'category_name': category, 'title': category}).insert()
|
||||
|
||||
param = get_params(user, 'Blog Category', 'general', is_default=1)
|
||||
add_user_permissions(param)
|
||||
|
||||
param = get_params(user, 'Blog Category', 'public')
|
||||
add_user_permissions(param)
|
||||
|
||||
frappe.set_user('test_user_perm1@example.com')
|
||||
doc = frappe.new_doc("Blog Post")
|
||||
|
||||
self.assertEquals(doc.blog_category, 'general')
|
||||
frappe.set_user('Administrator')
|
||||
|
||||
def test_apply_to_all(self):
|
||||
''' Create User permission for User having access to all applicable Doctypes'''
|
||||
user = create_user('test_bulk_creation_update@example.com')
|
||||
|
|
@ -88,7 +109,7 @@ class TestUserPermission(unittest.TestCase):
|
|||
self.assertIsNone(removed_applicable_second)
|
||||
self.assertEquals(is_created, 1)
|
||||
|
||||
def create_user(email):
|
||||
def create_user(email, role="System Manager"):
|
||||
''' create user with role system manager '''
|
||||
if frappe.db.exists('User', email):
|
||||
return frappe.get_doc('User', email)
|
||||
|
|
@ -96,7 +117,7 @@ def create_user(email):
|
|||
user = frappe.new_doc('User')
|
||||
user.email = email
|
||||
user.first_name = email.split("@")[0]
|
||||
user.add_roles("System Manager")
|
||||
user.add_roles(role)
|
||||
return user
|
||||
|
||||
def get_params(user, doctype, docname, is_default=0, applicable=None):
|
||||
|
|
|
|||
|
|
@ -6,5 +6,5 @@ from __future__ import unicode_literals
|
|||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestCSSClass(unittest.TestCase):
|
||||
class TestVideo(unittest.TestCase):
|
||||
pass
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2020, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Web View', {
|
||||
frappe.ui.form.on('Video', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
106
frappe/core/doctype/video/video.json
Normal file
106
frappe/core/doctype/video/video.json
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:title",
|
||||
"creation": "2018-10-17 05:47:13.087395",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"provider",
|
||||
"url",
|
||||
"column_break_4",
|
||||
"publish_date",
|
||||
"duration",
|
||||
"section_break_7",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Title",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "provider",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Provider",
|
||||
"options": "YouTube\nVimeo",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "url",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "URL",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "publish_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Publish Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "duration",
|
||||
"fieldtype": "Data",
|
||||
"label": "Duration"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_list_view": 1,
|
||||
"label": "Description",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-04-22 12:09:49.057403",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Video",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -6,5 +6,5 @@ from __future__ import unicode_literals
|
|||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class CSSClass(Document):
|
||||
class Video(Document):
|
||||
pass
|
||||
|
|
@ -28,6 +28,7 @@ def get_info(show_failed=False):
|
|||
if j.kwargs.get('site')==frappe.local.site:
|
||||
jobs.append({
|
||||
'job_name': j.kwargs.get('kwargs', {}).get('playbook_method') \
|
||||
or j.kwargs.get('kwargs', {}).get('job_type') \
|
||||
or str(j.kwargs.get('job_name')),
|
||||
'status': j.get_status(), 'queue': name,
|
||||
'creation': format_datetime(convert_utc_to_user_timezone(j.created_at)),
|
||||
|
|
|
|||
|
|
@ -76,7 +76,16 @@ class Dashboard {
|
|||
}
|
||||
|
||||
refresh() {
|
||||
this.get_permitted_dashboard_charts().then(charts => {
|
||||
frappe.run_serially([
|
||||
() => this.render_cards(),
|
||||
() => this.render_charts()
|
||||
]);
|
||||
}
|
||||
|
||||
render_charts() {
|
||||
return this.get_permitted_items(
|
||||
'frappe.desk.doctype.dashboard.dashboard.get_permitted_charts'
|
||||
).then(charts => {
|
||||
if (!charts.length) {
|
||||
frappe.msgprint(__('No Permitted Charts on this Dashboard'), __('No Permitted Charts'))
|
||||
}
|
||||
|
|
@ -92,6 +101,7 @@ class Dashboard {
|
|||
...chart
|
||||
}
|
||||
});
|
||||
|
||||
this.chart_group = new frappe.widget.WidgetGroup({
|
||||
title: null,
|
||||
container: this.container,
|
||||
|
|
@ -110,14 +120,46 @@ class Dashboard {
|
|||
});
|
||||
}
|
||||
|
||||
get_permitted_dashboard_charts() {
|
||||
render_cards() {
|
||||
return this.get_permitted_items(
|
||||
'frappe.desk.doctype.dashboard.dashboard.get_permitted_cards'
|
||||
).then(cards => {
|
||||
if (!cards.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.number_cards =
|
||||
cards.map(card => {
|
||||
return {
|
||||
name: card.card,
|
||||
};
|
||||
});
|
||||
|
||||
this.number_card_group = new frappe.widget.WidgetGroup({
|
||||
container: this.container,
|
||||
type: "number_card",
|
||||
columns: 3,
|
||||
options: {
|
||||
allow_sorting: false,
|
||||
allow_create: false,
|
||||
allow_delete: false,
|
||||
allow_hiding: false,
|
||||
allow_edit: false,
|
||||
},
|
||||
widgets: this.number_cards,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get_permitted_items(method) {
|
||||
return frappe.xcall(
|
||||
'frappe.desk.doctype.dashboard.dashboard.get_permitted_charts',
|
||||
method,
|
||||
{
|
||||
dashboard_name: this.dashboard_name
|
||||
}).then(charts => {
|
||||
return charts;
|
||||
});
|
||||
}
|
||||
).then(items => {
|
||||
return items;
|
||||
});
|
||||
}
|
||||
|
||||
set_dropdown() {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ frappe.PermissionEngine = Class.extend({
|
|||
setup_page: function() {
|
||||
var me = this;
|
||||
this.doctype_select
|
||||
= this.wrapper.page.add_select(__("Document Types"),
|
||||
= this.wrapper.page.add_select(__("Document Type"),
|
||||
[{value: "", label: __("Select Document Type")+"..."}].concat(this.options.doctypes))
|
||||
.change(function() {
|
||||
frappe.set_route("permission-manager", $(this).val());
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
"in_list_view",
|
||||
"in_standard_filter",
|
||||
"in_global_search",
|
||||
"in_preview",
|
||||
"bold",
|
||||
"report_hide",
|
||||
"search_index",
|
||||
|
|
@ -371,12 +372,18 @@
|
|||
"fieldname": "allow_in_quick_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow in Quick Entry"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_preview",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Preview"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-glass",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2020-03-16 14:52:43.954709",
|
||||
"modified": "2020-04-10 11:57:10.392218",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Custom Field",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"track_views",
|
||||
"allow_auto_repeat",
|
||||
"allow_import",
|
||||
"show_preview_popup",
|
||||
"image_view",
|
||||
"column_break_5",
|
||||
"title_field",
|
||||
|
|
@ -203,6 +204,12 @@
|
|||
"depends_on": "doc_type",
|
||||
"fieldname": "section_break_23",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_preview_popup",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Preview Popup"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
|
|
@ -210,7 +217,7 @@
|
|||
"idx": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-03-27 15:06:35.443861",
|
||||
"modified": "2020-04-10 12:16:01.320411",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ doctype_properties = {
|
|||
'track_views': 'Check',
|
||||
'allow_auto_repeat': 'Check',
|
||||
'allow_import': 'Check',
|
||||
'show_preview_popup': 'Check',
|
||||
'email_append_to': 'Check',
|
||||
'subject_field': 'Data',
|
||||
'sender_field': 'Data'
|
||||
|
|
@ -53,6 +54,7 @@ docfield_properties = {
|
|||
'in_list_view': 'Check',
|
||||
'in_standard_filter': 'Check',
|
||||
'in_global_search': 'Check',
|
||||
'in_preview': 'Check',
|
||||
'bold': 'Check',
|
||||
'hidden': 'Check',
|
||||
'collapsible': 'Check',
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"in_list_view",
|
||||
"in_standard_filter",
|
||||
"in_global_search",
|
||||
"in_preview",
|
||||
"bold",
|
||||
"allow_in_quick_entry",
|
||||
"translatable",
|
||||
|
|
@ -381,12 +382,18 @@
|
|||
"fieldtype": "Code",
|
||||
"label": "Read Only Depends On",
|
||||
"options": "JS"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "in_preview",
|
||||
"fieldtype": "Check",
|
||||
"label": "In Preview"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-15 02:26:59.673750",
|
||||
"modified": "2020-04-10 11:58:44.573537",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form Field",
|
||||
|
|
|
|||
|
|
@ -124,6 +124,8 @@ class Database(object):
|
|||
# in transaction validations
|
||||
self.check_transaction_status(query)
|
||||
|
||||
self.clear_db_table_cache(query)
|
||||
|
||||
# autocommit
|
||||
if auto_commit: self.commit()
|
||||
|
||||
|
|
@ -277,6 +279,11 @@ class Database(object):
|
|||
ret.append(frappe._dict(zip(keys, values)))
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def clear_db_table_cache(query):
|
||||
if query and query.strip().split()[0].lower() in {'drop', 'create'}:
|
||||
frappe.cache().delete_key('db_tables')
|
||||
|
||||
@staticmethod
|
||||
def needs_formatting(result, formatted):
|
||||
"""Returns true if the first row in the result has a Date, Datetime, Long Int."""
|
||||
|
|
@ -769,7 +776,16 @@ class Database(object):
|
|||
return ("tab" + doctype) in self.get_tables()
|
||||
|
||||
def get_tables(self):
|
||||
return [d[0] for d in self.sql("select table_name from information_schema.tables where table_schema not in ('pg_catalog', 'information_schema')")]
|
||||
tables = frappe.cache().get_value('db_tables')
|
||||
if not tables:
|
||||
table_rows = self.sql("""
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
||||
""")
|
||||
tables = {d[0] for d in table_rows}
|
||||
frappe.cache().set_value('db_tables', tables)
|
||||
return tables
|
||||
|
||||
def a_row_exists(self, doctype):
|
||||
"""Returns True if atleast one row exists."""
|
||||
|
|
|
|||
|
|
@ -137,16 +137,14 @@ class DBTable:
|
|||
if frappe.db.is_missing_column(e):
|
||||
# Unknown column 'column_name' in 'field list'
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
raise
|
||||
|
||||
if max_length and max_length[0][0] and max_length[0][0] > new_length:
|
||||
if col.fieldname in self.columns:
|
||||
self.columns[col.fieldname].length = current_length
|
||||
|
||||
frappe.msgprint(_("""Reverting length to {0} for '{1}' in '{2}';
|
||||
Setting the length as {3} will cause truncation of data.""")
|
||||
.format(current_length, col.fieldname, self.doctype, new_length))
|
||||
info_message = _("Reverting length to {0} for '{1}' in '{2}'. Setting the length as {3} will cause truncation of data.") \
|
||||
.format(current_length, col.fieldname, self.doctype, new_length)
|
||||
frappe.msgprint(info_message)
|
||||
|
||||
def is_new(self):
|
||||
return self.table_name not in frappe.db.get_tables()
|
||||
|
|
|
|||
|
|
@ -4,5 +4,21 @@
|
|||
frappe.ui.form.on('Dashboard', {
|
||||
refresh: function(frm) {
|
||||
frm.add_custom_button(__("Show Dashboard"), () => frappe.set_route('dashboard', frm.doc.name));
|
||||
|
||||
frm.set_query("chart", "charts", function() {
|
||||
return {
|
||||
filters: {
|
||||
is_public: 1
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("card", "cards", function() {
|
||||
return {
|
||||
filters: {
|
||||
is_public: 1
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
"field_order": [
|
||||
"dashboard_name",
|
||||
"is_default",
|
||||
"charts"
|
||||
"charts",
|
||||
"cards"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -31,10 +32,16 @@
|
|||
"label": "Charts",
|
||||
"options": "Dashboard Chart Link",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cards",
|
||||
"fieldtype": "Table",
|
||||
"label": "Cards",
|
||||
"options": "Number Card Link"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-03-25 21:09:37.080132",
|
||||
"modified": "2020-04-19 17:44:36.237163",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Dashboard",
|
||||
|
|
|
|||
|
|
@ -21,3 +21,12 @@ def get_permitted_charts(dashboard_name):
|
|||
if frappe.has_permission('Dashboard Chart', doc=chart.chart):
|
||||
permitted_charts.append(chart)
|
||||
return permitted_charts
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_permitted_cards(dashboard_name):
|
||||
permitted_cards = []
|
||||
dashboard = frappe.get_doc('Dashboard', dashboard_name)
|
||||
for card in dashboard.cards:
|
||||
if frappe.has_permission('Number Card', doc=card.card):
|
||||
permitted_cards.append(card)
|
||||
return permitted_cards
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
frm.add_fetch('source', 'timeseries', 'timeseries');
|
||||
},
|
||||
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.chart_filters = null;
|
||||
frm.add_custom_button('Add Chart to Dashboard', () => {
|
||||
|
|
@ -59,6 +60,10 @@ frappe.ui.form.on('Dashboard Chart', {
|
|||
if (frm.doc.report_name) {
|
||||
frm.trigger('set_chart_report_filters');
|
||||
}
|
||||
|
||||
if (!frappe.boot.developer_mode) {
|
||||
frm.set_df_property("custom_options", "hidden", 1);
|
||||
}
|
||||
},
|
||||
|
||||
source: function(frm) {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
"aggregate_function_based_on",
|
||||
"number_of_groups",
|
||||
"column_break_6",
|
||||
"is_public",
|
||||
"timespan",
|
||||
"from_date",
|
||||
"to_date",
|
||||
|
|
@ -33,6 +34,7 @@
|
|||
"type",
|
||||
"column_break_2",
|
||||
"color",
|
||||
"custom_options",
|
||||
"section_break_10",
|
||||
"last_synced_on"
|
||||
],
|
||||
|
|
@ -98,7 +100,7 @@
|
|||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.chart_type !== 'Group By'",
|
||||
"depends_on": "eval: ['Count', 'Sum', 'Average'].includes(doc.chart_type)",
|
||||
"fieldname": "timeseries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Time Series"
|
||||
|
|
@ -124,7 +126,7 @@
|
|||
"fieldname": "type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Type",
|
||||
"options": "Line\nBar\nPercentage\nPie",
|
||||
"options": "Line\nBar\nPercentage\nPie\nDonut",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -213,10 +215,23 @@
|
|||
"label": "Y Axis",
|
||||
"mandatory_depends_on": "eval:doc.report_name && !doc.is_custom",
|
||||
"options": "Dashboard Chart Field"
|
||||
},
|
||||
{
|
||||
"description": "Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"]",
|
||||
"fieldname": "custom_options",
|
||||
"fieldtype": "Code",
|
||||
"label": "Custom Options"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "This chart will be available to all Users if this is set",
|
||||
"fieldname": "is_public",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Public"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-04-08 18:54:36.739183",
|
||||
"modified": "2020-05-01 15:22:59.119341",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Dashboard Chart",
|
||||
|
|
@ -247,6 +262,7 @@
|
|||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
|
|
|
|||
|
|
@ -76,10 +76,10 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
|
|||
if to_date and len(to_date):
|
||||
to_date = get_datetime(to_date)
|
||||
else:
|
||||
to_date = chart.to_date
|
||||
to_date = get_datetime(chart.to_date)
|
||||
|
||||
timegrain = time_interval or chart.time_interval
|
||||
filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json)
|
||||
filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json) or []
|
||||
|
||||
# don't include cancelled documents
|
||||
filters.append([chart.document_type, 'docstatus', '<', 2, False])
|
||||
|
|
@ -92,22 +92,33 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
|
|||
return chart_config
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_report_chart(args):
|
||||
def create_dashboard_chart(args):
|
||||
args = frappe.parse_json(args)
|
||||
_doc = frappe.new_doc('Dashboard Chart')
|
||||
doc = frappe.new_doc('Dashboard Chart')
|
||||
|
||||
doc.update(args)
|
||||
|
||||
if args.get('custom_options'):
|
||||
doc.custom_options = json.dumps(args.get('custom_options'))
|
||||
|
||||
_doc.update(args)
|
||||
if frappe.db.exists('Dashboard Chart', args.chart_name):
|
||||
args.chart_name = append_number_if_name_exists('Dashboard Chart', args.chart_name)
|
||||
_doc.chart_name = args.chart_name
|
||||
_doc.insert(ignore_permissions=True)
|
||||
doc.chart_name = args.chart_name
|
||||
doc.insert(ignore_permissions=True)
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_report_chart(args):
|
||||
create_dashboard_chart(args)
|
||||
args = frappe.parse_json(args)
|
||||
if args.dashboard:
|
||||
add_chart_to_dashboard(json.dumps(args))
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_chart_to_dashboard(args):
|
||||
args = frappe.parse_json(args)
|
||||
|
||||
dashboard = frappe.get_doc('Dashboard', args.dashboard)
|
||||
dashboard_link = frappe.new_doc('Dashboard Chart Link')
|
||||
dashboard_link.chart = args.chart_name
|
||||
|
|
@ -351,6 +362,13 @@ def get_year_ending(date):
|
|||
# last day of this month
|
||||
return add_to_date(date, days=-1)
|
||||
|
||||
def get_charts_for_user(doctype, txt, searchfield, start, page_len, filters):
|
||||
or_filters = {'owner': frappe.session.user, 'is_public': 1}
|
||||
return frappe.db.get_list('Dashboard Chart',
|
||||
fields=['name'],
|
||||
filters=filters,
|
||||
or_filters=or_filters,
|
||||
as_list = 1)
|
||||
|
||||
class DashboardChart(Document):
|
||||
|
||||
|
|
@ -362,6 +380,8 @@ class DashboardChart(Document):
|
|||
self.check_required_field()
|
||||
self.check_document_type()
|
||||
|
||||
self.validate_custom_options()
|
||||
|
||||
def check_required_field(self):
|
||||
if not self.document_type:
|
||||
frappe.throw(_("Document type is required to create a dashboard chart"))
|
||||
|
|
@ -378,3 +398,10 @@ class DashboardChart(Document):
|
|||
def check_document_type(self):
|
||||
if frappe.get_meta(self.document_type).issingle:
|
||||
frappe.throw("You cannot create a dashboard chart from single DocTypes")
|
||||
|
||||
def validate_custom_options(self):
|
||||
if self.custom_options:
|
||||
try:
|
||||
json.loads(self.custom_options)
|
||||
except ValueError as error:
|
||||
frappe.throw("Invalid json added in the custom options: %s" % error)
|
||||
119
frappe/desk/doctype/number_card/number_card.js
Normal file
119
frappe/desk/doctype/number_card/number_card.js
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
// Copyright (c) 2020, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Number Card', {
|
||||
refresh: function(frm) {
|
||||
frm.set_df_property("filters_section", "hidden", 1);
|
||||
frm.trigger('set_options');
|
||||
frm.trigger('render_filters_table');
|
||||
},
|
||||
|
||||
document_type: function(frm) {
|
||||
frm.set_query('document_type', function() {
|
||||
return {
|
||||
filters: {
|
||||
'issingle': false
|
||||
}
|
||||
};
|
||||
});
|
||||
frm.set_value('filters_json', '[]');
|
||||
frm.set_value('aggregate_function_based_on', '');
|
||||
frm.trigger('set_options');
|
||||
},
|
||||
|
||||
set_options: function(frm) {
|
||||
let aggregate_based_on_fields = [];
|
||||
const doctype = frm.doc.document_type;
|
||||
|
||||
if (doctype) {
|
||||
frappe.model.with_doctype(doctype, () => {
|
||||
frappe.get_meta(doctype).fields.map(df => {
|
||||
if (frappe.model.numeric_fieldtypes.includes(df.fieldtype)) {
|
||||
if (df.fieldtype == 'Currency') {
|
||||
if (!df.options || df.options !== 'Company:company:default_currency') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
aggregate_based_on_fields.push({label: df.label, value: df.fieldname});
|
||||
}
|
||||
});
|
||||
|
||||
frm.set_df_property('aggregate_function_based_on', 'options', aggregate_based_on_fields);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
render_filters_table: function(frm) {
|
||||
frm.set_df_property("filters_section", "hidden", 0);
|
||||
|
||||
let wrapper = $(frm.get_field('filters_json').wrapper).empty();
|
||||
frm.filter_table = $(`<table class="table table-bordered" style="cursor:pointer; margin:0px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 33%">${__('Filter')}</th>
|
||||
<th style="width: 33%">${__('Condition')}</th>
|
||||
<th>${__('Value')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>`).appendTo(wrapper);
|
||||
|
||||
frm.filters = JSON.parse(frm.doc.filters_json || '[]');
|
||||
|
||||
frm.trigger('set_filters_in_table');
|
||||
|
||||
frm.filter_table.on('click', () => {
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __('Set Filters'),
|
||||
fields: [{
|
||||
fieldtype: 'HTML',
|
||||
fieldname: 'filter_area',
|
||||
}],
|
||||
primary_action: function() {
|
||||
let values = this.get_values();
|
||||
if (values) {
|
||||
this.hide();
|
||||
frm.filters = frm.filter_group.get_filters();
|
||||
frm.set_value('filters_json', JSON.stringify(frm.filters));
|
||||
frm.trigger('set_filters_in_table');
|
||||
}
|
||||
},
|
||||
primary_action_label: "Set"
|
||||
});
|
||||
|
||||
frappe.dashboards.filters_dialog = dialog;
|
||||
|
||||
frm.filter_group = new frappe.ui.FilterGroup({
|
||||
parent: dialog.get_field('filter_area').$wrapper,
|
||||
doctype: frm.doc.document_type,
|
||||
on_change: () => {},
|
||||
});
|
||||
|
||||
frm.filter_group.add_filters_to_filter_group(frm.filters);
|
||||
|
||||
dialog.show();
|
||||
dialog.set_values(frm.filters);
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
set_filters_in_table: function(frm) {
|
||||
if (!frm.filters.length) {
|
||||
const filter_row = $(`<tr><td colspan="3" class="text-muted text-center">
|
||||
${__("Click to Set Filters")}</td></tr>`);
|
||||
frm.filter_table.find('tbody').html(filter_row);
|
||||
} else {
|
||||
let filter_rows = '';
|
||||
frm.filters.forEach(filter => {
|
||||
filter_rows +=
|
||||
`<tr>
|
||||
<td>${filter[1]}</td>
|
||||
<td>${filter[2] || ""}</td>
|
||||
<td>${filter[3]}</td>
|
||||
</tr>`;
|
||||
|
||||
});
|
||||
frm.filter_table.find('tbody').html(filter_rows);
|
||||
}
|
||||
}
|
||||
});
|
||||
147
frappe/desk/doctype/number_card/number_card.json
Normal file
147
frappe/desk/doctype/number_card/number_card.json
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "CARD.#####",
|
||||
"creation": "2020-04-15 18:06:39.444683",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"label",
|
||||
"function",
|
||||
"aggregate_function_based_on",
|
||||
"column_break_2",
|
||||
"document_type",
|
||||
"is_public",
|
||||
"stats_section",
|
||||
"show_percentage_stats",
|
||||
"stats_time_interval",
|
||||
"filters_section",
|
||||
"filters_json",
|
||||
"color"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "document_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Document Type",
|
||||
"options": "DocType",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.document_type",
|
||||
"fieldname": "function",
|
||||
"fieldtype": "Select",
|
||||
"label": "Function",
|
||||
"options": "Count\nSum\nAverage\nMinimum\nMaximum",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.function !== 'Count'",
|
||||
"fieldname": "aggregate_function_based_on",
|
||||
"fieldtype": "Select",
|
||||
"label": "Aggregate Function Based On",
|
||||
"mandatory_depends_on": "eval: doc.function !== 'Count'"
|
||||
},
|
||||
{
|
||||
"fieldname": "filters_json",
|
||||
"fieldtype": "Code",
|
||||
"label": "Filters JSON",
|
||||
"options": "JSON"
|
||||
},
|
||||
{
|
||||
"fieldname": "label",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Label",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "color",
|
||||
"fieldtype": "Color",
|
||||
"label": "Color"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "filters_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Filters Section"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "This card will be available to all Users if this is set",
|
||||
"fieldname": "is_public",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Public"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "show_percentage_stats",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Percentage Stats"
|
||||
},
|
||||
{
|
||||
"default": "Daily",
|
||||
"depends_on": "eval: doc.show_percentage_stats",
|
||||
"description": "Show percentage difference according to this time interval",
|
||||
"fieldname": "stats_time_interval",
|
||||
"fieldtype": "Select",
|
||||
"label": "Stats Time Interval",
|
||||
"options": "Daily\nWeekly\nMonthly\nYearly"
|
||||
},
|
||||
{
|
||||
"fieldname": "stats_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Stats"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-05-01 15:23:29.550243",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Number Card",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Dashboard Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "label, document_type",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "label",
|
||||
"track_changes": 1
|
||||
}
|
||||
144
frappe/desk/doctype/number_card/number_card.py
Normal file
144
frappe/desk/doctype/number_card/number_card.py
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
# -*- 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 import cint
|
||||
|
||||
class NumberCard(Document):
|
||||
pass
|
||||
|
||||
|
||||
def get_permission_query_conditions(user=None):
|
||||
if not user:
|
||||
user = frappe.session.user
|
||||
|
||||
if user == 'Administrator':
|
||||
return
|
||||
|
||||
roles = frappe.get_roles(user)
|
||||
if "System Manager" in roles:
|
||||
return None
|
||||
|
||||
allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read())
|
||||
|
||||
return '''
|
||||
`tabNumber Card`.`document_type` in {allowed_doctypes}
|
||||
'''.format(
|
||||
allowed_doctypes=allowed_doctypes,
|
||||
)
|
||||
|
||||
def has_permission(doc, ptype, user):
|
||||
roles = frappe.get_roles(user)
|
||||
if "System Manager" in roles:
|
||||
return True
|
||||
|
||||
allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read())
|
||||
if doc.document_type in allowed_doctypes:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_result(doc, to_date=None):
|
||||
doc = frappe.parse_json(doc)
|
||||
fields = []
|
||||
sql_function_map = {
|
||||
'Count': 'count',
|
||||
'Sum': 'sum',
|
||||
'Average': 'avg',
|
||||
'Minimum': 'min',
|
||||
'Maximum': 'max'
|
||||
}
|
||||
|
||||
function = sql_function_map[doc.function]
|
||||
|
||||
if function == 'count':
|
||||
fields = ['{function}(*) as result'.format(function=function)]
|
||||
else:
|
||||
fields = ['{function}({based_on}) as result'.format(function=function, based_on=doc.aggregate_function_based_on)]
|
||||
|
||||
filters = frappe.parse_json(doc.filters_json)
|
||||
|
||||
if to_date:
|
||||
filters.append([doc.document_type, 'creation', '<', to_date, False])
|
||||
|
||||
res = frappe.db.get_all(doc.document_type, fields=fields, filters=filters)
|
||||
number = res[0]['result'] if res else 0
|
||||
|
||||
return cint(number)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_percentage_difference(doc, result):
|
||||
doc = frappe.parse_json(doc)
|
||||
result = frappe.parse_json(result)
|
||||
|
||||
doc = frappe.get_doc('Number Card', doc.name)
|
||||
|
||||
if not doc.get('show_percentage_stats'):
|
||||
return
|
||||
|
||||
previous_result = calculate_previous_result(doc)
|
||||
difference = (result - previous_result)/100.0
|
||||
|
||||
return difference
|
||||
|
||||
|
||||
def calculate_previous_result(doc):
|
||||
from frappe.utils import add_to_date
|
||||
|
||||
current_date = frappe.utils.now()
|
||||
if doc.stats_time_interval == 'Daily':
|
||||
previous_date = add_to_date(current_date, days=-1)
|
||||
elif doc.stats_time_interval == 'Weekly':
|
||||
previous_date = add_to_date(current_date, weeks=-1)
|
||||
elif doc.stats_time_interval == 'Monthly':
|
||||
previous_date = add_to_date(current_date, months=-1)
|
||||
else:
|
||||
previous_date = add_to_date(current_date, years=-1)
|
||||
|
||||
number = get_result(doc, previous_date)
|
||||
return number
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_number_card(args):
|
||||
args = frappe.parse_json(args)
|
||||
doc = frappe.new_doc('Number Card')
|
||||
|
||||
doc.update(args)
|
||||
doc.insert(ignore_permissions=True)
|
||||
return doc
|
||||
|
||||
def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters):
|
||||
meta = frappe.get_meta(doctype)
|
||||
searchfields = meta.get_search_fields()
|
||||
search_conditions = []
|
||||
|
||||
if txt:
|
||||
for field in searchfields:
|
||||
search_conditions.append('`tab{doctype}`.`{field}` like %(txt)s'.format(field=field, doctype=doctype, txt=txt))
|
||||
|
||||
search_conditions = ' or '.join(search_conditions)
|
||||
|
||||
search_conditions = 'and (' + search_conditions +')' if search_conditions else ''
|
||||
conditions, values = frappe.db.build_conditions(filters)
|
||||
values['txt'] = '%' + txt + '%'
|
||||
|
||||
return frappe.db.sql(
|
||||
'''select
|
||||
`tabNumber Card`.name, `tabNumber Card`.label, `tabNumber Card`.document_type
|
||||
from
|
||||
`tabNumber Card`
|
||||
where
|
||||
{conditions} and
|
||||
(`tabNumber Card`.owner = '{user}' or
|
||||
`tabNumber Card`.is_public = 1)
|
||||
{search_conditions}
|
||||
'''.format(
|
||||
filters=filters,
|
||||
user=frappe.session.user,
|
||||
search_conditions=search_conditions,
|
||||
conditions=conditions
|
||||
), values)
|
||||
10
frappe/desk/doctype/number_card/test_number_card.py
Normal file
10
frappe/desk/doctype/number_card/test_number_card.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestNumberCard(unittest.TestCase):
|
||||
pass
|
||||
31
frappe/desk/doctype/number_card_link/number_card_link.json
Normal file
31
frappe/desk/doctype/number_card_link/number_card_link.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2020-04-19 17:43:50.858343",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"card"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "card",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Card",
|
||||
"options": "Number Card"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-19 17:45:11.878472",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Number Card Link",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
10
frappe/desk/doctype/number_card_link/number_card_link.py
Normal file
10
frappe/desk/doctype/number_card_link/number_card_link.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class NumberCardLink(Document):
|
||||
pass
|
||||
|
|
@ -196,8 +196,6 @@ class FormMeta(Meta):
|
|||
self.get("__messages").update(messages, as_value=True)
|
||||
|
||||
def load_dashboard(self):
|
||||
if self.custom:
|
||||
return
|
||||
self.set('__dashboard', self.get_dashboard_data())
|
||||
|
||||
def load_kanban_meta(self):
|
||||
|
|
|
|||
|
|
@ -268,8 +268,9 @@ def get_open_count(doctype, name, items=[]):
|
|||
"count": out,
|
||||
}
|
||||
|
||||
module = frappe.get_meta_module(doctype)
|
||||
if hasattr(module, "get_timeline_data"):
|
||||
out["timeline_data"] = module.get_timeline_data(doctype, name)
|
||||
if not meta.custom:
|
||||
module = frappe.get_meta_module(doctype)
|
||||
if hasattr(module, "get_timeline_data"):
|
||||
out["timeline_data"] = module.get_timeline_data(doctype, name)
|
||||
|
||||
return out
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ def get_prepared_report_result(report, filters, dn="", user=None):
|
|||
columns = json.loads(doc.columns) if doc.columns else data[0]
|
||||
|
||||
for column in columns:
|
||||
if isinstance(column, dict):
|
||||
if isinstance(column, dict) and column.get("label"):
|
||||
column["label"] = _(column["label"])
|
||||
|
||||
latest_report_data = {
|
||||
|
|
@ -299,6 +299,7 @@ def export_query():
|
|||
_("You can try changing the filters of your report."))
|
||||
return
|
||||
|
||||
data.columns = [col for col in data.columns if isinstance(col, dict) and not col.get('hidden')]
|
||||
columns = get_columns_dict(data.columns)
|
||||
|
||||
from frappe.utils.xlsxutils import make_xlsx
|
||||
|
|
@ -310,7 +311,7 @@ def export_query():
|
|||
frappe.response['type'] = 'binary'
|
||||
|
||||
|
||||
def build_xlsx_data(columns, data, visible_idx,include_indentation):
|
||||
def build_xlsx_data(columns, data, visible_idx, include_indentation):
|
||||
result = [[]]
|
||||
|
||||
# add column headings
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import socket
|
|||
import time
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import validate_email_address, cint, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html
|
||||
from frappe.utils import validate_email_address, cint, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html, add_days
|
||||
from frappe.utils.user import is_system_user
|
||||
from frappe.utils.jinja import render_template
|
||||
from frappe.email.smtp import SMTPServer
|
||||
|
|
@ -533,28 +533,37 @@ class EmailAccount(Document):
|
|||
parent = None
|
||||
in_reply_to = (email.mail.get("In-Reply-To") or "").strip(" <>")
|
||||
|
||||
if in_reply_to and "@{0}".format(frappe.local.site) in in_reply_to:
|
||||
# reply to a communication sent from the system
|
||||
email_queue = frappe.db.get_value('Email Queue', dict(message_id=in_reply_to), ['communication','reference_doctype', 'reference_name'])
|
||||
if email_queue:
|
||||
parent_communication, parent_doctype, parent_name = email_queue
|
||||
if parent_communication:
|
||||
communication.in_reply_to = parent_communication
|
||||
if in_reply_to:
|
||||
if "@{0}".format(frappe.local.site) in in_reply_to:
|
||||
# reply to a communication sent from the system
|
||||
email_queue = frappe.db.get_value('Email Queue', dict(message_id=in_reply_to), ['communication','reference_doctype', 'reference_name'])
|
||||
if email_queue:
|
||||
parent_communication, parent_doctype, parent_name = email_queue
|
||||
if parent_communication:
|
||||
communication.in_reply_to = parent_communication
|
||||
else:
|
||||
reference, domain = in_reply_to.split("@", 1)
|
||||
parent_doctype, parent_name = 'Communication', reference
|
||||
|
||||
if frappe.db.exists(parent_doctype, parent_name):
|
||||
parent = frappe._dict(doctype=parent_doctype, name=parent_name)
|
||||
|
||||
# set in_reply_to of current communication
|
||||
if parent_doctype=='Communication':
|
||||
# communication.in_reply_to = email_queue.communication
|
||||
|
||||
if parent.reference_name:
|
||||
# the true parent is the communication parent
|
||||
parent = frappe.get_doc(parent.reference_doctype,
|
||||
parent.reference_name)
|
||||
else:
|
||||
reference, domain = in_reply_to.split("@", 1)
|
||||
parent_doctype, parent_name = 'Communication', reference
|
||||
|
||||
if frappe.db.exists(parent_doctype, parent_name):
|
||||
parent = frappe._dict(doctype=parent_doctype, name=parent_name)
|
||||
|
||||
# set in_reply_to of current communication
|
||||
if parent_doctype=='Communication':
|
||||
# communication.in_reply_to = email_queue.communication
|
||||
|
||||
if parent.reference_name:
|
||||
# the true parent is the communication parent
|
||||
parent = frappe.get_doc(parent.reference_doctype,
|
||||
parent.reference_name)
|
||||
comm = frappe.db.get_value('Communication',
|
||||
dict(
|
||||
message_id=in_reply_to,
|
||||
creation=['>=', add_days(get_datetime(), -30)]),
|
||||
['reference_doctype', 'reference_name'], as_dict=1)
|
||||
if comm:
|
||||
parent = frappe._dict(doctype=comm.reference_doctype, name=comm.reference_name)
|
||||
|
||||
return parent
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class EmailDomain(Document):
|
|||
|
||||
except Exception:
|
||||
frappe.throw(_("Incoming email account not correct"))
|
||||
return None
|
||||
|
||||
finally:
|
||||
try:
|
||||
if self.use_imap:
|
||||
|
|
@ -48,9 +48,10 @@ class EmailDomain(Document):
|
|||
test.quit()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
if self.use_ssl_for_outgoing:
|
||||
if not self.smtp_port:
|
||||
if self.get('use_ssl_for_outgoing'):
|
||||
if not self.get('smtp_port'):
|
||||
self.smtp_port = 465
|
||||
|
||||
sess = smtplib.SMTP_SSL((self.smtp_server or "").encode('utf-8'),
|
||||
|
|
@ -62,28 +63,15 @@ class EmailDomain(Document):
|
|||
sess.quit()
|
||||
except Exception:
|
||||
frappe.throw(_("Outgoing email account not correct"))
|
||||
return None
|
||||
return
|
||||
|
||||
def on_update(self):
|
||||
"""update all email accounts using this domain"""
|
||||
for email_account in frappe.get_all("Email Account",
|
||||
filters={"domain": self.name}):
|
||||
|
||||
for email_account in frappe.get_all("Email Account", filters={"domain": self.name}):
|
||||
try:
|
||||
email_account = frappe.get_doc("Email Account",
|
||||
email_account.name)
|
||||
email_account.set("email_server",self.email_server)
|
||||
email_account.set("use_imap",self.use_imap)
|
||||
email_account.set("use_ssl",self.use_ssl)
|
||||
email_account.set("use_tls",self.use_tls)
|
||||
email_account.set("attachment_limit",self.attachment_limit)
|
||||
email_account.set("smtp_server",self.smtp_server)
|
||||
email_account.set("smtp_port",self.smtp_port)
|
||||
email_account.set("use_ssl_for_outgoing", self.use_ssl_for_outgoing)
|
||||
email_account.set("append_emails_to_sent_folder", self.append_emails_to_sent_folder)
|
||||
email_account = frappe.get_doc("Email Account", email_account.name)
|
||||
for attr in ["email_server", "use_imap", "use_ssl", "use_tls", "attachment_limit", "smtp_server", "smtp_port", "use_ssl_for_outgoing", "append_emails_to_sent_folder"]:
|
||||
email_account.set(attr, self.get(attr, default=0))
|
||||
email_account.save()
|
||||
|
||||
except Exception as e:
|
||||
frappe.msgprint(email_account.name)
|
||||
frappe.throw(e)
|
||||
return None
|
||||
frappe.msgprint(_("Error has occurred in {0}").format(email_account.name), raise_exception=e.__class__)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import frappe
|
|||
import sys
|
||||
from six.moves import html_parser as HTMLParser
|
||||
import smtplib, quopri, json
|
||||
from frappe import msgprint, _, safe_decode, safe_encode
|
||||
from frappe import msgprint, _, safe_decode, safe_encode, enqueue
|
||||
from frappe.email.smtp import SMTPServer, get_outgoing_email_account
|
||||
from frappe.email.email_body import get_email, get_formatted_html, add_attachment
|
||||
from frappe.utils.verified_command import get_signed_params, verify_request
|
||||
|
|
@ -347,8 +347,20 @@ def flush(from_test=False):
|
|||
if not smtpserver:
|
||||
smtpserver = SMTPServer()
|
||||
smtpserver_dict[email.sender] = smtpserver
|
||||
|
||||
send_one(email.name, smtpserver, auto_commit, from_test=from_test)
|
||||
|
||||
if from_test:
|
||||
send_one(email.name, smtpserver, auto_commit)
|
||||
else:
|
||||
send_one_args = {
|
||||
'email': email.name,
|
||||
'smtpserver': smtpserver,
|
||||
'auto_commit': auto_commit,
|
||||
}
|
||||
enqueue(
|
||||
method = 'frappe.email.queue.send_one',
|
||||
queue = 'short',
|
||||
**send_one_args
|
||||
)
|
||||
|
||||
# NOTE: removing commit here because we pass auto_commit
|
||||
# finally:
|
||||
|
|
@ -366,7 +378,7 @@ def get_queue():
|
|||
limit 500''', { 'now': now_datetime() }, as_dict=True)
|
||||
|
||||
|
||||
def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=False):
|
||||
def send_one(email, smtpserver=None, auto_commit=True, now=False):
|
||||
'''Send Email Queue with given smtpserver'''
|
||||
|
||||
email = frappe.db.sql('''select
|
||||
|
|
@ -377,8 +389,13 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals
|
|||
`tabEmail Queue`
|
||||
where
|
||||
name=%s
|
||||
for update''', email, as_dict=True)[0]
|
||||
|
||||
for update''', email, as_dict=True)
|
||||
|
||||
if len(email):
|
||||
email = email[0]
|
||||
else:
|
||||
return
|
||||
|
||||
recipients_list = frappe.db.sql('''select name, recipient, status from
|
||||
`tabEmail Queue Recipient` where parent=%s''', email.name, as_dict=1)
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ class TimestampMismatchError(ValidationError): pass
|
|||
class EmptyTableError(ValidationError): pass
|
||||
class LinkExistsError(ValidationError): pass
|
||||
class InvalidEmailAddressError(ValidationError): pass
|
||||
class InvalidNameError(ValidationError): pass
|
||||
class InvalidPhoneNumberError(ValidationError): pass
|
||||
class TemplateNotFoundError(ValidationError): pass
|
||||
class UniqueValidationError(ValidationError): pass
|
||||
|
|
@ -95,4 +96,4 @@ class DataTooLongException(ValidationError): pass
|
|||
# OAuth exceptions
|
||||
class InvalidAuthorizationHeader(CSRFTokenError): pass
|
||||
class InvalidAuthorizationPrefix(CSRFTokenError): pass
|
||||
class InvalidAuthorizationToken(CSRFTokenError): pass
|
||||
class InvalidAuthorizationToken(CSRFTokenError): pass
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ from frappe.core.doctype.server_script.server_script_utils import run_server_scr
|
|||
from werkzeug.wrappers import Response
|
||||
from six import string_types
|
||||
|
||||
ALLOWED_MIMETYPES = ('image/png', 'image/jpeg', 'application/pdf', 'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.spreadsheet')
|
||||
|
||||
|
||||
def handle():
|
||||
"""handle request"""
|
||||
validate_auth()
|
||||
|
|
@ -148,12 +154,14 @@ def uploadfile():
|
|||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def upload_file():
|
||||
user = None
|
||||
if frappe.session.user == 'Guest':
|
||||
if frappe.get_system_settings('allow_guests_to_upload_files'):
|
||||
ignore_permissions = True
|
||||
else:
|
||||
return
|
||||
else:
|
||||
user = frappe.get_doc("User", frappe.session.user)
|
||||
ignore_permissions = False
|
||||
|
||||
files = frappe.request.files
|
||||
|
|
@ -175,11 +183,11 @@ def upload_file():
|
|||
frappe.local.uploaded_file = content
|
||||
frappe.local.uploaded_filename = filename
|
||||
|
||||
if frappe.session.user == 'Guest':
|
||||
if frappe.session.user == 'Guest' or (user and not user.has_desk_access()):
|
||||
import mimetypes
|
||||
filetype = mimetypes.guess_type(filename)[0]
|
||||
if filetype not in ['image/png', 'image/jpeg', 'application/pdf']:
|
||||
frappe.throw("You can only upload JPG, PNG or PDF files.")
|
||||
if filetype not in ALLOWED_MIMETYPES:
|
||||
frappe.throw(_("You can only upload JPG, PNG, PDF, or Microsoft documents."))
|
||||
|
||||
if method:
|
||||
method = frappe.get_attr(method)
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ permission_query_conditions = {
|
|||
"Dashboard Settings": "frappe.desk.doctype.dashboard_settings.dashboard_settings.get_permission_query_conditions",
|
||||
"Notification Log": "frappe.desk.doctype.notification_log.notification_log.get_permission_query_conditions",
|
||||
"Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.get_permission_query_conditions",
|
||||
"Number Card": "frappe.desk.doctype.number_card.number_card.get_permission_query_conditions",
|
||||
"Notification Settings": "frappe.desk.doctype.notification_settings.notification_settings.get_permission_query_conditions",
|
||||
"Note": "frappe.desk.doctype.note.note.get_permission_query_conditions",
|
||||
"Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.get_permission_query_conditions",
|
||||
|
|
@ -105,6 +106,7 @@ has_permission = {
|
|||
"User": "frappe.core.doctype.user.user.has_permission",
|
||||
"Note": "frappe.desk.doctype.note.note.has_permission",
|
||||
"Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.has_permission",
|
||||
"Number Card": "frappe.desk.doctype.number_card.number_card.has_permission",
|
||||
"Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.has_permission",
|
||||
"Contact": "frappe.contacts.address_and_contact.has_permission",
|
||||
"Address": "frappe.contacts.address_and_contact.has_permission",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from __future__ import unicode_literals
|
|||
import frappe
|
||||
from frappe import _, safe_encode
|
||||
from frappe.model.document import Document
|
||||
|
||||
from frappe.twofactor import (should_run_2fa, authenticate_for_2factor,confirm_otp_token)
|
||||
|
||||
class LDAPSettings(Document):
|
||||
def validate(self):
|
||||
|
|
@ -237,6 +237,10 @@ def login():
|
|||
user = ldap.authenticate(frappe.as_unicode(args.usr), frappe.as_unicode(args.pwd))
|
||||
|
||||
frappe.local.login_manager.user = user.name
|
||||
if should_run_2fa(user.name):
|
||||
authenticate_for_2factor(user.name)
|
||||
if not confirm_otp_token(frappe.local.login_manager):
|
||||
return False
|
||||
frappe.local.login_manager.post_login()
|
||||
|
||||
# because of a GET request!
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ class Webhook(Document):
|
|||
if self.request_structure == "Form URL-Encoded":
|
||||
self.webhook_json = None
|
||||
elif self.request_structure == "JSON":
|
||||
validate_json(self.webhook_json)
|
||||
validate_template(self.webhook_json)
|
||||
self.webhook_data = []
|
||||
|
||||
|
|
@ -130,3 +131,10 @@ def get_webhook_data(doc, webhook):
|
|||
data = json.loads(data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def validate_json(string):
|
||||
try:
|
||||
json.loads(string)
|
||||
except (TypeError, ValueError):
|
||||
frappe.throw(_("Request Body consists of an invalid JSON structure"), title=_("Invalid JSON"))
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ table_fields = ('Table', 'Table MultiSelect')
|
|||
core_doctypes_list = ('DocType', 'DocField', 'DocPerm', 'DocType Action', 'DocType Link', 'User', 'Role', 'Has Role',
|
||||
'Page', 'Module Def', 'Print Format', 'Report', 'Customize Form',
|
||||
'Customize Form Field', 'Property Setter', 'Custom Field', 'Custom Script')
|
||||
data_field_options = ('Email', 'Phone')
|
||||
data_field_options = ('Email', 'Name', 'Phone')
|
||||
|
||||
def copytables(srctype, src, srcfield, tartype, tar, tarfield, srcfields, tarfields=[]):
|
||||
if not tarfields:
|
||||
|
|
|
|||
|
|
@ -11,11 +11,12 @@ from frappe.model import default_fields, table_fields
|
|||
from frappe.model.naming import set_new_name
|
||||
from frappe.model.utils.link_count import notify_link_count
|
||||
from frappe.modules import load_doctype_module
|
||||
from frappe.model import display_fieldtypes, data_fieldtypes
|
||||
from frappe.model import display_fieldtypes
|
||||
from frappe.utils.password import get_decrypted_password, set_encrypted_password
|
||||
from frappe.utils import (cint, flt, now, cstr, strip_html, getdate, get_datetime, to_timedelta,
|
||||
from frappe.utils import (cint, flt, now, cstr, strip_html,
|
||||
sanitize_html, sanitize_email, cast_fieldtype)
|
||||
from frappe.utils.html_utils import unescape_html
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
max_positive_value = {
|
||||
'smallint': 2 ** 15,
|
||||
|
|
@ -288,7 +289,7 @@ class BaseDocument(object):
|
|||
if k in default_fields:
|
||||
del doc[k]
|
||||
|
||||
for key in ("_user_tags", "__islocal", "__onload", "_liked_by", "__run_link_triggers"):
|
||||
for key in ("_user_tags", "__islocal", "__onload", "_liked_by", "__run_link_triggers", "__unsaved"):
|
||||
if self.get(key):
|
||||
doc[key] = self.get(key)
|
||||
|
||||
|
|
@ -564,13 +565,20 @@ class BaseDocument(object):
|
|||
for data_field in self.meta.get_data_fields():
|
||||
data = self.get(data_field.fieldname)
|
||||
data_field_options = data_field.get("options")
|
||||
old_fieldtype = data_field.get("oldfieldtype")
|
||||
|
||||
if old_fieldtype and old_fieldtype != "Data":
|
||||
continue
|
||||
|
||||
if data_field_options == "Email":
|
||||
if (self.owner in STANDARD_USERS) and (data in STANDARD_USERS):
|
||||
return
|
||||
continue
|
||||
for email_address in frappe.utils.split_emails(data):
|
||||
frappe.utils.validate_email_address(email_address, throw=True)
|
||||
|
||||
if data_field_options == "Name":
|
||||
frappe.utils.validate_name(data, throw=True)
|
||||
|
||||
if data_field_options == "Phone":
|
||||
frappe.utils.validate_phone_number(data, throw=True)
|
||||
|
||||
|
|
@ -678,7 +686,7 @@ class BaseDocument(object):
|
|||
# doesn't look like html so no need
|
||||
continue
|
||||
|
||||
elif "<!-- markdown -->" in value and not ("<script" in value or "javascript:" in value):
|
||||
elif "<!-- markdown -->" in value and not bool(BeautifulSoup(value, "html.parser").find()):
|
||||
# should be handled separately via the markdown converter function
|
||||
continue
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ def make_new_doc(doctype):
|
|||
doc = doc.get_valid_dict(sanitize=False)
|
||||
doc["doctype"] = doctype
|
||||
doc["__islocal"] = 1
|
||||
doc["__unsaved"] = 1
|
||||
|
||||
return doc
|
||||
|
||||
|
|
@ -74,11 +75,9 @@ def set_user_and_static_default_values(doc):
|
|||
def get_user_default_value(df, defaults, doctype_user_permissions, allowed_records, default_doc):
|
||||
# don't set defaults for "User" link field using User Permissions!
|
||||
if df.fieldtype == "Link" and df.options != "User":
|
||||
# 1 - look in user permissions only for document_type==Setup
|
||||
# We don't want to include permissions of transactions to be used for defaults.
|
||||
if (frappe.get_meta(df.options).document_type=="Setup"
|
||||
and not df.ignore_user_permissions and default_doc):
|
||||
return default_doc
|
||||
# If user permission has Is Default enabled or single-user permission has found against respective doctype.
|
||||
if (not df.ignore_user_permissions and default_doc):
|
||||
return default_doc
|
||||
|
||||
# 2 - Look in user defaults
|
||||
user_default = defaults.get(df.fieldname)
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ def check_permission_and_not_submitted(doc):
|
|||
|
||||
# check if submitted
|
||||
if doc.docstatus == 1:
|
||||
frappe.msgprint(_("{0} {1}: Submitted Record cannot be deleted.").format(_(doc.doctype), doc.name),
|
||||
frappe.msgprint(_("{0} {1}: Submitted Record cannot be deleted. You must {2} Cancel {3} it first.").format(_(doc.doctype), doc.name, "<a href='https://docs.erpnext.com//docs/user/manual/en/setting-up/articles/delete-submitted-document' target='_blank'>", "</a>"),
|
||||
raise_exception=True)
|
||||
|
||||
def check_if_doc_is_linked(doc, method="Delete"):
|
||||
|
|
|
|||
|
|
@ -268,6 +268,10 @@ class Document(BaseDocument):
|
|||
if hasattr(self, "__islocal"):
|
||||
delattr(self, "__islocal")
|
||||
|
||||
# clear unsaved flag
|
||||
if hasattr(self, "__unsaved"):
|
||||
delattr(self, "__unsaved")
|
||||
|
||||
if not (frappe.flags.in_migrate or frappe.local.flags.in_install or frappe.flags.in_setup_wizard):
|
||||
follow_document(self.doctype, self.name, frappe.session.user)
|
||||
return self
|
||||
|
|
@ -329,6 +333,10 @@ class Document(BaseDocument):
|
|||
self.update_children()
|
||||
self.run_post_save_methods()
|
||||
|
||||
# clear unsaved flag
|
||||
if hasattr(self, "__unsaved"):
|
||||
delattr(self, "__unsaved")
|
||||
|
||||
return self
|
||||
|
||||
def copy_attachments_from_amended_from(self):
|
||||
|
|
@ -583,6 +591,9 @@ class Document(BaseDocument):
|
|||
if high_permlevel_fields:
|
||||
self.reset_values_if_no_permlevel_access(has_access_to, high_permlevel_fields)
|
||||
|
||||
# If new record then don't reset the values for child table
|
||||
if self.is_new(): return
|
||||
|
||||
# check for child tables
|
||||
for df in self.meta.get_table_fields():
|
||||
high_permlevel_fields = frappe.get_meta(df.options).get_high_permlevel_fields()
|
||||
|
|
@ -1318,6 +1329,9 @@ def make_event_update_log(doc, update_type):
|
|||
|
||||
def check_doctype_has_consumers(doctype):
|
||||
"""Check if doctype has event consumers for event streaming"""
|
||||
if not frappe.db.exists("DocType", "Event Consumer"):
|
||||
return False
|
||||
|
||||
event_consumers = frappe.get_all('Event Consumer')
|
||||
for event_consumer in event_consumers:
|
||||
consumer = frappe.get_doc('Event Consumer', event_consumer.name)
|
||||
|
|
|
|||
|
|
@ -425,17 +425,19 @@ class Meta(Document):
|
|||
implemented in other Frappe applications via hooks.
|
||||
'''
|
||||
data = frappe._dict()
|
||||
try:
|
||||
module = load_doctype_module(self.name, suffix='_dashboard')
|
||||
if hasattr(module, 'get_data'):
|
||||
data = frappe._dict(module.get_data())
|
||||
except ImportError:
|
||||
pass
|
||||
if not self.custom:
|
||||
try:
|
||||
module = load_doctype_module(self.name, suffix='_dashboard')
|
||||
if hasattr(module, 'get_data'):
|
||||
data = frappe._dict(module.get_data())
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
self.add_doctype_links(data)
|
||||
|
||||
for hook in frappe.get_hooks("override_doctype_dashboards", {}).get(self.name, []):
|
||||
data = frappe.get_attr(hook)(data=data)
|
||||
if not self.custom:
|
||||
for hook in frappe.get_hooks("override_doctype_dashboards", {}).get(self.name, []):
|
||||
data = frappe.get_attr(hook)(data=data)
|
||||
|
||||
return data
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe
|
|||
("custom", "custom_field"),
|
||||
("custom", "property_setter"),
|
||||
("website", "web_form"),
|
||||
("website", "web_template"),
|
||||
("website", "web_form_field"),
|
||||
("website", "portal_menu_item"),
|
||||
("data_migration", "data_migration_mapping_detail"),
|
||||
|
|
@ -78,7 +79,7 @@ def get_doc_files(files, start_path, force=0, sync_everything = False, verbose=F
|
|||
|
||||
# load in sequence - warning for devs
|
||||
document_types = ['doctype', 'page', 'report', 'dashboard_chart_source', 'print_format',
|
||||
'website_theme', 'web_form', 'notification', 'print_style',
|
||||
'website_theme', 'web_form', 'web_template', 'notification', 'print_style',
|
||||
'data_migration_mapping', 'data_migration_plan', 'onboarding_slide', 'desk_page']
|
||||
for doctype in document_types:
|
||||
doctype_path = os.path.join(start_path, doctype)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ def start(transaction_type="request", method=None, kwargs=None):
|
|||
|
||||
|
||||
def stop(response=None):
|
||||
if frappe.conf.monitor and hasattr(frappe.local, "monitor"):
|
||||
if hasattr(frappe.local, "monitor"):
|
||||
frappe.local.monitor.dump(response)
|
||||
|
||||
|
||||
|
|
@ -79,7 +79,7 @@ class Monitor:
|
|||
|
||||
if self.data.transaction_type == "request":
|
||||
self.data.request.status_code = response.status_code
|
||||
self.data.request.response_length = int(response.headers["Content-Length"])
|
||||
self.data.request.response_length = int(response.headers.get("Content-Length", 0))
|
||||
|
||||
self.store()
|
||||
except Exception:
|
||||
|
|
|
|||
|
|
@ -272,3 +272,6 @@ execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Account')
|
|||
execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Settings')
|
||||
frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats
|
||||
execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders()
|
||||
frappe.patches.v13_0.website_theme_custom_scss
|
||||
frappe.patches.v13_0.set_existing_dashboard_charts_as_public
|
||||
frappe.patches.v13_0.set_path_for_homepage_in_web_page_view
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("contacts", "doctype", "contact_email")
|
||||
frappe.reload_doc("contacts", "doctype", "contact_phone")
|
||||
frappe.reload_doc("contacts", "doctype", "contact")
|
||||
|
||||
contact_details = frappe.db.sql("""
|
||||
SELECT
|
||||
`name`, `email_id`, `phone`, `mobile_no`, `modified_by`, `creation`, `modified`
|
||||
|
|
@ -10,10 +14,6 @@ def execute():
|
|||
and `tabContact Email`.email_id=`tabContact`.email_id)
|
||||
""", as_dict=True)
|
||||
|
||||
frappe.reload_doc("contacts", "doctype", "contact_email")
|
||||
frappe.reload_doc("contacts", "doctype", "contact_phone")
|
||||
frappe.reload_doc("contacts", "doctype", "contact")
|
||||
|
||||
email_values = []
|
||||
phone_values = []
|
||||
for count, contact_detail in enumerate(contact_details):
|
||||
|
|
|
|||
0
frappe/patches/v13_0/__init__.py
Normal file
0
frappe/patches/v13_0/__init__.py
Normal file
6
frappe/patches/v13_0/remove_web_view.py
Normal file
6
frappe/patches/v13_0/remove_web_view.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.delete_doc_if_exists("DocType", "Web View")
|
||||
frappe.delete_doc_if_exists("DocType", "Web View Component")
|
||||
frappe.delete_doc_if_exists("DocType", "CSS Class")
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc('desk', 'doctype', 'dashboard_chart')
|
||||
|
||||
if not frappe.db.table_exists('Dashboard Chart'):
|
||||
return
|
||||
|
||||
users_with_permission = frappe.get_all(
|
||||
"Has Role",
|
||||
fields=["parent"],
|
||||
filters={"role": ['in', ['System Manager', 'Dashboard Manager']], "parenttype": "User"},
|
||||
distinct=True,
|
||||
)
|
||||
|
||||
users = [item.parent for item in users_with_permission]
|
||||
charts = frappe.db.get_all('Dashboard Chart', filters={'owner': ['in', users]})
|
||||
|
||||
for chart in charts:
|
||||
frappe.db.set_value('Dashboard Chart', chart.name, 'is_public', 1)
|
||||
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc('website', 'doctype', 'web_page_view', force=True)
|
||||
frappe.db.sql("""UPDATE `tabWeb Page View` set path="/" where path=''""")
|
||||
10
frappe/patches/v13_0/website_theme_custom_scss.py
Normal file
10
frappe/patches/v13_0/website_theme_custom_scss.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.reload_doctype('Website Theme')
|
||||
for theme in frappe.get_all('Website Theme'):
|
||||
doc = frappe.get_doc('Website Theme', theme.name)
|
||||
if not doc.get('custom_scss') and doc.theme_scss:
|
||||
# move old theme to new theme
|
||||
doc.custom_scss = doc.theme_scss
|
||||
doc.save()
|
||||
|
|
@ -441,18 +441,16 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
});
|
||||
},
|
||||
setup_field_settings: function() {
|
||||
|
||||
this.page.main.find(".field-settings").on("click", () => {
|
||||
var field = $(this).parent();
|
||||
|
||||
this.page.main.find(".field-settings").on("click", e => {
|
||||
const field = $(e.currentTarget).parent();
|
||||
// new dialog
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: "Set Properties",
|
||||
fields: [
|
||||
{
|
||||
label:__("Label"),
|
||||
fieldname:"label",
|
||||
fieldtype:"Data"
|
||||
label: __("Label"),
|
||||
fieldname: "label",
|
||||
fieldtype: "Data"
|
||||
},
|
||||
{
|
||||
label: __("Align Value"),
|
||||
|
|
@ -485,7 +483,7 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
});
|
||||
|
||||
// set current value
|
||||
if(field.attr('data-align')) {
|
||||
if (field.attr('data-align')) {
|
||||
d.set_value('align', field.attr('data-align'));
|
||||
} else {
|
||||
d.set_value('align', 'left');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
{
|
||||
"css/tailwind.css": [
|
||||
"public/tailwind.css"
|
||||
],
|
||||
"css/frappe-web-b4.css": [
|
||||
"public/scss/website.scss",
|
||||
"public/less/indicator.less"
|
||||
|
|
@ -90,6 +93,7 @@
|
|||
"public/css/font-awesome.css",
|
||||
"public/css/octicons/octicons.css",
|
||||
"public/less/desk.less",
|
||||
"public/less/module.less",
|
||||
"public/less/flex.less",
|
||||
"public/less/indicator.less",
|
||||
"public/less/avatar.less",
|
||||
|
|
@ -103,6 +107,7 @@
|
|||
"public/less/form.less",
|
||||
"public/less/mobile.less",
|
||||
"public/less/kanban.less",
|
||||
"public/less/dashboard_view.less",
|
||||
"public/less/controls.less",
|
||||
"public/less/chat.less",
|
||||
"public/less/filters.less",
|
||||
|
|
@ -295,6 +300,7 @@
|
|||
|
||||
"public/js/frappe/views/gantt/gantt_view.js",
|
||||
"public/js/frappe/views/calendar/calendar.js",
|
||||
"public/js/frappe/views/dashboard/dashboard_view.js",
|
||||
"public/js/frappe/views/image/image_view.js",
|
||||
"public/js/frappe/views/kanban/kanban_view.js",
|
||||
"public/js/frappe/views/inbox/inbox_view.js",
|
||||
|
|
|
|||
|
|
@ -1,64 +1,82 @@
|
|||
/* csslint ignore:start */
|
||||
|
||||
/* palette colors*/
|
||||
|
||||
body {
|
||||
line-height: 1.5;
|
||||
color: #36414c;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1em 0 !important;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-top: 1px solid #d1d8dd;
|
||||
}
|
||||
|
||||
.body-table {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
.body-table td {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
.email-header,
|
||||
.email-body,
|
||||
.email-footer {
|
||||
width: 100% !important;
|
||||
min-width: 100% !important;
|
||||
}
|
||||
|
||||
.email-body {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
border-top: 1px solid #d1d8dd;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.email-header {
|
||||
border: 1px solid #d1d8dd;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.email-header .brand-image {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.email-header-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.body-table.has-header .email-body {
|
||||
border: 1px solid #d1d8dd;
|
||||
border-radius: 0 0 4px 4px;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.body-table.has-header .email-footer {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.email-footer-container {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.email-footer-container > div:not(:last-child) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.email-unsubscribe a {
|
||||
color: #8d99a6;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.btn {
|
||||
text-decoration: none;
|
||||
padding: 7px 10px;
|
||||
|
|
@ -66,20 +84,24 @@ hr {
|
|||
border: 1px solid;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.btn.btn-default {
|
||||
color: #fff;
|
||||
background-color: #f0f4f7;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.btn.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #5e64ff;
|
||||
border-color: #444bff;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table td,
|
||||
.table th {
|
||||
padding: 8px;
|
||||
|
|
@ -88,53 +110,68 @@ hr {
|
|||
border-top: 1px solid #d1d8dd;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.table > thead > tr > th {
|
||||
vertical-align: middle;
|
||||
border-bottom: 2px solid #d1d8dd;
|
||||
}
|
||||
|
||||
.table > thead:first-child > tr:first-child > th {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.table.table-bordered {
|
||||
border: 1px solid #d1d8dd;
|
||||
}
|
||||
|
||||
.table.table-bordered td,
|
||||
.table.table-bordered th {
|
||||
border: 1px solid #d1d8dd;
|
||||
}
|
||||
|
||||
.more-info {
|
||||
font-size: 80% !important;
|
||||
color: #8d99a6 !important;
|
||||
border-top: 1px solid #ebeff2;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #8d99a6 !important;
|
||||
}
|
||||
|
||||
.text-extra-muted {
|
||||
color: #d1d8dd !important;
|
||||
}
|
||||
|
||||
.text-regular {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.text-medium {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.text-small {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.text-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
|
|
@ -143,33 +180,43 @@ hr {
|
|||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.indicator.indicator-blue {
|
||||
background-color: #5e64ff;
|
||||
}
|
||||
|
||||
.indicator.indicator-green {
|
||||
background-color: #98d85b;
|
||||
}
|
||||
|
||||
.indicator.indicator-orange {
|
||||
background-color: #ffa00a;
|
||||
}
|
||||
|
||||
.indicator.indicator-red {
|
||||
background-color: #ff5858;
|
||||
}
|
||||
|
||||
.indicator.indicator-yellow {
|
||||
background-color: #feef72;
|
||||
}
|
||||
|
||||
.screenshot {
|
||||
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #d1d8dd;
|
||||
margin: 8px 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.list-unstyled {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* auto email report */
|
||||
|
||||
.report-title {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* csslint ignore:end */
|
||||
|
|
|
|||
|
|
@ -2523,7 +2523,7 @@ class extends Component {
|
|||
h("div",{class:"input-group input-group-lg"},
|
||||
!frappe._.is_empty(props.actions) ?
|
||||
h("div",{class:"input-group-btn dropup"},
|
||||
h(frappe.components.Button,{ class: "dropdown-toggle", "data-toggle": "dropdown"},
|
||||
h(frappe.components.Button,{ class: (frappe.session.user === "Guest" ? "disabled" : "dropdown-toggle"), "data-toggle": "dropdown"},
|
||||
h(frappe.components.FontAwesome, { class: "text-muted", type: "paperclip", fixed: true })
|
||||
),
|
||||
h("div",{ class:"dropdown-menu dropdown-menu-left", onclick: e => e.stopPropagation() },
|
||||
|
|
|
|||
|
|
@ -86,6 +86,14 @@ frappe.Application = Class.extend({
|
|||
this.show_update_available();
|
||||
}
|
||||
|
||||
if (!frappe.boot.developer_mode) {
|
||||
let console_security_message = __("Using this console may allow attackers to impersonate you and steal your information. Do not enter or paste code that you do not understand.");
|
||||
console.log(
|
||||
`%c${console_security_message}`,
|
||||
"font-size: large"
|
||||
);
|
||||
}
|
||||
|
||||
this.show_notes();
|
||||
|
||||
if (frappe.boot.is_first_startup) {
|
||||
|
|
|
|||
|
|
@ -85,6 +85,10 @@ frappe.dom = {
|
|||
);
|
||||
},
|
||||
|
||||
is_element_in_modal(element) {
|
||||
return Boolean($(element).parents('.modal').length);
|
||||
},
|
||||
|
||||
set_style: function(txt, id) {
|
||||
if(!txt) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ frappe.ui.form.ControlBarcode = frappe.ui.form.ControlData.extend({
|
|||
const svg = this.barcode_area.find('svg')[0];
|
||||
JsBarcode(svg, value, this.get_options(value));
|
||||
$(svg).attr('data-barcode-value', value);
|
||||
$(svg).attr('width', '100%');
|
||||
return this.barcode_area.html();
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -152,12 +152,14 @@ frappe.ui.form.Control = Class.extend({
|
|||
() => me.set_model_value(value),
|
||||
() => {
|
||||
me.set_mandatory && me.set_mandatory(value);
|
||||
me.set_invalid && me.set_invalid();
|
||||
|
||||
if(me.df.change || me.df.onchange) {
|
||||
// onchange event specified in df
|
||||
return (me.df.change || me.df.onchange).apply(me, [e]);
|
||||
let set = (me.df.change || me.df.onchange).apply(me, [e]);
|
||||
me.set_invalid && me.set_invalid();
|
||||
return set;
|
||||
}
|
||||
me.set_invalid && me.set_invalid();
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -180,7 +180,14 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
|
|||
this.$wrapper.toggleClass("has-error", (this.df.reqd && is_null(value)) ? true : false);
|
||||
},
|
||||
set_invalid: function () {
|
||||
this.$wrapper.toggleClass("has-error", (this.df.invalid ? true : false));
|
||||
let invalid = !!this.df.invalid;
|
||||
if (this.grid) {
|
||||
this.$wrapper.parents('.grid-static-col').toggleClass('invalid', invalid);
|
||||
this.$input.toggleClass('invalid', invalid);
|
||||
this.grid_row.columns[this.df.fieldname].is_invalid = invalid;
|
||||
} else {
|
||||
this.$wrapper.toggleClass('has-error', invalid);
|
||||
}
|
||||
},
|
||||
set_bold: function() {
|
||||
if(this.$input) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
|
|||
this.ace_editor_target = $('<div class="ace-editor-target"></div>')
|
||||
.appendTo(this.input_area);
|
||||
|
||||
this.expanded = false;
|
||||
this.$expand_button = $(`<button class="btn btn-xs btn-default">${__('Expand')}</button>`).click(() => {
|
||||
this.expanded = !this.expanded;
|
||||
this.refresh_height();
|
||||
this.toggle_label();
|
||||
}).appendTo(this.$input_wrapper);
|
||||
// styling
|
||||
this.ace_editor_target.addClass('border rounded');
|
||||
this.ace_editor_target.css('height', 300);
|
||||
|
|
@ -26,6 +32,16 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
|
|||
}, 300));
|
||||
},
|
||||
|
||||
refresh_height() {
|
||||
this.ace_editor_target.css('height', this.expanded ? 600 : 300);
|
||||
this.editor.resize();
|
||||
},
|
||||
|
||||
toggle_label() {
|
||||
const button_label = this.expanded ? __('Collapse') : __('Expand');
|
||||
this.$expand_button.text(button_label);
|
||||
},
|
||||
|
||||
set_language() {
|
||||
const language_map = {
|
||||
'Javascript': 'ace/mode/javascript',
|
||||
|
|
@ -34,7 +50,9 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
|
|||
'CSS': 'ace/mode/css',
|
||||
'Markdown': 'ace/mode/markdown',
|
||||
'SCSS': 'ace/mode/scss',
|
||||
'JSON': 'ace/mode/json'
|
||||
'JSON': 'ace/mode/json',
|
||||
'Golang': 'ace/mode/golang',
|
||||
'Go': 'ace/mode/golang'
|
||||
};
|
||||
const language = this.df.options;
|
||||
|
||||
|
|
|
|||
|
|
@ -96,6 +96,9 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({
|
|||
if(this.df.options == 'Phone') {
|
||||
this.df.invalid = !validate_phone(v);
|
||||
return v;
|
||||
} else if (this.df.options == 'Name') {
|
||||
this.df.invalid = !validate_name(v);
|
||||
return v;
|
||||
} else if(this.df.options == 'Email') {
|
||||
var email_list = frappe.utils.split_emails(v);
|
||||
if (!email_list) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({
|
|||
this.$list_wrapper = $(template);
|
||||
this.$input = $('<input>');
|
||||
this.input = this.$input.get(0);
|
||||
this.has_input = true;
|
||||
this.$list_wrapper.prependTo(this.input_area);
|
||||
this.$filter_input = this.$list_wrapper.find('input');
|
||||
this.$list_wrapper.on('click', '.dropdown-menu', e => {
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({
|
|||
[{ 'color': [] }, { 'background': [] }],
|
||||
['blockquote', 'code-block'],
|
||||
['link', 'image'],
|
||||
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
|
||||
[{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'list': 'check' }],
|
||||
[{ 'align': [] }],
|
||||
[{ 'indent': '-1'}, { 'indent': '+1' }],
|
||||
[{'table': [
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue