Merge branch 'develop' into file-api

This commit is contained in:
Chinmay Pai 2018-09-21 13:42:32 +05:30 committed by GitHub
commit 6e84bbe80d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
272 changed files with 39962 additions and 33149 deletions

View file

@ -1,44 +1,41 @@
language: python
dist: trusty
sudo: required
python:
- "2.7"
- "3.6"
- 2.7
- 3.6
env:
- DB=mariadb
- DB=postgres
services:
- mysql
addons:
postgresql: "9.5"
hosts:
- test_site
- test_site_postgres
matrix:
allow_failures:
- python: 2.7
env: DB=postgres
fast_finish: true
install:
- sudo rm /etc/apt/sources.list.d/mongodb*.list
- sudo rm /etc/apt/sources.list.d/docker.list
- sudo apt-get install hhvm && rm -rf /home/travis/.kiex/
- sudo apt-get purge -y mysql-common mysql-server mysql-client
- nvm install v8.10.0
- wget https://raw.githubusercontent.com/frappe/bench/master/playbooks/install.py
- sudo python install.py --develop --user travis --without-bench-setup
- sudo pip install -e ~/bench
- rm $TRAVIS_BUILD_DIR/.git/shallow
- cd ~/ && bench init frappe-bench --python $(which python) --frappe-path $TRAVIS_BUILD_DIR
- cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/
- $TRAVIS_BUILD_DIR/.travis/install.sh
before_script:
- mysql -u root -ptravis -e 'create database test_frappe'
- echo "USE mysql;\nCREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe';\nFLUSH PRIVILEGES;\n" | mysql -u root -ptravis
- echo "USE mysql;\nGRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost';\n" | mysql -u root -ptravis
- cd ~/frappe-bench
- bench use test_site
- bench reinstall --yes
- bench setup-help
- bench setup-global-help --mariadb_root_password travis
- bench scheduler disable
- sed -i 's/9000/9001/g' sites/common_site_config.json
- bench start &
- sleep 10
script:
- bench run-tests
- $TRAVIS_BUILD_DIR/.travis/run-tests.sh
after_script:
- coveralls -b apps/frappe -d ../../sites/.coverage

22
.travis/install.sh Executable file
View file

@ -0,0 +1,22 @@
#!/bin/bash
set -e
sudo rm /etc/apt/sources.list.d/mongodb*.list
sudo rm /etc/apt/sources.list.d/docker.list
sudo apt-get install hhvm && rm -rf /home/travis/.kiex/
sudo apt-get purge -y mysql-common mysql-server mysql-client
source ~/.nvm/nvm.sh
nvm install v8.10.0
pip install python-coveralls
wget https://raw.githubusercontent.com/frappe/bench/master/playbooks/install.py
sudo python install.py --develop --user travis --without-bench-setup
sudo pip install -e ~/bench
rm $TRAVIS_BUILD_DIR/.git/shallow
cd ~/ && bench init frappe-bench --python $(which python) --frappe-path $TRAVIS_BUILD_DIR
cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/
cp -r $TRAVIS_BUILD_DIR/test_sites/test_site_postgres ~/frappe-bench/sites/

22
.travis/run-tests.sh Executable file
View file

@ -0,0 +1,22 @@
#!/bin/bash
set -e
if [[ $DB == 'mariadb' ]]; then
mysql -u root -ptravis -e 'create database test_frappe'
mysql -u root -ptravis -e "USE mysql; CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'; FLUSH PRIVILEGES; "
mysql -u root -ptravis -e "USE mysql; GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost';"
bench --site test_site reinstall --yes
bench --site test_site setup-help
bench setup-global-help --root_password travis
bench --site test_site scheduler disable
bench --site test_site run-tests --coverage
elif [[ $DB == 'postgres' ]]; then
psql -c "CREATE DATABASE test_frappe;" -U postgres
psql -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe';" -U postgres
bench --site test_site_postgres reinstall --yes
bench --site test_site_postgres setup-help
bench setup-global-help --db_type=postgres --root_password travis
bench --site test_site_postgres scheduler disable
bench --site test_site_postgres run-tests --coverage
fi

View file

@ -20,8 +20,16 @@
<a href='https://frappe.io/docs'>
<img src='https://img.shields.io/badge/docs-📖-7575FF.svg?style=flat-square'/>
</a>
<a href='https://www.codetriage.com/frappe/frappe'>
<img src='https://www.codetriage.com/frappe/frappe/badges/users.svg'>
</a>
<a href='https://coveralls.io/github/frappe/frappe?branch=develop'>
<img src='https://coveralls.io/repos/github/frappe/frappe/badge.svg?branch=develop'>
</a>
</div>
Full-stack web application framework that uses Python and MariaDB on the server side and a tightly integrated client side library. Built for [ERPNext](https://erpnext.com)
### Table of Contents

1
bandit.yml Normal file
View file

@ -0,0 +1 @@
skips: ['B605', 'B404', 'B603', 'B607']

View file

@ -17,7 +17,7 @@ from faker import Faker
from .exceptions import *
from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader)
__version__ = '10.1.46'
__version__ = '10.1.48'
__title__ = "Frappe Framework"
local = Local()
@ -168,17 +168,17 @@ def connect(site=None, db_name=None):
:param site: If site is given, calls `frappe.init`.
:param db_name: Optional. Will use from `site_config.json`."""
from frappe.database import Database
from frappe.database import get_db
if site:
init(site)
local.db = Database(user=db_name or local.conf.db_name)
local.db = get_db(user=db_name or local.conf.db_name)
set_user("Administrator")
def connect_read_only():
from frappe.database import Database
from frappe.database import get_db
local.read_only_db = Database(local.conf.slave_host, local.conf.slave_db_name,
local.read_only_db = get_db(local.conf.slave_host, local.conf.slave_db_name,
local.conf.slave_db_password)
# swap db connections
@ -259,7 +259,7 @@ def errprint(msg):
:param msg: Message."""
msg = as_unicode(msg)
if not request or (not "cmd" in local.form_dict) or conf.developer_mode:
print(msg.encode('utf-8'))
print(msg)
error_log.append(msg)
@ -589,7 +589,7 @@ def has_website_permission(doc=None, ptype='read', user=None, verbose=False, doc
# check permission in controller
if hasattr(doc, 'has_website_permission'):
return doc.has_website_permission(doc, ptype, user, verbose=verbose)
return doc.has_website_permission(ptype, user, verbose=verbose)
hooks = (get_hooks("has_website_permission") or {}).get(doctype, [])
if hooks:

View file

@ -25,12 +25,6 @@ from frappe.utils.error import make_error_snapshot
from frappe.core.doctype.communication.comment import update_comments_in_parent_after_request
from frappe import _
# imports - third-party imports
import pymysql
from pymysql.constants import ER
# imports - module imports
local_manager = LocalManager([frappe.local])
_site = None
@ -148,8 +142,8 @@ def handle_exception(e):
response = frappe.utils.response.report_error(http_status_code)
elif (http_status_code==500
and isinstance(e, pymysql.InternalError)
and e.args[0] in (ER.LOCK_WAIT_TIMEOUT, ER.LOCK_DEADLOCK)):
and (frappe.db and isinstance(e, frappe.db.InternalError))
and (frappe.db and (frappe.db.is_deadlocked(e) or frappe.db.is_timedout(e)))):
http_status_code = 508
elif http_status_code==401:

View file

@ -91,7 +91,7 @@ class HTTPRequest:
def connect(self, ac_name = None):
"""connect to db, from ac_name or db_name"""
frappe.local.db = frappe.database.Database(user = self.get_db_name(), \
frappe.local.db = frappe.database.get_db(user = self.get_db_name(), \
password = getattr(conf, 'db_password', ''))
class LoginManager:

View file

@ -128,19 +128,19 @@ def get_user_pages_or_reports(parent):
standard_roles = frappe.db.sql("""
select distinct
tab{parent}.name,
tab{parent}.modified,
`tab{parent}`.name as name,
`tab{parent}`.modified,
{column}
from `tabHas Role`, `tab{parent}`
where
`tabHas Role`.role in ({roles})
and `tabHas Role`.parent = `tab{parent}`.name
and tab{parent}.name not in (
and `tab{parent}`.`name` not in (
select `tabCustom Role`.{field} from `tabCustom Role`
where `tabCustom Role`.{field} is not null)
{condition}
""".format(parent=parent, column=column, roles = ', '.join(['%s']*len(roles)),
field=parent.lower(), condition="and tabReport.disabled=0" if parent == "Report" else ""),
field=parent.lower(), condition="and `tabReport`.disabled=0" if parent == "Report" else ""),
roles, as_dict=True)
for p in standard_roles:
@ -157,7 +157,7 @@ def get_user_pages_or_reports(parent):
from `tab{parent}`
where
(select count(*) from `tabHas Role`
where `tabHas Role`.parent=tab{parent}.name) = 0
where `tabHas Role`.parent=`tab{parent}`.`name`) = 0
""".format(parent=parent, column=column), as_dict=1)
for p in pages_with_no_roles:
@ -173,7 +173,7 @@ def get_user_pages_or_reports(parent):
def get_column(doctype):
column = "`tabPage`.title as title"
if doctype == "Report":
column = "`tabReport`.name as name, `tabReport`.name as title, `tabReport`.ref_doctype, `tabReport`.report_type"
column = "`tabReport`.`name` as title, `tabReport`.ref_doctype, `tabReport`.report_type"
return column
@ -193,9 +193,9 @@ def load_translations(bootinfo):
def get_fullnames():
"""map of user fullnames"""
ret = frappe.db.sql("""select name, full_name as fullname,
user_image as image, gender, email, username
from tabUser where enabled=1 and user_type!="Website User" """, as_dict=1)
ret = frappe.db.sql("""select `name`, full_name as fullname,
user_image as image, gender, email, username
from tabUser where enabled=1 and user_type!='Website User'""", as_dict=1)
d = {}
for r in ret:
@ -245,10 +245,10 @@ def load_print_css(bootinfo, print_settings):
bootinfo.print_css = frappe.www.printview.get_print_style(print_settings.print_style or "Modern", for_legacy=True)
def get_unseen_notes():
return frappe.db.sql('''select name, title, content, notify_on_every_login from tabNote where notify_on_login=1
return frappe.db.sql('''select `name`, title, content, notify_on_every_login from `tabNote` where notify_on_login=1
and expire_notification_on > %s and %s not in
(select user from `tabNote Seen By` nsb
where nsb.parent=tabNote.name)''', (frappe.utils.now(), frappe.session.user), as_dict=True)
where nsb.parent=`tabNote`.name)''', (frappe.utils.now(), frappe.session.user), as_dict=True)
def get_gsuite_status():
return (frappe.get_value('Gsuite Settings', None, 'enable') == '1')

View file

@ -67,9 +67,9 @@ def clear_doctype_cache(doctype=None):
clear_single(doctype)
# clear all parent doctypes
for dt in frappe.db.sql("""select parent from tabDocField
where fieldtype="Table" and options=%s""", (doctype,)):
clear_single(dt[0])
for dt in frappe.db.get_all('DocField', 'parent', dict(fieldtype='Table', options=doctype)):
clear_single(dt.parent)
# clear all notifications
delete_notification_count_for(doctype)

View file

@ -79,12 +79,12 @@ def create(user, exists_ok = False, fields = None):
result = frappe.db.sql("""
SELECT *
FROM `tabChat Profile`
WHERE user = "{user}"
WHERE `user` = '{user}'
""".format(user = user))
if result:
if not exists_ok:
frappe.throw(_('Chat Profile for User {user} exists.'.format(user = user)))
frappe.throw(_('Chat Profile for User {0} exists.').format(user))
else:
dprof = frappe.new_doc('Chat Profile')
dprof.user = user

View file

@ -33,7 +33,7 @@ def is_direct(owner, other, bidirectional = False):
exists = len(get_room(owner, other)) == 1
if bidirectional:
exists = exists or len(get_room(other, owner)) == 1
return exists
def get_chat_room_user_set(users, filter_ = None):
@ -56,19 +56,17 @@ class ChatRoom(Document):
if self.type == "Direct":
if len(self.users) != 1:
frappe.throw(_('{type} room must have atmost one user.'.format(type = self.type)))
frappe.throw(_('{0} room must have atmost one user.').format(self.type))
other = squashify(self.users)
if self.is_new():
if is_direct(self.owner, other.user, bidirectional = True):
frappe.throw(_('Direct room with {other} already exists.'.format(
other = other.user
)))
frappe.throw(_('Direct room with {0} already exists.').format(other.user))
if self.type == "Group" and not self.room_name:
frappe.throw(_('Group name cannot be empty.'))
def before_save(self):
if not self.is_new():
self.get_doc_before_save()
@ -83,12 +81,12 @@ class ChatRoom(Document):
update = { }
for changed in diff.changed:
field, old, new = changed
if field == 'last_message':
new = chat_message.get(new)
update.update({ field: new })
if diff.added or diff.removed:
update.update(dict(users = [u.user for u in self.users]))
@ -121,7 +119,7 @@ def get(user, rooms = None, fields = None, filters = None):
default = ['name', 'type', 'room_name', 'creation', 'owner', 'avatar']
handle = ['users', 'last_message']
param = [f for f in fields if f not in handle]
rooms = frappe.get_all('Chat Room',
@ -151,7 +149,7 @@ def get(user, rooms = None, fields = None, filters = None):
rooms[i]['last_message'] = None
rooms = squashify(dictify(rooms))
return rooms
@frappe.whitelist(allow_guest = True)
@ -177,7 +175,7 @@ def create(kind, owner, users = None, name = None):
room.type = kind
room.owner = owner
room.room_name = name
dusers = [ ]
if kind != 'Visitor':
@ -198,7 +196,7 @@ def create(kind, owner, users = None, name = None):
for user in dsettings.chat_operators:
if user.user not in users:
room.append('users', user)
room.save(ignore_permissions = True)
room = get(owner, rooms = room.name)
@ -218,5 +216,5 @@ def history(room, user, fields = None, limit = 10, start = None, end = None):
mess = chat_message.history(room, limit = limit, start = start, end = end)
mess = squashify(mess)
return dictify(mess)

View file

@ -97,7 +97,7 @@ def get_value(doctype, fieldname, filters=None, as_dict=True, debug=False, paren
@frappe.whitelist()
def get_single_value(doctype, field):
if not frappe.has_permission(doctype):
frappe.throw(_("No permission for {doctype}".format(doctype = doctype)), frappe.PermissionError)
frappe.throw(_("No permission for {0}").format(doctype), frappe.PermissionError)
value = frappe.db.get_single_value(doctype, field)
return value

View file

@ -10,15 +10,10 @@ from frappe.installer import update_site_config
from frappe.utils import touch_file, get_site_path
from six import text_type
# imports - third-party imports
from pymysql.constants import ER
# imports - module imports
from frappe.exceptions import SQLError
@click.command('new-site')
@click.argument('site')
@click.option('--db-name', help='Database name')
@click.option('--db-type', default='mariadb', type=click.Choice(['mariadb', 'postgres']), help='Optional "postgres" or "mariadb". Default is "mariadb"')
@click.option('--mariadb-root-username', default='root', help='Root username for MariaDB')
@click.option('--mariadb-root-password', help='Root password for MariaDB')
@click.option('--admin-password', help='Administrator password for new site', default=None)
@ -26,22 +21,27 @@ from frappe.exceptions import SQLError
@click.option('--force', help='Force restore if site/database already exists', is_flag=True, default=False)
@click.option('--source_sql', help='Initiate database with a SQL file')
@click.option('--install-app', multiple=True, help='Install app after installation')
def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin_password=None, verbose=False, install_apps=None, source_sql=None, force=None, install_app=None, db_name=None):
def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin_password=None,
verbose=False, install_apps=None, source_sql=None, force=None, install_app=None,
db_name=None, db_type=None):
"Create a new site"
frappe.init(site=site, new_site=True)
_new_site(db_name, site, mariadb_root_username=mariadb_root_username, mariadb_root_password=mariadb_root_password, admin_password=admin_password,
verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force)
_new_site(db_name, site, mariadb_root_username=mariadb_root_username,
mariadb_root_password=mariadb_root_password, admin_password=admin_password,
verbose=verbose, install_apps=install_app, source_sql=source_sql, force=force,
db_type = db_type)
if len(frappe.utils.get_sites()) == 1:
use(site)
def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=None, admin_password=None,
verbose=False, install_apps=None, source_sql=None,force=False, reinstall=False):
def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=None,
admin_password=None, verbose=False, install_apps=None, source_sql=None,force=False,
reinstall=False, db_type=None):
"""Install a new Frappe site"""
if not db_name:
db_name = hashlib.sha1(site.encode()).hexdigest()[:16]
db_name = '_' + hashlib.sha1(site.encode()).hexdigest()[:16]
from frappe.installer import install_db, make_site_dirs
from frappe.installer import install_app as _install_app
@ -61,8 +61,9 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
try:
installing = touch_file(get_site_path('locks', 'installing.lock'))
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)
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_type=db_type)
apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
for app in apps_to_install:
@ -347,8 +348,7 @@ def drop_site(site, root_login='root', root_password=None, archived_sites_path=N
def _drop_site(site, root_login='root', root_password=None, archived_sites_path=None, force=False):
"Remove site from database and filesystem"
from frappe.installer import get_root_connection
from frappe.model.db_schema import DbManager
from frappe.database import drop_user_and_database
from frappe.utils.backups import scheduled_backup
frappe.init(site=site)
@ -356,25 +356,20 @@ def _drop_site(site, root_login='root', root_password=None, archived_sites_path=
try:
scheduled_backup(ignore_files=False, force=True)
except SQLError as err:
if err[0] == ER.NO_SUCH_TABLE:
if force:
pass
else:
click.echo("="*80)
click.echo("Error: The operation has stopped because backup of {s}'s database failed.".format(s=site))
click.echo("Reason: {reason}{sep}".format(reason=err[1], sep="\n"))
click.echo("Fix the issue and try again.")
click.echo(
"Hint: Use 'bench drop-site {s} --force' to force the removal of {s}".format(sep="\n", tab="\t", s=site)
)
sys.exit(1)
except Exception as err:
if force:
pass
else:
click.echo("="*80)
click.echo("Error: The operation has stopped because backup of {s}'s database failed.".format(s=site))
click.echo("Reason: {reason}{sep}".format(reason=err[1], sep="\n"))
click.echo("Fix the issue and try again.")
click.echo(
"Hint: Use 'bench drop-site {s} --force' to force the removal of {s}".format(sep="\n", tab="\t", s=site)
)
sys.exit(1)
db_name = frappe.local.conf.db_name
frappe.local.db = get_root_connection(root_login, root_password)
dbman = DbManager(frappe.local.db)
dbman.delete_user(db_name)
dbman.drop_database(db_name)
drop_user_and_database(frappe.conf.db_name, root_login, root_password)
if not archived_sites_path:
archived_sites_path = os.path.join(frappe.get_app_path('frappe'), '..', '..', '..', 'archived_sites')

View file

@ -6,8 +6,9 @@ import json, os, sys, subprocess
from distutils.spawn import find_executable
import frappe
from frappe.commands import pass_context, get_site
from frappe.utils import update_progress_bar
from frappe.utils import update_progress_bar, get_bench_path
from frappe.utils.response import json_handler
from coverage import Coverage
@click.command('build')
@click.option('--app', help='Build assets for app')
@ -315,6 +316,18 @@ def mariadb(context):
'--pager=less -SFX',
"-A"])
@click.command('postgres')
@pass_context
def postgres(context):
"""
Enter into postgres console for a given site.
"""
site = get_site(context)
frappe.init(site=site)
# This is assuming you're within the bench instance.
psql = find_executable('psql')
subprocess.run([ psql, '-d', frappe.conf.db_name])
@click.command('jupyter')
@pass_context
def jupyter(context):
@ -374,10 +387,16 @@ def console(context):
@click.option('--ui-tests', is_flag=True, default=False, help="Run UI Tests")
@click.option('--module', help="Run tests in a module")
@click.option('--profile', is_flag=True, default=False)
@click.option('--coverage', is_flag=True, default=False)
@click.option('--skip-test-records', is_flag=True, default=False, help="Don't create test records")
@click.option('--skip-before-tests', is_flag=True, default=False, help="Don't run before tests hook")
@click.option('--junit-xml-output', help="Destination file path for junit xml report")
@click.option('--failfast', is_flag=True, default=False)
@pass_context
def run_tests(context, app=None, module=None, doctype=None, test=(),
driver=None, profile=False, junit_xml_output=False, ui_tests = False, doctype_list_path=None):
driver=None, profile=False, coverage=False, junit_xml_output=False, ui_tests = False,
doctype_list_path=None, skip_test_records=False, skip_before_tests=False, failfast=False):
"Run tests"
import frappe.test_runner
tests = test
@ -385,9 +404,23 @@ def run_tests(context, app=None, module=None, doctype=None, test=(),
site = get_site(context)
frappe.init(site=site)
frappe.flags.skip_before_tests = skip_before_tests
frappe.flags.skip_test_records = skip_test_records
if coverage:
# Generate coverage report only for app that is being tested
source_path = os.path.join(get_bench_path(), 'apps', app or 'frappe')
cov = Coverage(source=[source_path], omit=['*.html', '*.js', '*.css'])
cov.start()
ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests,
force=context.force, profile=profile, junit_xml_output=junit_xml_output,
ui_tests = ui_tests, doctype_list_path = doctype_list_path)
ui_tests = ui_tests, doctype_list_path = doctype_list_path, failfast=failfast)
if coverage:
cov.stop()
cov.save()
if len(ret.failures) == 0 and len(ret.errors) == 0:
ret = 0
@ -533,8 +566,9 @@ def get_version():
@click.command('setup-global-help')
@click.option('--mariadb_root_password')
def setup_global_help(mariadb_root_password=None):
@click.option('--db_type')
@click.option('--root_password')
def setup_global_help(db_type=None, root_password=None):
'''setup help table in a separate database that will be
shared by the whole bench and set `global_help_setup` as 1 in
common_site_config.json'''
@ -550,8 +584,12 @@ def setup_global_help(mariadb_root_password=None):
update_site_config('global_help_setup', 1,
site_config_path=os.path.join('.', 'common_site_config.json'))
if mariadb_root_password:
frappe.local.conf.root_password = mariadb_root_password
if root_password:
frappe.local.conf.root_password = root_password
if not frappe.local.conf.db_type:
frappe.local.conf.db_type = db_type
from frappe.utils.help import sync
sync()
@ -663,6 +701,7 @@ commands = [
make_app,
mysql,
mariadb,
postgres,
request,
reset_perms,
run_tests,

File diff suppressed because it is too large Load diff

View file

@ -250,10 +250,10 @@ def address_query(doctype, txt, searchfield, start, page_len, filters):
`tabAddress`.idx desc, `tabAddress`.name
limit %(start)s, %(page_len)s """.format(
mcond=get_match_cond(doctype),
key=frappe.db.escape(searchfield),
key=searchfield,
condition=condition or ""),
{
'txt': "%%%s%%" % frappe.db.escape(txt),
'txt': frappe.db.escape('%' + txt + '%'),
'_txt': txt.replace("%", ""),
'start': start,
'page_len': page_len,

View file

@ -51,6 +51,18 @@ frappe.ui.form.on("Contact", {
frappe.model.remove_from_locals(d.link_doctype, d.link_name);
});
}
},
after_save: function() {
frappe.run_serially([
() => frappe.timeout(1),
() => {
var last_route = frappe.route_history.slice(-2, -1)[0];
if(frappe.dynamic_link && frappe.dynamic_link.doc
&& last_route.length > 2 && frappe.dynamic_link.doc.name == last_route[2]){
frappe.set_route(last_route[0], last_route[1], last_route[2]);
}
}
]);
}
});

File diff suppressed because it is too large Load diff

View file

@ -151,9 +151,9 @@ def contact_query(doctype, txt, searchfield, start, page_len, filters):
`tabContact`.idx desc, `tabContact`.name
limit %(start)s, %(page_len)s """.format(
mcond=get_match_cond(doctype),
key=frappe.db.escape(searchfield)),
key=searchfield),
{
'txt': "%%%s%%" % frappe.db.escape(txt),
'txt': frappe.db.escape('%' + txt + '%'),
'_txt': txt.replace("%", ""),
'start': start,
'page_len': page_len,

View file

@ -46,4 +46,4 @@ def add_authentication_log(subject, user, operation="Login", status="Success"):
def clear_authentication_logs():
"""clear 100 day old authentication logs"""
frappe.db.sql("""delete from `tabActivity Log` where \
creation<DATE_SUB(NOW(), INTERVAL 100 DAY)""")
creation< (NOW() - INTERVAL '100' DAY)""")

View file

@ -59,7 +59,7 @@ def logout_feed(user, reason):
def get_feed_match_conditions(user=None, force=True):
if not user: user = frappe.session.user
conditions = ['`tabCommunication`.owner="{user}" or `tabCommunication`.reference_owner="{user}"'.format(user=frappe.db.escape(user))]
conditions = ['`tabCommunication`.owner="{user}" or `tabCommunication`.reference_owner={user}'.format(user=frappe.db.escape(user))]
user_permissions = frappe.permissions.get_user_permissions(user)
can_read = frappe.get_user().get_can_read()
@ -76,7 +76,7 @@ def get_feed_match_conditions(user=None, force=True):
can_read_docs = []
for doctype, obj in user_permissions.items():
for n in obj.get("docs", []):
can_read_docs.append('"{}|{}"'.format(doctype, frappe.db.escape(n)))
can_read_docs.append('{}|{}'.format(doctype, frappe.db.escape(n)))
if can_read_docs:
conditions.append("concat_ws('|', `tabCommunication`.reference_doctype, `tabCommunication`.reference_name) in ({})".format(

View file

@ -39,9 +39,10 @@ class TestActivityLog(unittest.TestCase):
frappe.local.form_dict = frappe._dict()
def get_auth_log(self, operation='Login'):
names = frappe.db.sql_list("""select name from `tabActivity Log`
where user='Administrator' and operation='{operation}' order by
creation desc""".format(operation=operation))
names = frappe.db.get_all('Activity Log', filters={
'user': 'Administrator',
'operation': operation,
}, order_by='`creation` DESC')
name = names[0]
auth_log = frappe.get_doc('Activity Log', name)

View file

@ -8,7 +8,7 @@ import json
from frappe.core.doctype.user.user import extract_mentions
from frappe.utils import get_fullname, get_link_to_form
from frappe.website.render import clear_cache
from frappe.model.db_schema import add_column
from frappe.database.schema import add_column
from frappe.exceptions import ImplicitCommitError
def on_trash(doc):
@ -114,9 +114,7 @@ def get_comments_from_parent(doc):
_comments = frappe.db.get_value(doc.reference_doctype, doc.reference_name, "_comments") or "[]"
except Exception as e:
if e.args[0] in (1146, 1054):
# 1146 = no table
# 1054 = missing column
if frappe.db.is_missing_table_or_column(e):
_comments = "[]"
else:
@ -140,7 +138,7 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments):
"%s", "%s"), (json.dumps(_comments), reference_name))
except Exception as e:
if e.args[0] == 1054 and getattr(frappe.local, 'request', None):
if frappe.db.is_column_missing(e) and getattr(frappe.local, 'request', None):
# missing column and in request, add column and update after commit
frappe.local._comments = (getattr(frappe.local, "_comments", [])
+ [(reference_doctype, reference_name, _comments)])

View file

@ -18,10 +18,6 @@ import time
from frappe import _
from frappe.utils.background_jobs import enqueue
# imports - third-party imports
import pymysql
from pymysql.constants import ER
@frappe.whitelist()
def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent",
sender=None, sender_full_name=None, recipients=None, communication_medium="Email", send_email=False,
@ -495,9 +491,9 @@ def sendmail(communication_name, print_html=None, print_format=None, attachments
communication._notify(print_html=print_html, print_format=print_format, attachments=attachments,
recipients=recipients, cc=cc, bcc=bcc)
except pymysql.InternalError as e:
except frappe.db.InternalError:
# deadlock, try again
if e.args[0] == ER.LOCK_DEADLOCK:
if frappe.db.is_deadlocked():
frappe.db.rollback()
time.sleep(1)
continue

View file

@ -144,8 +144,9 @@ const add_doctype_field_multicheck_control = (doctype, parent_wrapper) => {
const options = fields
.map(df => {
return {
label: df.label + (df.reqd ? ' (M)' : ''),
label: df.label,
value: df.fieldname,
danger: df.reqd,
checked: 1
};
});

View file

@ -134,9 +134,10 @@ class DataExporter:
# build list of valid docfields
tablecolumns = []
for f in frappe.db.sql('desc `tab%s`' % dt):
field = meta.get_field(f[0])
if field and ((self.select_columns and f[0] in self.select_columns[dt]) or not self.select_columns):
table_name = 'tab' + dt
for f in frappe.db.get_table_columns_description(table_name):
field = meta.get_field(f.name)
if field and ((self.select_columns and f.name in self.select_columns[dt]) or not self.select_columns):
tablecolumns.append(field)
tablecolumns.sort(key = lambda a: int(a.idx))

View file

@ -37,7 +37,7 @@ frappe.ui.form.on('Data Import', {
frm.disable_save();
frm.dashboard.clear_headline();
if (frm.doc.reference_doctype && !frm.doc.import_file) {
frm.page.set_indicator(__('Please attach a file to import'), 'orange');
frm.page.set_indicator(__('Attach file'), 'orange');
} else {
if (frm.doc.import_status) {
const listview_settings = frappe.listview_settings['Data Import'];
@ -147,24 +147,22 @@ frappe.data_import.download_dialog = function(frm) {
const filter_fields = df => frappe.model.is_value_type(df) && !df.hidden;
const get_fields = dt => frappe.meta.get_docfields(dt).filter(filter_fields);
const get_doctypes = parentdt => {
return [parentdt].concat(
frappe.meta.get_table_fields(parentdt).map(df => df.options)
);
};
const get_doctype_checkbox_fields = () => {
return dialog.fields.filter(df => df.fieldname.endsWith('_fields'))
.map(df => dialog.fields_dict[df.fieldname]);
};
const doctype_fields = get_fields(frm.doc.reference_doctype)
.map(df => ({
label: df.label + ((df.reqd || df.fieldname == 'naming_series') ? ' (M)' : ''),
reqd: (df.reqd || df.fieldname == 'naming_series') ? 1 : 0,
value: df.fieldname,
checked: 1
}));
.map(df => {
let reqd = (df.reqd || df.fieldname == 'naming_series') ? 1 : 0;
return {
label: df.label,
reqd: reqd,
danger: reqd,
value: df.fieldname,
checked: 1
};
});
let fields = [
{
@ -176,15 +174,14 @@ frappe.data_import.download_dialog = function(frm) {
"onchange": function() {
const fields = get_doctype_checkbox_fields();
fields.map(f => f.toggle(true));
if(this.value == 'Mandatory') {
if(this.value == 'Mandatory' || this.value == 'Manually') {
checkbox_toggle(true);
fields.map(multicheck_field => {
multicheck_field.options.map(option => {
if(!option.reqd) return;
$(multicheck_field.$wrapper).find(`:checkbox[data-unit="${option.value}"]`)
.prop('checked', false)
.trigger('click')
.prop('disabled', true);
.trigger('click');
});
});
} else if(this.value == 'All'){
@ -237,10 +234,11 @@ frappe.data_import.download_dialog = function(frm) {
"options": frappe.meta.get_docfields(df.options)
.filter(filter_fields)
.map(df => ({
label: df.label + (df.reqd ? ' (M)' : ''),
label: df.label,
reqd: df.reqd ? 1 : 0,
value: df.fieldname,
checked: 1
checked: 1,
danger: df.reqd
})),
"columns": 2,
"hidden": 1

View file

@ -11,14 +11,11 @@ class DefaultValue(Document):
def on_doctype_update():
"""Create indexes for `tabDefaultValue` on `(parent, defkey)`"""
if not frappe.db.sql("""show index from `tabDefaultValue`
where Key_name="defaultvalue_parent_defkey_index" """):
frappe.db.commit()
frappe.db.sql("""alter table `tabDefaultValue`
add index defaultvalue_parent_defkey_index(parent, defkey)""")
frappe.db.commit()
frappe.db.add_index(doctype='DefaultValue',
fields=['parent', 'defkey'],
index_name='defaultvalue_parent_defkey_index')
if not frappe.db.sql("""show index from `tabDefaultValue`
where Key_name="defaultvalue_parent_parenttype_index" """):
frappe.db.commit()
frappe.db.sql("""alter table `tabDefaultValue`
add index defaultvalue_parent_parenttype_index(parent, parenttype)""")
frappe.db.add_index(doctype='DefaultValue',
fields=['parent', 'parenttype'],
index_name='defaultvalue_parent_parenttype_index')

View file

@ -16,6 +16,7 @@
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -47,6 +48,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -80,6 +82,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -113,6 +116,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -147,6 +151,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -180,6 +185,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -209,6 +215,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -242,6 +249,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -275,6 +283,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -305,6 +314,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -333,41 +343,43 @@
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "track_views",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Track Views",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "track_views",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Track Views",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -401,6 +413,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -431,6 +444,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -462,6 +476,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -495,6 +510,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -525,11 +541,12 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Naming Options:\n<ol><li><b>field:[fieldname]</b> - By Field</li><li><b>naming_series:</b> - By Naming Series (field called naming_series must be present</li><li><b>Prompt</b> - Prompt user for a name</li><li><b>[series]</b> - Series by prefix (separated by a dot); for example PRE.#####</li>\n<li><b>concatenate:[fieldname1],[fieldname2],...[fieldnameX]</b> - By fieldname concatenation (you can concatenate as many fields as you like. Series option also works)</li></ol>",
"description": "Naming Options:\n<ol><li><b>field:[fieldname]</b> - By Field</li><li><b>naming_series:</b> - By Naming Series (field called naming_series must be present</li><li><b>Prompt</b> - Prompt user for a name</li><li><b>[series]</b> - Series by prefix (separated by a dot); for example PRE.#####</li>\n<li><b>format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####}</b> - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.</li></ol>",
"fieldname": "autoname",
"fieldtype": "Data",
"hidden": 0,
@ -558,6 +575,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -591,6 +609,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -621,6 +640,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -653,6 +673,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -684,6 +705,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -716,6 +738,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -749,6 +772,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -781,6 +805,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -814,6 +839,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -844,6 +870,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -878,6 +905,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -910,6 +938,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -940,6 +969,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -971,6 +1001,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1002,6 +1033,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1034,6 +1066,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1066,6 +1099,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1098,6 +1132,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1130,6 +1165,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1161,6 +1197,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1192,6 +1229,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1226,6 +1264,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1256,6 +1295,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1286,6 +1326,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1316,6 +1357,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1347,6 +1389,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1379,6 +1422,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1409,6 +1453,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1441,6 +1486,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1471,6 +1517,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1502,6 +1549,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1535,6 +1583,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1568,6 +1617,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1598,6 +1648,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1630,6 +1681,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1662,6 +1714,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1693,6 +1746,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1725,6 +1779,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1758,6 +1813,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1788,6 +1844,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1820,6 +1877,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1853,6 +1911,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
@ -1884,6 +1943,7 @@
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -1928,7 +1988,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-04-27 09:27:59.444089",
"modified": "2018-08-28 01:14:38.862469",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
@ -1981,5 +2041,6 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}
"track_seen": 0,
"track_views": 0
}

View file

@ -10,19 +10,15 @@ import frappe
from frappe import _
from frappe.utils import now, cint
from frappe.model import no_value_fields, default_fields
from frappe.model import no_value_fields, default_fields, data_fieldtypes
from frappe.model.document import Document
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.desk.notifications import delete_notification_count_for
from frappe.modules import make_boilerplate, get_doc_path
from frappe.model.db_schema import validate_column_name, validate_column_length, type_map
from frappe.database.schema import validate_column_name, validate_column_length
from frappe.model.docfield import supports_translation
import frappe.website.render
# imports - third-party imports
import pymysql
from pymysql.constants import ER
class InvalidFieldNameError(frappe.ValidationError): pass
form_grid_templates = {
@ -123,20 +119,31 @@ class DocType(Document):
link_fieldname, source_fieldname = df.fetch_from.split('.', 1)
link_df = new_meta.get_field(link_fieldname)
self.flags.update_fields_to_fetch_queries.append('''update
`tab{link_doctype}` source,
`tab{doctype}` target
set
target.`{fieldname}` = source.`{source_fieldname}`
where
target.`{link_fieldname}` = source.name
and ifnull(target.`{fieldname}`, '')="" '''.format(
link_doctype = link_df.options,
source_fieldname = source_fieldname,
doctype = self.name,
fieldname = df.fieldname,
link_fieldname = link_fieldname
))
if frappe.conf.db_type == 'postgres':
update_query = '''
UPDATE `tab{doctype}`
SET `{fieldname}` = source.`{source_fieldname}`
FROM `tab{link_doctype}` as source
WHERE `{link_fieldname}` = source.name
AND ifnull(`{fieldname}`, '')=''
'''
else:
update_query = '''
UPDATE `tab{doctype}` as target
INNER JOIN `tab{link_doctype}` as source
ON `target`.`{link_fieldname}` = `source`.`name`
SET `target`.`{fieldname}` = `source`.`{source_fieldname}`
WHERE ifnull(`target`.`{fieldname}`, '')=""
'''
self.flags.update_fields_to_fetch_queries.append(update_query.format(
link_doctype = link_df.options,
source_fieldname = source_fieldname,
doctype = self.name,
fieldname = df.fieldname,
link_fieldname = link_fieldname
)
)
def update_fields_to_fetch(self):
'''Update fetch values based on queries setup'''
@ -164,10 +171,9 @@ class DocType(Document):
"""Change the timestamp of parent DocType if the current one is a child to clear caches."""
if frappe.flags.in_import:
return
parent_list = frappe.db.sql("""SELECT parent
from tabDocField where fieldtype="Table" and options=%s""", self.name)
parent_list = frappe.db.get_all('DocField', 'parent', dict(fieldtype='Table', options=self.name))
for p in parent_list:
frappe.db.sql('UPDATE tabDocType SET modified=%s WHERE `name`=%s', (now(), p[0]))
frappe.db.sql('UPDATE `tabDocType` SET modified=%s WHERE `name`=%s', (now(), p.parent))
def scrub_field_names(self):
"""Sluggify fieldnames if not set from Label."""
@ -199,12 +205,12 @@ class DocType(Document):
"""Strip options for whitespaces"""
for field in self.fields:
if field.fieldtype == "Select" and field.options is not None:
new_options = ""
for option in field.options.split("\n"):
new_options += option.strip()
new_options += "\n"
new_options.rstrip("\n")
field.options = new_options
options_list = []
for i, option in enumerate(field.options.split("\n")):
_option = option.strip()
if i==0 or _option:
options_list.append(_option)
field.options = '\n'.join(options_list)
def validate_series(self, autoname=None, name=None):
"""Validate if `autoname` property is correctly set."""
@ -229,19 +235,24 @@ class DocType(Document):
if autoname and (not autoname.startswith('field:')) \
and (not autoname.startswith('eval:')) \
and (not autoname.lower() in ('prompt', 'hash')) \
and (not autoname.startswith('naming_series:')):
and (not autoname.startswith('naming_series:')) \
and (not autoname.startswith('format:')):
prefix = autoname.split('.')[0]
used_in = frappe.db.sql('select name from tabDocType where substring_index(autoname, ".", 1) = %s and name!=%s', (prefix, name))
used_in = frappe.db.sql("""
SELECT `name`
FROM `tabDocType`
WHERE `autoname` LIKE CONCAT(%s, '.%%')
AND `name`!=%s
""", (prefix, name))
if used_in:
frappe.throw(_("Series {0} already used in {1}").format(prefix, used_in[0][0]))
def on_update(self):
"""Update database schema, make controller templates if `custom` is not set and clear cache."""
from frappe.model.db_schema import updatedb
self.delete_duplicate_custom_fields()
try:
updatedb(self.name, self)
frappe.db.updatedb(self.name, self)
except Exception as e:
print("\n\nThere was an issue while migrating the DocType: {}\n".format(self.name))
raise e
@ -280,7 +291,9 @@ class DocType(Document):
def delete_duplicate_custom_fields(self):
if not (frappe.db.table_exists(self.name) and frappe.db.table_exists("Custom Field")):
return
fields = [d.fieldname for d in self.fields if d.fieldtype in type_map]
fields = [d.fieldname for d in self.fields if d.fieldtype in data_fieldtypes]
frappe.db.sql('''delete from
`tabCustom Field`
where
@ -514,6 +527,9 @@ def validate_fields(meta):
options = frappe.db.get_value("DocType", d.options, "name")
if not options:
frappe.throw(_("Options must be a valid DocType for field {0} in row {1}").format(d.label, d.idx))
elif not (options == d.options):
frappe.throw(_("Options {0} must be the same as doctype name {1} for the field {2}")
.format(d.options, options, d.label))
else:
# fix case
d.options = options
@ -568,8 +584,8 @@ def validate_fields(meta):
group by `{fieldname}` having count(*) > 1 limit 1""".format(
doctype=d.parent, fieldname=d.fieldname))
except pymysql.InternalError as e:
if e.args and e.args[0] == ER.BAD_FIELD_ERROR:
except frappe.db.InternalError as e:
if frappe.db.is_missing_column(e):
# ignore if missing column, else raise
# this happens in case of Custom Field
pass
@ -740,13 +756,15 @@ def validate_permissions_for_doctype(doctype, for_remove=False):
def clear_permissions_cache(doctype):
frappe.clear_cache(doctype=doctype)
delete_notification_count_for(doctype)
for user in frappe.db.sql_list("""select
distinct `tabHas Role`.parent
from
for user in frappe.db.sql_list("""
SELECT
DISTINCT `tabHas Role`.`parent`
FROM
`tabHas Role`,
tabDocPerm
where tabDocPerm.parent = %s
and tabDocPerm.role = `tabHas Role`.role""", doctype):
`tabDocPerm`
WHERE `tabDocPerm`.`parent` = %s
AND `tabDocPerm`.`role` = `tabHas Role`.`role`
""", doctype):
frappe.clear_cache(user=user)
def validate_permissions(doctype, for_remove=False):
@ -849,7 +867,8 @@ def make_module_and_roles(doc, perm_fieldname="permissions"):
not frappe.db.exists('Domain', doc.restrict_to_domain):
frappe.get_doc(dict(doctype='Domain', domain=doc.restrict_to_domain)).insert()
if not frappe.db.exists("Module Def", doc.module):
if ("tabModule Def" in frappe.db.get_tables()
and not frappe.db.exists("Module Def", doc.module)):
m = frappe.get_doc({"doctype": "Module Def", "module_name": doc.module})
m.app_name = frappe.local.module_app[frappe.scrub(doc.module)]
m.flags.ignore_mandatory = m.flags.ignore_permissions = True
@ -865,8 +884,8 @@ def make_module_and_roles(doc, perm_fieldname="permissions"):
r.insert()
except frappe.DoesNotExistError as e:
pass
except frappe.SQLError as e:
if e.args[0]==1146:
except frappe.db.ProgrammingError as e:
if frappe.db.is_table_missing(e):
pass
else:
raise

View file

@ -14,14 +14,14 @@ class ErrorLog(Document):
def set_old_logs_as_seen():
# set logs as seen
frappe.db.sql("""update `tabError Log` set seen=1
where seen=0 and datediff(curdate(), creation) > 7""")
frappe.db.sql("""UPDATE `tabError Log` SET `seen`=1
WHERE `seen`=0 AND `creation` < (NOW() - INTERVAL '7' DAY)""")
# clear old logs
frappe.db.sql("""delete from `tabError Log` where datediff(curdate(), creation) > 30""")
frappe.db.sql("""DELETE FROM `tabError Log` WHERE `creation` < (NOW() - INTERVAL '30' DAY)""")
@frappe.whitelist()
def clear_error_logs():
'''Flush all Error Logs'''
frappe.only_for('System Manager')
frappe.db.sql('''delete from `tabError Log`''')
frappe.db.sql('''DELETE FROM `tabError Log`''')

View file

@ -35,4 +35,4 @@ def is_valid_feedback_request(key=None):
def delete_feedback_request():
""" clear 100 days old feedback request """
frappe.db.sql("""delete from `tabFeedback Request` where creation<DATE_SUB(NOW(), INTERVAL 100 DAY)""")
frappe.db.sql("""delete from `tabFeedback Request` where `creation` < (NOW() - INTERVAL '100' DAY)""")

View file

@ -42,7 +42,7 @@
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 1
"unique": 0
},
{
"allow_bulk_edit": 0,
@ -600,7 +600,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-06-26 14:48:49.989952",
"modified": "2018-06-27 14:48:49.989952",
"modified_by": "Administrator",
"module": "Core",
"name": "Report",

View file

@ -16,7 +16,7 @@ class TransactionLog(Document):
self.timestamp = now()
if index != 1:
prev_hash = frappe.db.sql(
"SELECT chaining_hash FROM `tabTransaction Log` WHERE row_index = {0}".format(index - 1))
"SELECT `chaining_hash` FROM `tabTransaction Log` WHERE `row_index` = '{0}'".format(index - 1))
if prev_hash:
self.previous_hash = prev_hash[0][0]
else:
@ -46,16 +46,18 @@ class TransactionLog(Document):
def get_current_index():
current = frappe.db.sql(
"SELECT `current` FROM tabSeries WHERE name='TRANSACTLOG' FOR UPDATE")
current = frappe.db.sql("""SELECT `current`
FROM `tabSeries`
WHERE `name` = 'TRANSACTLOG'
FOR UPDATE""")
if current and current[0][0] is not None:
current = current[0][0]
frappe.db.sql(
"UPDATE tabSeries SET current = current+1 where name='TRANSACTLOG'")
frappe.db.sql("""UPDATE `tabSeries`
SET `current` = `current` + 1
where `name` = 'TRANSACTLOG'""")
current = cint(current) + 1
else:
frappe.db.sql(
"INSERT INTO tabSeries (name, current) VALUES ('TRANSACTLOG', 1)")
frappe.db.sql("INSERT INTO `tabSeries` (name, current) VALUES ('TRANSACTLOG', 1)")
current = 1
return current

View file

@ -278,4 +278,4 @@ class TestUser(unittest.TestCase):
self.assertEqual(extract_mentions(user_name)[0], "test.user@example.com")
def delete_contact(user):
frappe.db.sql("delete from tabContact where email_id='%s'" % frappe.db.escape(user))
frappe.db.sql("DELETE FROM `tabContact` WHERE `email_id`= %s", user)

View file

@ -97,8 +97,7 @@ class User(Document):
if self.name not in ('Administrator', 'Guest') and not self.user_image:
frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name)
def has_website_permission(self, ptype, verbose=False):
def has_website_permission(self, ptype, user, verbose=False):
"""Returns true if current user is the session user"""
return self.name == frappe.session.user
@ -230,12 +229,12 @@ class User(Document):
return link
def get_other_system_managers(self):
return frappe.db.sql("""select distinct user.name from `tabHas Role` user_role, tabUser user
return frappe.db.sql("""select distinct `user`.`name` from `tabHas Role` as `user_role`, `tabUser` as `user`
where user_role.role='System Manager'
and user.docstatus<2
and user.enabled=1
and user_role.parent = user.name
and user_role.parent not in ('Administrator', %s) limit 1""", (self.name,))
and `user`.docstatus<2
and `user`.enabled=1
and `user_role`.parent = `user`.name
and `user_role`.parent not in ('Administrator', %s) limit 1""", (self.name,))
def get_fullname(self):
"""get first_name space last_name"""
@ -312,8 +311,8 @@ class User(Document):
frappe.local.login_manager.logout(user=self.name)
# delete todos
frappe.db.sql("""delete from `tabToDo` where owner=%s""", (self.name,))
frappe.db.sql("""update tabToDo set assigned_by=null where assigned_by=%s""",
frappe.db.sql("""DELETE FROM `tabToDo` WHERE `owner`=%s""", (self.name,))
frappe.db.sql("""UPDATE `tabToDo` SET `assigned_by`=NULL WHERE `assigned_by`=%s""",
(self.name,))
# delete events
@ -346,23 +345,23 @@ class User(Document):
validate_email_add(email.strip(), True)
def after_rename(self, old_name, new_name, merge=False):
tables = frappe.db.sql("show tables")
tables = frappe.db.get_tables()
for tab in tables:
desc = frappe.db.sql("desc `%s`" % tab[0], as_dict=1)
desc = frappe.db.get_table_columns_description(tab)
has_fields = []
for d in desc:
if d.get('Field') in ['owner', 'modified_by']:
has_fields.append(d.get('Field'))
if d.get('name') in ['owner', 'modified_by']:
has_fields.append(d.get('name'))
for field in has_fields:
frappe.db.sql("""\
update `%s` set `%s`=%s
where `%s`=%s""" % \
(tab[0], field, '%s', field, '%s'), (new_name, old_name))
frappe.db.sql("""UPDATE `%s`
SET `%s` = %s
WHERE `%s` = %s""" %
(tab, field, '%s', field, '%s'), (new_name, old_name))
# set email
frappe.db.sql("""\
update `tabUser` set email=%s
where name=%s""", (new_name, new_name))
frappe.db.sql("""UPDATE `tabUser`
SET email = %s
WHERE name = %s""", (new_name, new_name))
def append_roles(self, *roles):
"""Add roles to user"""
@ -688,9 +687,8 @@ def setup_user_email_inbox(email_account, awaiting_password, email_id, enable_ou
"awaiting_password": awaiting_password or 0
})
else:
frappe.msgprint(_("Enabled email inbox for user {users}".format(
users=" and ".join([frappe.bold(user.get("name")) for user in user_names])
)))
users = " and ".join([frappe.bold(user.get("name")) for user in user_names])
frappe.msgprint(_("Enabled email inbox for user {0}").format(users))
ask_pass_update()
@ -824,31 +822,33 @@ def user_query(doctype, txt, searchfield, start, page_len, filters):
user_type_condition = ''
txt = "%{}%".format(txt)
return frappe.db.sql("""select name, concat_ws(' ', first_name, middle_name, last_name)
from `tabUser`
where enabled=1
return frappe.db.sql("""SELECT `name`, CONCAT_WS(' ', first_name, middle_name, last_name)
FROM `tabUser`
WHERE `enabled`=1
{user_type_condition}
and docstatus < 2
and name not in ({standard_users})
and ({key} like %(txt)s
or concat_ws(' ', first_name, middle_name, last_name) like %(txt)s)
AND `docstatus` < 2
AND `name` NOT IN ({standard_users})
AND ({key} LIKE %(txt)s
OR CONCAT_WS(' ', first_name, middle_name, last_name) LIKE %(txt)s)
{mcond}
order by
case when name like %(txt)s then 0 else 1 end,
case when concat_ws(' ', first_name, middle_name, last_name) like %(txt)s
then 0 else 1 end,
name asc
limit %(start)s, %(page_len)s""".format(
ORDER BY
CASE WHEN `name` LIKE %(txt)s THEN 0 ELSE 1 END,
CASE WHEN concat_ws(' ', first_name, middle_name, last_name) LIKE %(txt)s
THEN 0 ELSE 1 END,
NAME asc
LIMIT %(page_len)s OFFSET %(start)s""".format(
user_type_condition = user_type_condition,
standard_users=", ".join(["'{0}'".format(frappe.db.escape(u)) for u in STANDARD_USERS]),
standard_users=", ".join([frappe.db.escape(u) for u in STANDARD_USERS]),
key=searchfield, mcond=get_match_cond(doctype)),
dict(start=start, page_len=page_len, txt=txt))
def get_total_users():
"""Returns total no. of system users"""
return frappe.db.sql('''select sum(simultaneous_sessions) from `tabUser`
where enabled=1 and user_type="System User"
and name not in ({})'''.format(", ".join(["%s"]*len(STANDARD_USERS))), STANDARD_USERS)[0][0]
return frappe.db.sql('''SELECT SUM(`simultaneous_sessions`)
FROM `tabUser`
WHERE `enabled` = 1
AND `user_type` = 'System User'
AND `name` NOT IN ({})'''.format(", ".join(["%s"]*len(STANDARD_USERS))), STANDARD_USERS)[0][0]
def get_system_users(exclude_users=None, limit=None):
if not exclude_users:

View file

@ -58,8 +58,8 @@ def get_user_permissions(user=None):
if meta.is_nested_set():
out[perm.allow]["docs"].extend(frappe.db.get_descendants(perm.allow, perm.for_value))
frappe.cache().hset("user_permissions", user, out)
except frappe.SQLError as e:
if e.args[0]==1146:
except frappe.db.SQLError:
if frappe.db.is_table_missing():
# called from patch
pass

View file

@ -47,7 +47,7 @@ def get_unseen_likes():
return frappe.db.sql("""select count(*) from `tabCommunication`
where
communication_type='Comment'
and modified >= DATE_SUB(NOW(),INTERVAL 1 YEAR)
and modified >= (NOW() - INTERVAL '1' YEAR)
and comment_type='Like'
and owner is not null and owner!=%(user)s
and reference_owner=%(user)s
@ -60,12 +60,12 @@ def get_unread_emails():
SELECT count(*)
FROM `tabCommunication`
WHERE communication_type='Communication'
AND communication_medium="Email"
AND sent_or_received="Received"
AND email_status not in ("Spam", "Trash")
AND communication_medium='Email'
AND sent_or_received='Received'
AND email_status not in ('Spam', 'Trash')
AND email_account in (
SELECT distinct email_account from `tabUser Email` WHERE parent=%(user)s
)
AND modified >= DATE_SUB(NOW(),INTERVAL 1 YEAR)
AND modified >= (NOW() - INTERVAL '1' YEAR)
AND seen=0
""", {"user": frappe.session.user})[0][0]

View file

@ -6,7 +6,7 @@ import frappe
from frappe import _, throw
import frappe.utils.user
from frappe.permissions import check_admin_or_system_manager
from frappe.model.db_schema import type_map
from frappe.model import data_fieldtypes
def execute(filters=None):
user, doctype, show_permissions = filters.get("user"), filters.get("doctype"), filters.get("show_permissions")
@ -34,7 +34,7 @@ def get_columns_and_fields(doctype):
columns = ["Name:Link/{}:200".format(doctype)]
fields = ["`name`"]
for df in frappe.get_meta(doctype).fields:
if df.in_list_view and df.fieldtype in type_map:
if df.in_list_view and df.fieldtype in data_fieldtypes:
fields.append("`{0}`".format(df.fieldname))
fieldtype = "Link/{}".format(df.options) if df.fieldtype=="Link" else df.fieldtype
columns.append("{label}:{fieldtype}:{width}".format(label=df.label, fieldtype=fieldtype, width=df.width or 100))

View file

@ -63,8 +63,7 @@ class CustomField(Document):
if not frappe.db.get_value('DocType', self.dt, 'issingle'):
if (self.fieldname not in frappe.db.get_table_columns(self.dt)
or getattr(self, "_old_fieldtype", None) != self.fieldtype):
from frappe.model.db_schema import updatedb
updatedb(self.dt)
frappe.db.updatedb(self.dt)
def on_trash(self):
# delete property setter entries

View file

@ -10,7 +10,5 @@ import unittest
test_records = frappe.get_test_records('Custom Field')
from frappe.model.db_schema import InvalidColumnName
class TestCustomField(unittest.TestCase):
pass

View file

@ -149,8 +149,7 @@ class CustomizeForm(Document):
validate_fields_for_doctype(self.doc_type)
if self.flags.update_db:
from frappe.model.db_schema import updatedb
updatedb(self.doc_type)
frappe.db.updatedb(self.doc_type)
if not hasattr(self, 'hide_success') or not self.hide_success:
frappe.msgprint(_("{0} updated").format(_(self.doc_type)))
@ -183,7 +182,7 @@ class CustomizeForm(Document):
continue
elif property == "reqd" and \
((frappe.db.get_value("DocField",
((frappe.db.get_value("DocField",
{"parent":self.doc_type,"fieldname":df.fieldname}, "reqd") == 1) \
and (df.get(property) == 0)):
frappe.msgprint(_("Row {0}: Not allowed to disable Mandatory for standard fields")\
@ -322,7 +321,7 @@ class CustomizeForm(Document):
try:
property_value = frappe.db.get_value("DocType", self.doc_type, property_name)
except Exception as e:
if e.args[0]==1054:
if frappe.db.is_column_missing(e):
property_value = None
else:
raise
@ -342,7 +341,8 @@ class CustomizeForm(Document):
if not self.doc_type:
return
frappe.db.sql("""delete from `tabProperty Setter` where doc_type=%s
and !(`field_name`='naming_series' and `property`='options')""", self.doc_type)
frappe.db.sql("""DELETE FROM `tabProperty Setter` WHERE doc_type=%s
and `field_name`!='naming_series'
and `property`!='options'""", self.doc_type)
frappe.clear_cache(doctype=self.doc_type)
self.fetch_to_customize()

View file

@ -33,7 +33,7 @@ class PropertySetter(Document):
frappe.db.sql("""delete from `tabProperty Setter` where
doctype_or_field = %(doctype_or_field)s
and doc_type = %(doc_type)s
and ifnull(field_name,'') = ifnull(%(field_name)s, '')
and coalesce(field_name,'') = coalesce(%(field_name)s, '')
and property = %(property)s""", self.get_valid_dict())
def get_property_list(self, dt):
@ -41,7 +41,7 @@ class PropertySetter(Document):
from tabDocField
where parent=%s
and fieldtype not in ('Section Break', 'Column Break', 'HTML', 'Read Only', 'Table', 'Fold')
and ifnull(fieldname, '') != ''
and coalesce(fieldname, '') != ''
order by label asc""", dt, as_dict=1)
def get_setup_data(self):
@ -69,7 +69,7 @@ class PropertySetter(Document):
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype
validate_fields_for_doctype(self.doc_type)
def make_property_setter(doctype, fieldname, property, value, property_type, for_doctype = False,
def make_property_setter(doctype, fieldname, property, value, property_type, for_doctype = False,
validate_fields_for_doctype=True):
# WARNING: Ignores Permissions
property_setter = frappe.get_doc({

View file

@ -1,14 +0,0 @@
{
"app_url": "https://github.com/revant/civil_contracting.git",
"app_name": "civil_contracting",
"app_icon": "octicon octicon-file-directory",
"app_color": "grey",
"app_description": "Civil Contracting App to manage workers, wages and measurements",
"app_publisher": "Revant Nandgaonkar",
"app_email": "revant@mntechnique.com",
"repo_url": "https://github.com/revant/civil_contracting.git",
"app_title": "Civil Contracting",
"app_version": "1.5.0",
"app_category": "Integrations",
"featured": 1
}

View file

@ -1,14 +0,0 @@
{
"app_url": "https://github.com/frappe/erpnext_shopify.git",
"app_name": "erpnext_shopify",
"app_icon": "octicon octicon-file-directory",
"app_color": "grey",
"app_description": "Shopify connector for ERPNext",
"app_publisher": "Frappe",
"app_email": "hello@frappe.io",
"repo_url": "https://github.com/frappe/erpnext_shopify.git",
"app_title": "ERPNext Shopify",
"app_version": "1.0.0",
"app_category": "Integrations",
"featured": 1
}

View file

@ -1,13 +0,0 @@
{
"app_url": "https://github.com/frappe/mandrill_integration",
"app_name": "mandrill_integration",
"app_icon": "octicon octicon-inbox",
"app_color": "#4CB6E6",
"app_description": "Set email communication status (Sent, Bounced etc) from Mandrill via webhooks.",
"app_publisher": "Frappe Technologies Pvt Ltd, Sponsored by Rohit Industries Group Pvt Ltd",
"app_email": "hello@frappe.io",
"repo_url": "https://github.com/frappe/mandrill_integration.git",
"app_title": "Mandrill Integration",
"app_version": "0.1.0",
"app_category": "Integrations"
}

View file

@ -1,13 +0,0 @@
{
"app_url": "https://github.com/semilimes/sendgrid_integration",
"app_name": "sendgrid_integration",
"app_icon": "octicon octicon-inbox",
"app_color": "#4CB6E6",
"app_description": "Set email communication status from SendGrid via webhook.",
"app_publisher": "Semilimes",
"app_email": "all@semilimes.com",
"repo_url": "https://github.com/semilimes/sendgrid_integration.git",
"app_title": "SendGrid Integration",
"app_version": "0.0.1",
"app_category": "Integrations"
}

View file

@ -1,42 +0,0 @@
{
"db_name": "testdb",
"db_password": "password",
"mute_emails": true,
"limits": {
"emails": 1500,
"space": 0.157,
"expiry": "2016-07-25",
"users": 1
}
"developer_mode": 1,
"auto_cache_clear": true,
"disable_website_cache": true,
"max_file_size": 1000000,
"mail_server": "localhost",
"mail_login": null,
"mail_password": null,
"mail_port": 25,
"use_ssl": 0,
"auto_email_id": "hello@example.com",
"google_login": {
"client_id": "google_client_id",
"client_secret": "google_client_secret"
},
"github_login": {
"client_id": "github_client_id",
"client_secret": "github_client_secret"
},
"facebook_login": {
"client_id": "facebook_client_id",
"client_secret": "facebook_client_secret"
},
"celery_broker": "redis://localhost",
"celery_result_backend": null,
"scheduler_interval": 300,
"celery_queue_per_site": true
}

View file

@ -45,11 +45,11 @@ class TestDataMigrationRun(unittest.TestCase):
created_todo = frappe.get_doc('ToDo', {'description': event_subject})
self.assertEqual(created_todo.description, event_subject)
todo_list = frappe.get_list('ToDo', filters={'description': 'Data migration todo'}, fields=['name'])
todo_list = frappe.get_list('ToDo', filters={'description': 'data migration todo'}, fields=['name'])
todo_name = todo_list[0].name
todo = frappe.get_doc('ToDo', todo_name)
todo.description = 'Data migration todo updated'
todo.description = 'data migration todo updated'
todo.save()
run = frappe.get_doc({
@ -77,7 +77,7 @@ def create_plan():
{ 'remote_fieldname': 'starts_on', 'local_fieldname': 'eval:frappe.utils.get_datetime_str(frappe.utils.get_datetime())' }
],
'condition': '{"description": "data migration todo" }'
}).insert()
}).insert(ignore_if_duplicate=True)
frappe.get_doc({
'doctype': 'Data Migration Mapping',
@ -91,23 +91,24 @@ def create_plan():
'fields': [
{ 'remote_fieldname': 'subject', 'local_fieldname': 'description' }
]
}).insert()
}).insert(ignore_if_duplicate=True)
frappe.get_doc({
'doctype': 'Data Migration Plan',
'plan_name': 'ToDo sync',
'plan_name': 'ToDo Sync',
'module': 'Core',
'mappings': [
{ 'mapping': 'Todo to Event' },
{ 'mapping': 'Event to ToDo' }
]
}).insert()
}).insert(ignore_if_duplicate=True)
frappe.get_doc({
'doctype': 'Data Migration Connector',
'connector_name': 'Local Connector',
'connector_type': 'Frappe',
'hostname': 'http://localhost:8000',
# connect to same host.
'hostname': frappe.conf.host_name,
'username': 'Administrator',
'password': 'admin'
}).insert()
}).insert(ignore_if_duplicate=True)

View file

@ -0,0 +1,42 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
# Database Module
# --------------------
from __future__ import unicode_literals
def setup_database(force, source_sql=None, verbose=None):
import frappe
if frappe.conf.db_type == 'postgres':
import frappe.database.postgres.setup_db
return frappe.database.postgres.setup_db.setup_database(force, source_sql, verbose)
else:
import frappe.database.mariadb.setup_db
return frappe.database.mariadb.setup_db.setup_database(force, source_sql, verbose)
def drop_user_and_database(db_name, root_login=None, root_password=None):
import frappe
if frappe.conf.db_type == 'postgres':
pass
else:
import frappe.database.mariadb.setup_db
return frappe.database.mariadb.setup_db.drop_user_and_database(db_name, root_login, root_password)
def get_db(host=None, user=None, password=None):
import frappe
if frappe.conf.db_type == 'postgres':
import frappe.database.postgres.database
return frappe.database.postgres.database.PostgresDatabase(host, user, password)
else:
import frappe.database.mariadb.database
return frappe.database.mariadb.database.MariaDBDatabase(host, user, password)
def setup_help_database(help_db_name):
import frappe
if frappe.conf.db_type == 'postgres':
import frappe.database.postgres.setup_db
return frappe.database.postgres.setup_db.setup_help_database(help_db_name)
else:
import frappe.database.mariadb.setup_db
return frappe.database.mariadb.setup_db.setup_help_database(help_db_name)

View file

@ -5,66 +5,55 @@
# --------------------
from __future__ import unicode_literals
import warnings
import datetime
import frappe
import frappe.defaults
from time import time
import re
import time
import frappe
import datetime
import frappe.defaults
import frappe.model.meta
from frappe.utils import now, get_datetime, cstr, cast_fieldtype
from frappe import _
from frappe.model.utils.link_count import flush_local_link_count
from frappe.model.utils import STANDARD_FIELD_CONVERSION_MAP
from time import time
from frappe.utils import now, getdate, cast_fieldtype
from frappe.utils.background_jobs import execute_job, get_queue
from frappe import as_unicode
import six
from frappe.model.utils.link_count import flush_local_link_count
# imports - compatibility imports
from six import (
integer_types,
string_types,
binary_type,
text_type,
iteritems
)
# imports - third-party imports
from markdown2 import UnicodeWithAttrs
from pymysql.times import TimeDelta
from pymysql.constants import ER, FIELD_TYPE
from pymysql.converters import conversions
import pymysql
# Helpers
def _cast_result(doctype, result):
batch = [ ]
try:
for field, value in result:
df = frappe.get_meta(doctype).get_field(field)
if df:
value = cast_fieldtype(df.fieldtype, value)
batch.append(tuple([field, value]))
except frappe.exceptions.DoesNotExistError:
return result
return tuple(batch)
class Database:
class Database(object):
"""
Open a database connection with the given parmeters, if use_default is True, use the
login details from `conf.py`. This is called by the request handler and is accessible using
the `db` global variable. the `sql` method is also global to run queries
"""
def __init__(self, host=None, user=None, password=None, ac_name=None, use_default = 0, local_infile = 0):
VARCHAR_LEN = 140
MAX_COLUMN_LENGTH = 64
OPTIONAL_COLUMNS = ["_user_tags", "_comments", "_assign", "_liked_by"]
DEFAULT_SHORTCUTS = ['_Login', '__user', '_Full Name', 'Today', '__today', "now", "Now"]
STANDARD_VARCHAR_COLUMNS = ('name', 'owner', 'modified_by', 'parent', 'parentfield', 'parenttype')
DEFAULT_COLUMNS = ['name', 'creation', 'modified', 'modified_by', 'owner', 'docstatus', 'parent',
'parentfield', 'parenttype', 'idx']
class InvalidColumnName(frappe.ValidationError): pass
def __init__(self, host=None, user=None, password=None, ac_name=None, use_default=0):
self.setup_type_map()
self.host = host or frappe.conf.db_host or 'localhost'
self.user = user or frappe.conf.db_name
self.db_name = frappe.conf.db_name
self._conn = None
if ac_name:
self.user = self.get_db_login(ac_name) or frappe.conf.db_name
self.user = ac_name or frappe.conf.db_name
if use_default:
self.user = frappe.conf.db_name
@ -75,63 +64,25 @@ class Database:
self.password = password or frappe.conf.db_password
self.value_cache = {}
# this param is to load CSV's with LOCAL keyword.
# it can be set in site_config as > bench set-config local_infile 1
# once the local-infile is set on MySql Server, the client needs to connect with this option
# Connections without this option leads to: 'The used command is not allowed with this MariaDB version' error
self.local_infile = local_infile or frappe.conf.local_infile
def get_db_login(self, ac_name):
return ac_name
def setup_type_map(self):
pass
def connect(self):
"""Connects to a database as set in `site_config.json`."""
warnings.filterwarnings('ignore', category=pymysql.Warning)
usessl = 0
if frappe.conf.db_ssl_ca and frappe.conf.db_ssl_cert and frappe.conf.db_ssl_key:
usessl = 1
self.ssl = {
'ca':frappe.conf.db_ssl_ca,
'cert':frappe.conf.db_ssl_cert,
'key':frappe.conf.db_ssl_key
}
conversions.update({
FIELD_TYPE.NEWDECIMAL: float,
FIELD_TYPE.DATETIME: get_datetime,
UnicodeWithAttrs: conversions[text_type]
})
if six.PY2:
conversions.update({
TimeDelta: conversions[binary_type]
})
if usessl:
self._conn = pymysql.connect(self.host, self.user or '', self.password or '',
charset='utf8mb4', use_unicode = True, ssl=self.ssl, conv = conversions, local_infile = self.local_infile)
else:
self._conn = pymysql.connect(self.host, self.user or '', self.password or '',
charset='utf8mb4', use_unicode = True, conv = conversions, local_infile = self.local_infile)
# MYSQL_OPTION_MULTI_STATEMENTS_OFF = 1
# # self._conn.set_server_option(MYSQL_OPTION_MULTI_STATEMENTS_OFF)
self.cur_db_name = self.user
self._conn = self.get_connection()
self._cursor = self._conn.cursor()
if self.user != 'root':
self.use(self.user)
frappe.local.rollback_observers = []
def use(self, db_name):
"""`USE` db_name."""
self._conn.select_db(db_name)
self.cur_db_name = db_name
def validate_query(self, q):
"""Throw exception for dangerous queries: `ALTER`, `DROP`, `TRUNCATE` if not `Administrator`."""
cmd = q.strip().lower().split()[0]
if cmd in ['alter', 'drop', 'truncate'] and frappe.session.user != 'Administrator':
frappe.throw(_("Not permitted"), frappe.PermissionError)
def get_connection(self):
pass
def get_database_size(self):
pass
def sql(self, query, values=(), as_dict = 0, as_list = 0, formatted = 0,
debug=0, ignore_ddl=0, as_utf8=0, auto_commit=0, update=None, explain=False):
@ -161,6 +112,10 @@ class Database:
{"name": "a%", "owner":"test@example.com"})
"""
if re.search(r'ifnull\(', query, flags=re.IGNORECASE):
# replaces ifnull in query with coalesce
query = re.sub(r'ifnull\(', 'coalesce(', query, flags=re.IGNORECASE)
if not self._conn:
self.connect()
@ -183,7 +138,7 @@ class Database:
if not isinstance(values, (dict, tuple, list)):
values = (values,)
if debug and query.lower().startswith('select'):
if debug:
try:
if explain:
self.explain_query(query, values)
@ -214,23 +169,19 @@ class Database:
frappe.errprint(("Execution time: {0} sec").format(round(time_end - time_start, 2)))
except Exception as e:
if ignore_ddl and e.args[0] in (ER.BAD_FIELD_ERROR, ER.NO_SUCH_TABLE,
ER.CANT_DROP_FIELD_OR_KEY):
pass
if(frappe.conf.db_type == 'postgres'):
self.rollback()
# NOTE: causes deadlock
# elif e.args[0]==2006:
# # mysql has gone away
# self.connect()
# return self.sql(query=query, values=values,
# as_dict=as_dict, as_list=as_list, formatted=formatted,
# debug=debug, ignore_ddl=ignore_ddl, as_utf8=as_utf8,
# auto_commit=auto_commit, update=update)
if ignore_ddl and (self.is_missing_column(e) or self.is_missing_table(e) or self.cant_drop_field_or_key(e)):
pass
else:
raise
if auto_commit: self.commit()
if not self._cursor.description:
return ()
# scrub output if required
if as_dict:
ret = self.fetch_as_dict(formatted, as_utf8)
@ -256,7 +207,7 @@ class Database:
import json
frappe.errprint(json.dumps(self.fetch_as_dict(), indent=1))
frappe.errprint("--- query explain end ---")
except:
except Exception:
frappe.errprint("error in query explain")
def sql_list(self, query, values=(), debug=False):
@ -290,7 +241,7 @@ class Database:
self.transaction_writes += 1
if self.transaction_writes > 200000:
if self.auto_commit_on_many_writes:
frappe.db.commit()
self.commit()
else:
frappe.throw(_("Too many writes in one request. Please send smaller requests"), frappe.ValidationError)
@ -298,26 +249,21 @@ class Database:
"""Internal. Converts results to dict."""
result = self._cursor.fetchall()
ret = []
needs_formatting = self.needs_formatting(result, formatted)
if result:
keys = [column[0] for column in self._cursor.description]
for r in result:
values = []
for i in range(len(r)):
if needs_formatting:
val = self.convert_to_simple_type(r[i], formatted)
else:
val = r[i]
if as_utf8 and type(val) is text_type:
val = val.encode('utf-8')
values.append(val)
for value in r:
if as_utf8 and isinstance(value, text_type):
value = value.encode('utf-8')
values.append(value)
ret.append(frappe._dict(zip(keys, values)))
return ret
def needs_formatting(self, result, formatted):
@staticmethod
def needs_formatting(result, formatted):
"""Returns true if the first row in the result has a Date, Datetime, Long Int."""
if result and result[0]:
for v in result[0]:
@ -332,65 +278,21 @@ class Database:
"""Returns result metadata."""
return self._cursor.description
def convert_to_simple_type(self, v, formatted=0):
"""Format date, time, longint values."""
return v
from frappe.utils import formatdate, fmt_money
if isinstance(v, (datetime.date, datetime.timedelta, datetime.datetime, integer_types)):
if isinstance(v, datetime.date):
v = text_type(v)
if formatted:
v = formatdate(v)
# time
elif isinstance(v, (datetime.timedelta, datetime.datetime)):
v = text_type(v)
# long
elif isinstance(v, integer_types):
v=int(v)
# convert to strings... (if formatted)
if formatted:
if isinstance(v, float):
v=fmt_money(v)
elif isinstance(v, int):
v = text_type(v)
return v
def convert_to_lists(self, res, formatted=0, as_utf8=0):
@staticmethod
def convert_to_lists(res, formatted=0, as_utf8=0):
"""Convert tuple output to lists (internal)."""
nres = []
needs_formatting = self.needs_formatting(res, formatted)
for r in res:
nr = []
for c in r:
if needs_formatting:
val = self.convert_to_simple_type(c, formatted)
else:
val = c
if as_utf8 and type(val) is text_type:
for val in r:
if as_utf8 and isinstance(val, text_type):
val = val.encode('utf-8')
nr.append(val)
nres.append(nr)
return nres
def convert_to_utf8(self, res, formatted=0):
"""Encode result as UTF-8."""
nres = []
for r in res:
nr = []
for c in r:
if type(c) is text_type:
c = c.encode('utf-8')
nr.append(self.convert_to_simple_type(c, formatted))
nres.append(nr)
return nres
def build_conditions(self, filters):
@staticmethod
def build_conditions(filters):
"""Convert filters sent as dict, lists to SQL conditions. filter's key
is passed by map function, build conditions like:
@ -430,7 +332,7 @@ class Database:
if "[" in key:
split_key = key.split("[")
condition = "ifnull(`" + split_key[0] + "`, " + split_key[1][:-1] + ") " \
condition = "coalesce(`" + split_key[0] + "`, " + split_key[1][:-1] + ") " \
+ _operator + _rhs
else:
condition = "`" + key + "` " + _operator + _rhs
@ -527,10 +429,10 @@ class Database:
try:
out = self._get_values_from_table(fields, filters, doctype, as_dict, debug, order_by, update)
except Exception as e:
if ignore and e.args[0] in (1146, 1054):
if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)):
# table or column not found, return None
out = None
elif (not ignore) and e.args[0]==1146:
elif (not ignore) and frappe.db.is_table_missing(e):
# table not found, look in singles
out = self.get_values_from_single(fields, filters, doctype, as_dict, debug, update)
else:
@ -566,14 +468,13 @@ class Database:
return values and [values] or []
if isinstance(fields, list):
return [map(lambda d: values.get(d), fields)]
return [map(values.get, fields)]
else:
r = self.sql("""select field, value
from tabSingles where field in (%s) and doctype=%s""" \
from `tabSingles` where field in (%s) and doctype=%s"""
% (', '.join(['%s'] * len(fields)), '%s'),
tuple(fields) + (doctype,), as_dict=False, debug=debug)
# r = _cast_result(doctype, r)
if as_dict:
if r:
@ -607,10 +508,12 @@ class Database:
return dict_
def get_all(self, *args, **kwargs):
@staticmethod
def get_all(*args, **kwargs):
return frappe.get_all(*args, **kwargs)
def get_list(self, *args, **kwargs):
@staticmethod
def get_list(*args, **kwargs):
return frappe.get_list(*args, **kwargs)
def get_single_value(self, doctype, fieldname, cache=False):
@ -631,8 +534,8 @@ class Database:
if fieldname in self.value_cache[doctype]:
return self.value_cache[doctype][fieldname]
val = self.sql("""select value from
tabSingles where doctype=%s and field=%s""", (doctype, fieldname))
val = self.sql("""select `value` from
`tabSingles` where `doctype`=%s and `field`=%s""", (doctype, fieldname))
val = val[0][0] if val else None
if val=="0" or val=="1":
@ -675,8 +578,10 @@ class Database:
names = list(filter(None, names))
if names:
return dict(self.sql("select name, `%s` from `tab%s` where name in (%s)" \
% (field, doctype, ", ".join(["%s"]*len(names))), names, debug=debug))
return self.get_all(doctype,
fields=['name', field],
filters=[['name', 'in', names]],
debug=debug, as_list=1)
else:
return {}
@ -732,12 +637,12 @@ class Database:
# for singles
keys = list(to_update)
self.sql('''
delete from tabSingles
delete from `tabSingles`
where field in ({0}) and
doctype=%s'''.format(', '.join(['%s']*len(keys))),
list(keys) + [dt], debug=debug)
for key, value in iteritems(to_update):
self.sql('''insert into tabSingles(doctype, field, value) values (%s, %s, %s)''',
self.sql('''insert into `tabSingles` (doctype, field, value) values (%s, %s, %s)''',
(dt, key, value), debug=debug)
if dt in self.value_cache:
@ -745,25 +650,27 @@ class Database:
frappe.clear_document_cache(dt, dn)
def set(self, doc, field, val):
@staticmethod
def set(doc, field, val):
"""Set value in document. **Avoid**"""
doc.db_set(field, val)
def touch(self, doctype, docname):
"""Update the modified timestamp of this document."""
from frappe.utils import now
modified = now()
frappe.db.sql("""update `tab{doctype}` set `modified`=%s
self.sql("""update `tab{doctype}` set `modified`=%s
where name=%s""".format(doctype=doctype), (modified, docname))
return modified
def set_temp(self, value):
@staticmethod
def set_temp(value):
"""Set a temperory value and return a key."""
key = frappe.generate_hash()
frappe.cache().hset("temp", key, value)
return key
def get_temp(self, key):
@staticmethod
def get_temp(key):
"""Return the temperory value and delete it."""
return frappe.cache().hget("temp", key)
@ -775,20 +682,24 @@ class Database:
"""Returns a global key value."""
return self.get_default(key, user)
def set_default(self, key, val, parent="__default", parenttype=None):
"""Sets a global / user default value."""
frappe.defaults.set_default(key, val, parent, parenttype)
def add_default(self, key, val, parent="__default", parenttype=None):
"""Append a default value for a key, there can be multiple default values for a particular key."""
frappe.defaults.add_default(key, val, parent, parenttype)
def get_default(self, key, parent="__default"):
"""Returns default value as a list if multiple or single"""
d = self.get_defaults(key, parent)
return isinstance(d, list) and d[0] or d
def get_defaults(self, key=None, parent="__default"):
@staticmethod
def set_default(key, val, parent="__default", parenttype=None):
"""Sets a global / user default value."""
frappe.defaults.set_default(key, val, parent, parenttype)
@staticmethod
def add_default(key, val, parent="__default", parenttype=None):
"""Append a default value for a key, there can be multiple default values for a particular key."""
frappe.defaults.add_default(key, val, parent, parenttype)
@staticmethod
def get_defaults(key=None, parent="__default"):
"""Get all defaults"""
if key:
defaults = frappe.defaults.get_defaults(parent)
@ -800,7 +711,7 @@ class Database:
return frappe.defaults.get_defaults(parent)
def begin(self):
self.sql("start transaction")
self.sql("START TRANSACTION")
def commit(self):
"""Commit current transaction. Calls SQL `COMMIT`."""
@ -810,7 +721,8 @@ class Database:
enqueue_jobs_after_commit()
flush_local_link_count()
def flush_realtime_log(self):
@staticmethod
def flush_realtime_log():
for args in frappe.local.realtime_log:
frappe.realtime.emit_via_redis(*args)
@ -834,7 +746,7 @@ class Database:
return ("tab" + doctype) in self.get_tables()
def get_tables(self):
return [d[0] for d in self.sql("show tables")]
return [d[0] for d in self.sql("select table_name from information_schema.tables where table_schema not in ('pg_catalog', 'information_schema')")]
def a_row_exists(self, doctype):
"""Returns True if atleast one row exists."""
@ -850,7 +762,7 @@ class Database:
return True # single always exists (!)
try:
return self.get_value(dt, dn, "name", cache=cache)
except:
except Exception:
return None
elif isinstance(dt, dict) and dt.get('doctype'):
@ -858,10 +770,9 @@ class Database:
conditions = []
for d in dt:
if d == 'doctype': continue
conditions.append('`%s` = "%s"' % (d, cstr(dt[d]).replace('"', '\"')))
return self.sql('select name from `tab%s` where %s' % \
(dt['doctype'], " and ".join(conditions)))
except:
conditions.append([d, '=', dt[d]])
return self.get_all(dt['doctype'], filters=conditions, as_list=1)
except Exception:
return None
def count(self, dt, filters=None, debug=False, cache=False):
@ -872,11 +783,11 @@ class Database:
return cache_count
if filters:
conditions, filters = self.build_conditions(filters)
count = frappe.db.sql("""select count(*)
count = self.sql("""select count(*)
from `tab%s` where %s""" % (dt, conditions), filters, debug=debug)[0][0]
return count
else:
count = frappe.db.sql("""select count(*)
count = self.sql("""select count(*)
from `tab%s`""" % (dt,))[0][0]
if cache:
@ -884,58 +795,69 @@ class Database:
return count
@staticmethod
def format_date(date):
return getdate(date).strftime("%Y-%m-%d")
@staticmethod
def format_datetime(datetime):
if not datetime:
return '0001-01-01 00:00:00.000000'
if isinstance(datetime, frappe.string_types):
if ':' not in datetime:
datetime = datetime + ' 00:00:00.000000'
else:
datetime = datetime.strftime("%Y-%m-%d %H:%M:%S.%f")
return datetime
def get_creation_count(self, doctype, minutes):
"""Get count of records created in the last x minutes"""
from frappe.utils import now_datetime
from dateutil.relativedelta import relativedelta
return frappe.db.sql("""select count(name) from `tab{doctype}`
return self.sql("""select count(name) from `tab{doctype}`
where creation >= %s""".format(doctype=doctype),
now_datetime() - relativedelta(minutes=minutes))[0][0]
def get_db_table_columns(self, table):
"""Returns list of column names from given table."""
return [r[0] for r in self.sql("DESC `%s`" % table)]
return [r[0] for r in self.sql('''
select column_name
from information_schema.columns
where table_name = %s ''', table)]
def get_table_columns(self, doctype):
"""Returns list of column names from given doctype."""
return self.get_db_table_columns('tab' + doctype)
columns = self.get_db_table_columns('tab' + doctype)
if not columns:
raise self.ProgrammingError
return columns
def has_column(self, doctype, column):
"""Returns True if column exists in database."""
return column in self.get_table_columns(doctype)
def get_column_type(self, doctype, column):
return frappe.db.sql('''SELECT column_type FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'tab{0}' AND COLUMN_NAME = "{1}"'''.format(doctype, column))[0][0]
return self.sql('''SELECT column_type FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'tab{0}' AND column_name = '{1}' '''.format(doctype, column))[0][0]
def has_index(self, table_name, index_name):
pass
def add_index(self, doctype, fields, index_name=None):
"""Creates an index with given fields if not already created.
Index name will be `fieldname1_fieldname2_index`"""
if not index_name:
index_name = "_".join(fields) + "_index"
# remove index length if present e.g. (10) from index name
index_name = re.sub(r"\s*\([^)]+\)\s*", r"", index_name)
if not frappe.db.sql("""show index from `tab%s` where Key_name="%s" """ % (doctype, index_name)):
frappe.db.commit()
frappe.db.sql("""alter table `tab%s`
add index `%s`(%s)""" % (doctype, index_name, ", ".join(fields)))
pass
def add_unique(self, doctype, fields, constraint_name=None):
if isinstance(fields, string_types):
fields = [fields]
if not constraint_name:
constraint_name = "unique_" + "_".join(fields)
pass
if not frappe.db.sql("""select CONSTRAINT_NAME from information_schema.TABLE_CONSTRAINTS
where table_name=%s and constraint_type='UNIQUE' and CONSTRAINT_NAME=%s""",
('tab' + doctype, constraint_name)):
frappe.db.commit()
frappe.db.sql("""alter table `tab%s`
add unique `%s`(%s)""" % (doctype, constraint_name, ", ".join(fields)))
@staticmethod
def get_index_name(fields):
index_name = "_".join(fields) + "_index"
# remove index length if present e.g. (10) from index name
index_name = re.sub(r"\s*\([^)]+\)\s*", r"", index_name)
return index_name
def get_system_setting(self, key):
def _load_system_settings():
@ -950,20 +872,11 @@ class Database:
self._cursor = None
self._conn = None
def escape(self, s, percent=True):
@staticmethod
def escape(s, percent=True):
"""Excape quotes and percent in given string."""
# pymysql expects unicode argument to escape_string with Python 3
s = as_unicode(pymysql.escape_string(as_unicode(s)), "utf-8").replace("`", "\\`")
# NOTE separating % escape, because % escape should only be done when using LIKE operator
# or when you use python format string to generate query that already has a %s
# for example: sql("select name from `tabUser` where name=%s and {0}".format(conditions), something)
# defaulting it to True, as this is the most frequent use case
# ideally we shouldn't have to use ESCAPE and strive to pass values via the values argument of sql
if percent:
s = s.replace("%", "%%")
return s
# implemented in specific class
pass
def get_descendants(self, doctype, name):
'''Return descendants of the current record'''
@ -971,6 +884,25 @@ class Database:
return self.sql_list('''select name from `tab{doctype}`
where lft > {lft} and rgt < {rgt}'''.format(doctype=doctype, lft=lft, rgt=rgt))
def is_missing_table_or_column(self, e):
return self.is_missing_column(e) or self.is_missing_table(e)
def multisql(self, sql_dict, values=(), **kwargs):
current_dialect = frappe.conf.db_type or 'mariadb'
query = sql_dict.get(current_dialect)
return self.sql(query, values, **kwargs)
def delete(self, doctype, conditions):
if conditions:
conditions, values = self.build_conditions(conditions)
return self.sql("DELETE FROM `tab{doctype}` where {conditions}".format(
doctype=doctype,
conditions=conditions
), values)
else:
frappe.throw('No conditions provided')
def enqueue_jobs_after_commit():
if frappe.flags.enqueue_after_commit and len(frappe.flags.enqueue_after_commit) > 0:
for job in frappe.flags.enqueue_after_commit:
@ -978,3 +910,19 @@ def enqueue_jobs_after_commit():
q.enqueue_call(execute_job, timeout=job.get("timeout"),
kwargs=job.get("queue_args"))
frappe.flags.enqueue_after_commit = []
# Helpers
def _cast_result(doctype, result):
batch = [ ]
try:
for field, value in result:
df = frappe.get_meta(doctype).get_field(field)
if df:
value = cast_fieldtype(df.fieldtype, value)
batch.append(tuple([field, value]))
except frappe.exceptions.DoesNotExistError:
return result
return tuple(batch)

View file

@ -0,0 +1,88 @@
import os
import frappe
class DbManager:
def __init__(self, db):
"""
Pass root_conn here for access to all databases.
"""
if db:
self.db = db
def get_current_host(self):
return self.db.sql("select user()")[0][0].split('@')[1]
def create_user(self, user, password, host=None):
# Create user if it doesn't exist.
if not host:
host = self.get_current_host()
if password:
self.db.sql("CREATE USER '%s'@'%s' IDENTIFIED BY '%s';" % (user, host, password))
else:
self.db.sql("CREATE USER '%s'@'%s';" % (user, host))
def delete_user(self, target, host=None):
if not host:
host = self.get_current_host()
try:
self.db.sql("DROP USER '%s'@'%s';" % (target, host))
except Exception as e:
if e.args[0] == 1396:
pass
else:
raise
def create_database(self, target):
if target in self.get_database_list():
self.drop_database(target)
self.db.sql("CREATE DATABASE `%s` ;" % target)
def drop_database(self, target):
self.db.sql("DROP DATABASE IF EXISTS `%s`;" % target)
def grant_all_privileges(self, target, user, host=None):
if not host:
host = self.get_current_host()
self.db.sql("GRANT ALL PRIVILEGES ON `%s`.* TO '%s'@'%s';" % (target, user, host))
def flush_privileges(self):
self.db.sql("FLUSH PRIVILEGES")
def get_database_list(self):
"""get list of databases"""
return [d[0] for d in self.db.sql("SHOW DATABASES")]
@staticmethod
def restore_database(target, source, user, password):
from frappe.utils import make_esc
esc = make_esc('$ ')
from distutils.spawn import find_executable
pipe = find_executable('pv')
if pipe:
pipe = '{pipe} {source} |'.format(
pipe=pipe,
source=source
)
source = ''
else:
pipe = ''
source = '< {source}'.format(source=source)
if pipe:
print('Creating Database...')
command = '{pipe} mysql -u {user} -p{password} -h{host} {target} {source}'.format(
pipe=pipe,
user=esc(user),
password=esc(password),
host=esc(frappe.db.host),
target=esc(target),
source=source
)
os.system(command)

View file

View file

@ -0,0 +1,282 @@
from __future__ import unicode_literals
import frappe
import warnings
import pymysql
from pymysql.times import TimeDelta
from pymysql.constants import ER, FIELD_TYPE
from pymysql.converters import conversions
from frappe.utils import get_datetime, cstr
from markdown2 import UnicodeWithAttrs
from frappe.database.database import Database
from six import PY2, binary_type, text_type, string_types
from frappe.database.mariadb.schema import MariaDBTable
class MariaDBDatabase(Database):
ProgrammingError = pymysql.err.ProgrammingError
OperationalError = pymysql.err.OperationalError
InternalError = pymysql.err.InternalError
SQLError = pymysql.err.ProgrammingError
DataError = pymysql.err.DataError
REGEX_CHARACTER = 'regexp'
def setup_type_map(self):
self.type_map = {
'Currency': ('decimal', '18,6'),
'Int': ('int', '11'),
'Long Int': ('bigint', '20'), # convert int to bigint if length is more than 11
'Float': ('decimal', '18,6'),
'Percent': ('decimal', '18,6'),
'Check': ('int', '1'),
'Small Text': ('text', ''),
'Long Text': ('longtext', ''),
'Code': ('longtext', ''),
'Text Editor': ('longtext', ''),
'Date': ('date', ''),
'Datetime': ('datetime', '6'),
'Time': ('time', '6'),
'Text': ('text', ''),
'Data': ('varchar', self.VARCHAR_LEN),
'Link': ('varchar', self.VARCHAR_LEN),
'Dynamic Link': ('varchar', self.VARCHAR_LEN),
'Password': ('varchar', self.VARCHAR_LEN),
'Select': ('varchar', self.VARCHAR_LEN),
'Read Only': ('varchar', self.VARCHAR_LEN),
'Attach': ('text', ''),
'Attach Image': ('text', ''),
'Signature': ('longtext', ''),
'Color': ('varchar', self.VARCHAR_LEN),
'Barcode': ('longtext', ''),
'Geolocation': ('longtext', '')
}
def get_connection(self):
warnings.filterwarnings('ignore', category=pymysql.Warning)
usessl = 0
if frappe.conf.db_ssl_ca and frappe.conf.db_ssl_cert and frappe.conf.db_ssl_key:
usessl = 1
ssl_params = {
'ca':frappe.conf.db_ssl_ca,
'cert':frappe.conf.db_ssl_cert,
'key':frappe.conf.db_ssl_key
}
conversions.update({
FIELD_TYPE.NEWDECIMAL: float,
FIELD_TYPE.DATETIME: get_datetime,
UnicodeWithAttrs: conversions[text_type]
})
if PY2:
conversions.update({
TimeDelta: conversions[binary_type]
})
if usessl:
conn = pymysql.connect(self.host, self.user or '', self.password or '',
charset='utf8mb4', use_unicode = True, ssl=ssl_params,
conv = conversions, local_infile = frappe.conf.local_infile)
else:
conn = pymysql.connect(self.host, self.user or '', self.password or '',
charset='utf8mb4', use_unicode = True, conv = conversions,
local_infile = frappe.conf.local_infile)
# MYSQL_OPTION_MULTI_STATEMENTS_OFF = 1
# # self._conn.set_server_option(MYSQL_OPTION_MULTI_STATEMENTS_OFF)
if self.user != 'root':
conn.select_db(self.user)
return conn
def get_database_size(self):
''''Returns database size in MB'''
db_size = self.sql('''
SELECT `table_schema` as `database_name`,
SUM(`data_length` + `index_length`) / 1024 / 1024 AS `database_size`
FROM information_schema.tables WHERE `table_schema` = %s GROUP BY `table_schema`
''', self.db_name, as_dict=True)
return db_size[0].get('database_size')
@staticmethod
def escape(s, percent=True):
"""Excape quotes and percent in given string."""
# pymysql expects unicode argument to escape_string with Python 3
s = frappe.as_unicode(pymysql.escape_string(frappe.as_unicode(s)), "utf-8").replace("`", "\\`")
# NOTE separating % escape, because % escape should only be done when using LIKE operator
# or when you use python format string to generate query that already has a %s
# for example: sql("select name from `tabUser` where name=%s and {0}".format(conditions), something)
# defaulting it to True, as this is the most frequent use case
# ideally we shouldn't have to use ESCAPE and strive to pass values via the values argument of sql
if percent:
s = s.replace("%", "%%")
return "'" + s + "'"
# column type
@staticmethod
def is_type_number(code):
return code == pymysql.NUMBER
@staticmethod
def is_type_datetime(code):
return code in (pymysql.DATE, pymysql.DATETIME)
# exception types
@staticmethod
def is_deadlocked(e):
return e.args[0] == ER.LOCK_DEADLOCK
@staticmethod
def is_timedout(e):
return e.args[0] == ER.LOCK_WAIT_TIMEOUT
@staticmethod
def is_table_missing(e):
return e.args[0] == ER.NO_SUCH_TABLE
@staticmethod
def is_missing_column(e):
return e.args[0] == ER.BAD_FIELD_ERROR
@staticmethod
def is_duplicate_fieldname(e):
return e.args[0] == ER.DUP_FIELDNAME
@staticmethod
def is_duplicate_entry(e):
return e.args[0] == ER.DUP_ENTRY
@staticmethod
def is_access_denied( e):
return e.args[0] == ER.ACCESS_DENIED_ERROR
@staticmethod
def cant_drop_field_or_key(e):
return e.args[0] == ER.CANT_DROP_FIELD_OR_KEY
def is_primary_key_violation(self, e):
return self.is_duplicate_entry(e) and 'PRIMARY' in cstr(e.args[1])
def is_unique_key_violation(self, e):
return self.is_duplicate_entry(e) and 'Duplicate' in cstr(e.args[1])
def create_auth_table(self):
self.sql_ddl("""create table if not exists `__Auth` (
`doctype` VARCHAR(140) NOT NULL,
`name` VARCHAR(255) NOT NULL,
`fieldname` VARCHAR(140) NOT NULL,
`password` VARCHAR(255) NOT NULL,
`encrypted` INT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`doctype`, `name`, `fieldname`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci""")
def create_global_search_table(self):
if not '__global_search' in self.get_tables():
self.sql('''create table __global_search(
doctype varchar(100),
name varchar({0}),
title varchar({0}),
content text,
fulltext(content),
route varchar({0}),
published int(1) not null default 0,
unique `doctype_name` (doctype, name))
COLLATE=utf8mb4_unicode_ci
ENGINE=MyISAM
CHARACTER SET=utf8mb4'''.format(self.VARCHAR_LEN))
def create_user_settings_table(self):
self.sql_ddl("""create table if not exists __UserSettings (
`user` VARCHAR(180) NOT NULL,
`doctype` VARCHAR(180) NOT NULL,
`data` TEXT,
UNIQUE(user, doctype)
) ENGINE=InnoDB DEFAULT CHARSET=utf8""")
def create_help_table(self):
self.sql('''create table help(
path varchar(255),
content text,
title text,
intro text,
full_path text,
fulltext(title),
fulltext(content),
index (path))
COLLATE=utf8mb4_unicode_ci
ENGINE=MyISAM
CHARACTER SET=utf8mb4''')
@staticmethod
def get_on_duplicate_update(key=None):
return 'ON DUPLICATE key UPDATE '
def get_table_columns_description(self, table_name):
"""Returns list of column and its description"""
return self.sql('''select
column_name as 'name',
column_type as 'type',
column_default as 'default',
column_key = 'MUL' as 'index',
column_key = 'UNI' as 'unique'
from information_schema.columns
where table_name = '{table_name}' '''.format(table_name=table_name), as_dict=1)
def has_index(self, table_name, index_name):
return self.sql("""SHOW INDEX FROM `{table_name}`
WHERE Key_name='{index_name}'""".format(
table_name=table_name,
index_name=index_name
))
def add_index(self, doctype, fields, index_name=None):
"""Creates an index with given fields if not already created.
Index name will be `fieldname1_fieldname2_index`"""
index_name = index_name or self.get_index_name(fields)
table_name = 'tab' + doctype
if not self.has_index(table_name, index_name):
self.commit()
self.sql("""ALTER TABLE `%s`
ADD INDEX `%s`(%s)""" % (table_name, index_name, ", ".join(fields)))
def add_unique(self, doctype, fields, constraint_name=None):
if isinstance(fields, string_types):
fields = [fields]
if not constraint_name:
constraint_name = "unique_" + "_".join(fields)
if not self.sql("""select CONSTRAINT_NAME from information_schema.TABLE_CONSTRAINTS
where table_name=%s and constraint_type='UNIQUE' and CONSTRAINT_NAME=%s""",
('tab' + doctype, constraint_name)):
self.commit()
self.sql("""alter table `tab%s`
add unique `%s`(%s)""" % (doctype, constraint_name, ", ".join(fields)))
def updatedb(self, doctype, meta=None):
"""
Syncs a `DocType` to the table
* creates if required
* updates columns
* updates indices
"""
res = self.sql("select issingle from `tabDocType` where name=%s", (doctype,))
if not res:
raise Exception('Wrong doctype {0} in updatedb'.format(doctype))
if not res[0][0]:
db_table = MariaDBTable(doctype, meta)
db_table.validate()
self.commit()
db_table.sync()
self.begin()
def get_database_list(self, target):
return [d[0] for d in self.sql("SHOW DATABASES;")]

View file

@ -0,0 +1,86 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.database.schema import DBTable
class MariaDBTable(DBTable):
def create(self):
add_text = ''
# columns
column_defs = self.get_column_definitions()
if column_defs: add_text += ',\n'.join(column_defs) + ',\n'
# index
index_defs = self.get_index_definitions()
if index_defs: add_text += ',\n'.join(index_defs) + ',\n'
# create table
frappe.db.sql("""create table `%s` (
name varchar({varchar_len}) not null primary key,
creation datetime(6),
modified datetime(6),
modified_by varchar({varchar_len}),
owner varchar({varchar_len}),
docstatus int(1) not null default '0',
parent varchar({varchar_len}),
parentfield varchar({varchar_len}),
parenttype varchar({varchar_len}),
idx int(8) not null default '0',
%sindex parent(parent),
index modified(modified))
ENGINE={engine}
ROW_FORMAT=COMPRESSED
CHARACTER SET=utf8mb4
COLLATE=utf8mb4_unicode_ci""".format(varchar_len=frappe.db.VARCHAR_LEN,
engine=self.meta.get("engine") or 'InnoDB') % (self.table_name, add_text))
def alter(self):
for col in self.columns.values():
col.build_for_alter_table(self.current_columns.get(col.fieldname.lower()))
add_column_query = []
modify_column_query = []
add_index_query = []
drop_index_query = []
columns_to_modify = set(self.change_type + self.add_unique + self.set_default)
for col in self.add_column:
add_column_query.append("ADD COLUMN `{}` {}".format(col.fieldname, col.get_definition()))
for col in columns_to_modify:
modify_column_query.append("MODIFY `{}` {}".format(col.fieldname, col.get_definition()))
for col in self.add_index:
# if index key not exists
if not frappe.db.sql("SHOW INDEX FROM `%s` WHERE key_name = %s" %
(self.table_name, '%s'), col.fieldname):
add_index_query.append("ADD INDEX `{}`(`{}`)".format(col.fieldname, col.fieldname))
for col in self.drop_index:
if col.fieldname != 'name': # primary key
# if index key exists
if frappe.db.sql("""SHOW INDEX FROM `{0}`
WHERE key_name=%s
AND Non_unique=%s""".format(self.table_name), (col.fieldname, col.unique)):
drop_index_query.append("drop index `{}`".format(col.fieldname))
try:
for query_parts in [add_column_query, modify_column_query, add_index_query, drop_index_query]:
if query_parts:
query_body = ", ".join(query_parts)
query = "ALTER TABLE `{}` {}".format(self.table_name, query_body)
frappe.db.sql(query)
except Exception as e:
# sanitize
if e.args[0]==1060:
frappe.throw(str(e))
elif e.args[0]==1062:
fieldname = str(e).split("'")[-2]
frappe.throw(_("{0} field cannot be set as unique in {1}, as there are non-unique existing values".format(
fieldname, self.table_name)))
else:
raise e

View file

@ -0,0 +1,148 @@
from __future__ import unicode_literals
import frappe
import os, sys
from frappe.database.db_manager import DbManager
def setup_database(force, source_sql, verbose):
frappe.local.session = frappe._dict({'user':'Administrator'})
db_name = frappe.local.conf.db_name
root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password)
dbman = DbManager(root_conn)
if force or (db_name not in dbman.get_database_list()):
dbman.delete_user(db_name)
dbman.drop_database(db_name)
else:
raise Exception("Database %s already exists" % (db_name,))
dbman.create_user(db_name, frappe.conf.db_password)
if verbose: print("Created user %s" % db_name)
dbman.create_database(db_name)
if verbose: print("Created database %s" % db_name)
dbman.grant_all_privileges(db_name, db_name)
dbman.flush_privileges()
if verbose: print("Granted privileges to user %s and database %s" % (db_name, db_name))
# close root connection
root_conn.close()
bootstrap_database(db_name, verbose, source_sql)
def setup_help_database(help_db_name):
dbman = DbManager(get_root_connection(frappe.flags.root_login, frappe.flags.root_password))
dbman.drop_database(help_db_name)
# make database
if not help_db_name in dbman.get_database_list():
try:
dbman.create_user(help_db_name, help_db_name)
except Exception as e:
# user already exists
if e.args[0] != 1396: raise
dbman.create_database(help_db_name)
dbman.grant_all_privileges(help_db_name, help_db_name)
dbman.flush_privileges()
def drop_user_and_database(db_name, root_login, root_password):
frappe.local.db = get_root_connection(root_login, root_password)
dbman = DbManager(frappe.local.db)
dbman.delete_user(db_name)
dbman.drop_database(db_name)
def bootstrap_database(db_name, verbose, source_sql=None):
frappe.connect(db_name=db_name)
check_if_ready_for_barracuda()
import_db_from_sql(source_sql, verbose)
if not 'tabDefaultValue' in frappe.db.get_tables():
print('''Database not installed, this can due to lack of permission, or that the database name exists.
Check your mysql root password, or use --force to reinstall''')
sys.exit(1)
def import_db_from_sql(source_sql=None, verbose=False):
if verbose: print("Starting database import...")
db_name = frappe.conf.db_name
if not source_sql:
source_sql = os.path.join(os.path.dirname(__file__), 'framework_mariadb.sql')
DbManager(frappe.local.db).restore_database(db_name, source_sql, db_name, frappe.conf.db_password)
if verbose: print("Imported from database %s" % source_sql)
def check_if_ready_for_barracuda():
mariadb_variables = frappe._dict(frappe.db.sql("""show variables"""))
mariadb_minor_version = int(mariadb_variables.get('version').split('-')[0].split('.')[1])
if mariadb_minor_version < 3:
check_database(mariadb_variables, {
"innodb_file_format": "Barracuda",
"innodb_file_per_table": "ON",
"innodb_large_prefix": "ON"
})
check_database(mariadb_variables, {
"character_set_server": "utf8mb4",
"collation_server": "utf8mb4_unicode_ci"
})
def check_database(mariadb_variables, variables_dict):
mariadb_minor_version = int(mariadb_variables.get('version').split('-')[0].split('.')[1])
for key, value in variables_dict.items():
if mariadb_variables.get(key) != value:
site = frappe.local.site
msg = ("Creation of your site - {x} failed because MariaDB is not properly {sep}"
"configured to use the Barracuda storage engine. {sep}"
"Please add the settings below to MariaDB's my.cnf, restart MariaDB then {sep}"
"run `bench new-site {x}` again.{sep2}"
"").format(x=site, sep2="\n"*2, sep="\n")
if mariadb_minor_version < 3:
print_db_config(msg, expected_config_for_barracuda_2)
else:
print_db_config(msg, expected_config_for_barracuda_3)
raise frappe.exceptions.ImproperDBConfigurationError(
reason="MariaDB default file format is not Barracuda"
)
def get_root_connection(root_login, root_password):
import getpass
if not frappe.local.flags.root_connection:
if not root_login:
root_login = 'root'
if not root_password:
root_password = frappe.conf.get("root_password") or None
if not root_password:
root_password = getpass.getpass("MySQL root password: ")
frappe.local.flags.root_connection = frappe.database.get_db(user=root_login, password=root_password)
return frappe.local.flags.root_connection
def print_db_config(explanation, config_text):
print("="*80)
print(explanation)
print(config_text)
print("="*80)
expected_config_for_barracuda_2 = """
[mysqld]
innodb-file-format=barracuda
innodb-file-per-table=1
innodb-large-prefix=1
character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
[mysql]
default-character-set = utf8mb4
"""
expected_config_for_barracuda_3 = """
[mysqld]
character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
[mysql]
default-character-set = utf8mb4
"""

View file

View file

@ -0,0 +1,309 @@
from __future__ import unicode_literals
import re
import frappe
import psycopg2
import psycopg2.extensions
from six import string_types
from frappe.utils import cstr
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
from frappe.database.database import Database
from frappe.database.postgres.schema import PostgresTable
# cast decimals as floats
DEC2FLOAT = psycopg2.extensions.new_type(
psycopg2.extensions.DECIMAL.values,
'DEC2FLOAT',
lambda value, curs: float(value) if value is not None else None)
psycopg2.extensions.register_type(DEC2FLOAT)
class PostgresDatabase(Database):
ProgrammingError = psycopg2.ProgrammingError
OperationalError = psycopg2.OperationalError
InternalError = psycopg2.InternalError
SQLError = psycopg2.ProgrammingError
DataError = psycopg2.DataError
InterfaceError = psycopg2.InterfaceError
REGEX_CHARACTER = '~'
def setup_type_map(self):
self.type_map = {
'Currency': ('decimal', '18,6'),
'Int': ('bigint', None),
'Long Int': ('bigint', None), # convert int to bigint if length is more than 11
'Float': ('decimal', '18,6'),
'Percent': ('decimal', '18,6'),
'Check': ('smallint', None),
'Small Text': ('text', ''),
'Long Text': ('text', ''),
'Code': ('text', ''),
'Text Editor': ('text', ''),
'Date': ('date', ''),
'Datetime': ('timestamp', None),
'Time': ('time', '6'),
'Text': ('text', ''),
'Data': ('varchar', self.VARCHAR_LEN),
'Link': ('varchar', self.VARCHAR_LEN),
'Dynamic Link': ('varchar', self.VARCHAR_LEN),
'Password': ('varchar', self.VARCHAR_LEN),
'Select': ('varchar', self.VARCHAR_LEN),
'Read Only': ('varchar', self.VARCHAR_LEN),
'Attach': ('text', ''),
'Attach Image': ('text', ''),
'Signature': ('text', ''),
'Color': ('varchar', self.VARCHAR_LEN),
'Barcode': ('text', ''),
'Geolocation': ('text', '')
}
def get_connection(self):
# warnings.filterwarnings('ignore', category=psycopg2.Warning)
conn = psycopg2.connect('host={} dbname={}'.format(self.host, self.user))
conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) # TODO: Remove this
# conn = psycopg2.connect('host={} dbname={} user={} password={}'.format(self.host,
# self.user, self.user, self.password))
return conn
def escape(self, s, percent=True):
"""Excape quotes and percent in given string."""
if isinstance(s, bytes):
s = s.decode('utf-8')
if percent:
s = s.replace("%", "%%")
s = s.encode('utf-8')
return str(psycopg2.extensions.QuotedString(s))
def get_database_size(self):
''''Returns database size in MB'''
db_size = self.sql("SELECT (pg_database_size(%s) / 1024 / 1024) as database_size",
self.db_name, as_dict=True)
return db_size[0].get('database_size')
# pylint: disable=W0221
def sql(self, *args, **kwargs):
if len(args):
# since tuple is immutable
args = list(args)
args[0] = modify_query(args[0])
args = tuple(args)
elif kwargs.get('query'):
kwargs['query'] = modify_query(kwargs.get('query'))
return super(PostgresDatabase, self).sql(*args, **kwargs)
def get_tables(self):
return [d[0] for d in self.sql("""select table_name
from information_schema.tables
where table_catalog='{0}'
and table_type = 'BASE TABLE'
and table_schema='public'""".format(frappe.conf.db_name))]
def format_date(self, date):
if not date:
return '0001-01-01::DATE'
if isinstance(date, frappe.string_types):
if ':' not in date:
date = date + '::DATE'
else:
date = date.strftime('%Y-%m-%d') + '::DATE'
return date
# column type
@staticmethod
def is_type_number(code):
return code == psycopg2.NUMBER
@staticmethod
def is_type_datetime(code):
return code == psycopg2.DATETIME
# exception type
@staticmethod
def is_deadlocked(e):
return e.pgcode == '40P01'
@staticmethod
def is_timedout(e):
# http://initd.org/psycopg/docs/extensions.html?highlight=datatype#psycopg2.extensions.QueryCanceledError
return isinstance(e, psycopg2.extensions.QueryCanceledError)
@staticmethod
def is_table_missing(e):
return e.pgcode == '42P01'
@staticmethod
def is_missing_column(e):
return e.pgcode == '42703'
@staticmethod
def is_access_denied(e):
return e.pgcode == '42501'
@staticmethod
def cant_drop_field_or_key(e):
return e.pgcode.startswith('23')
@staticmethod
def is_duplicate_entry(e):
return e.pgcode == '23505'
@staticmethod
def is_primary_key_violation(e):
return e.pgcode == '23505' and '_pkey' in cstr(e.args[0])
@staticmethod
def is_unique_key_violation(e):
return e.pgcode == '23505' and '_key' in cstr(e.args[0])
@staticmethod
def is_duplicate_fieldname(e):
return e.pgcode == '42701'
def create_auth_table(self):
self.sql_ddl("""create table if not exists "__Auth" (
"doctype" VARCHAR(140) NOT NULL,
"name" VARCHAR(255) NOT NULL,
"fieldname" VARCHAR(140) NOT NULL,
"password" VARCHAR(255) NOT NULL,
"encrypted" INT NOT NULL DEFAULT 0,
PRIMARY KEY ("doctype", "name", "fieldname")
)""")
def create_global_search_table(self):
if not '__global_search' in self.get_tables():
self.sql('''create table "__global_search"(
doctype varchar(100),
name varchar({0}),
title varchar({0}),
content text,
route varchar({0}),
published int not null default 0,
unique (doctype, name))'''.format(self.VARCHAR_LEN))
def create_user_settings_table(self):
self.sql_ddl("""create table if not exists "__UserSettings" (
"user" VARCHAR(180) NOT NULL,
"doctype" VARCHAR(180) NOT NULL,
"data" TEXT,
UNIQUE ("user", "doctype")
)""")
def create_help_table(self):
self.sql('''CREATE TABLE "help"(
"path" varchar(255),
"content" text,
"title" text,
"intro" text,
"full_path" text)''')
self.sql('''CREATE INDEX IF NOT EXISTS "help_index" ON "help" ("path")''')
def updatedb(self, doctype, meta=None):
"""
Syncs a `DocType` to the table
* creates if required
* updates columns
* updates indices
"""
res = self.sql("select issingle from `tabDocType` where name='{}'".format(doctype))
if not res:
raise Exception('Wrong doctype {0} in updatedb'.format(doctype))
if not res[0][0]:
db_table = PostgresTable(doctype, meta)
db_table.validate()
self.commit()
db_table.sync()
self.begin()
@staticmethod
def get_on_duplicate_update(key='name'):
if isinstance(key, list):
key = '", "'.join(key)
return 'ON CONFLICT ("{key}") DO UPDATE SET '.format(
key=key
)
def check_transaction_status(self, query):
pass
def has_index(self, table_name, index_name):
return self.sql("""SELECT 1 FROM pg_indexes WHERE tablename='{table_name}'
and indexname='{index_name}' limit 1""".format(table_name=table_name, index_name=index_name))
def add_index(self, doctype, fields, index_name=None):
"""Creates an index with given fields if not already created.
Index name will be `fieldname1_fieldname2_index`"""
index_name = index_name or self.get_index_name(fields)
table_name = 'tab' + doctype
self.commit()
self.sql("""CREATE INDEX IF NOT EXISTS "{}" ON `{}`("{}")""".format(index_name, table_name, '", "'.join(fields)))
def add_unique(self, doctype, fields, constraint_name=None):
if isinstance(fields, string_types):
fields = [fields]
if not constraint_name:
constraint_name = "unique_" + "_".join(fields)
if not self.sql("""
SELECT CONSTRAINT_NAME
FROM information_schema.TABLE_CONSTRAINTS
WHERE table_name=%s
AND constraint_type='UNIQUE'
AND CONSTRAINT_NAME=%s""",
('tab' + doctype, constraint_name)):
self.commit()
self.sql("""ALTER TABLE `tab%s`
ADD CONSTRAINT %s UNIQUE (%s)""" % (doctype, constraint_name, ", ".join(fields)))
def get_table_columns_description(self, table_name):
"""Returns list of column and its description"""
# pylint: disable=W1401
return self.sql('''
SELECT a.column_name AS name,
CASE a.data_type
WHEN 'character varying' THEN CONCAT('varchar(', a.character_maximum_length ,')')
WHEN 'timestamp without TIME zone' THEN 'timestamp'
ELSE a.data_type
END AS type,
COUNT(b.indexdef) AS Index,
COALESCE(a.column_default, NULL) AS default,
BOOL_OR(b.unique) AS unique
FROM information_schema.columns a
LEFT JOIN
(SELECT indexdef, tablename, indexdef LIKE '%UNIQUE INDEX%' AS unique
FROM pg_indexes
WHERE tablename='{table_name}') b
ON SUBSTRING(b.indexdef, '\(.*\)') LIKE CONCAT('%', a.column_name, '%')
WHERE a.table_name = '{table_name}'
GROUP BY a.column_name, a.data_type, a.column_default, a.character_maximum_length;'''
.format(table_name=table_name), as_dict=1)
def get_database_list(self, target):
return [d[0] for d in self.sql("SELECT datname FROM pg_database;")]
def modify_query(query):
""""Modifies query according to the requirements of postgres"""
# replace ` with " for definitions
query = query.replace('`', '"')
query = replace_locate_with_strpos(query)
# select from requires ""
if re.search('from tab', query, flags=re.IGNORECASE):
query = re.sub('from tab([a-zA-Z]*)', r'from "tab\1"', query, flags=re.IGNORECASE)
return query
def replace_locate_with_strpos(query):
# strpos is the locate equivalent in postgres
if re.search(r'locate\(', query, flags=re.IGNORECASE):
query = re.sub(r'locate\(([^,]+),([^)]+)\)', r'strpos(\2, \1)', query, flags=re.IGNORECASE)
return query

View file

@ -0,0 +1,279 @@
-- Core Elements to install WNFramework
-- To be called from install.py
--
-- Table structure for table "tabDocField"
--
DROP TABLE IF EXISTS "tabDocField";
CREATE TABLE "tabDocField" (
"name" varchar(255) NOT NULL,
"creation" timestamp(6) DEFAULT NULL,
"modified" timestamp(6) DEFAULT NULL,
"modified_by" varchar(255) DEFAULT NULL,
"owner" varchar(255) DEFAULT NULL,
"docstatus" smallint NOT NULL DEFAULT 0,
"parent" varchar(255) DEFAULT NULL,
"parentfield" varchar(255) DEFAULT NULL,
"parenttype" varchar(255) DEFAULT NULL,
"idx" bigint NOT NULL DEFAULT 0,
"fieldname" varchar(255) DEFAULT NULL,
"label" varchar(255) DEFAULT NULL,
"oldfieldname" varchar(255) DEFAULT NULL,
"fieldtype" varchar(255) DEFAULT NULL,
"oldfieldtype" varchar(255) DEFAULT NULL,
"options" text,
"search_index" smallint NOT NULL DEFAULT 0,
"hidden" smallint NOT NULL DEFAULT 0,
"set_only_once" smallint NOT NULL DEFAULT 0,
"allow_in_quick_entry" smallint NOT NULL DEFAULT 0,
"print_hide" smallint NOT NULL DEFAULT 0,
"report_hide" smallint NOT NULL DEFAULT 0,
"reqd" smallint NOT NULL DEFAULT 0,
"bold" smallint NOT NULL DEFAULT 0,
"in_global_search" smallint NOT NULL DEFAULT 0,
"collapsible" smallint NOT NULL DEFAULT 0,
"unique" smallint NOT NULL DEFAULT 0,
"no_copy" smallint NOT NULL DEFAULT 0,
"allow_on_submit" smallint NOT NULL DEFAULT 0,
"trigger" varchar(255) DEFAULT NULL,
"collapsible_depends_on" text,
"depends_on" text,
"permlevel" bigint NOT NULL DEFAULT 0,
"ignore_user_permissions" smallint NOT NULL DEFAULT 0,
"width" varchar(255) DEFAULT NULL,
"print_width" varchar(255) DEFAULT NULL,
"columns" bigint NOT NULL DEFAULT 0,
"default" text,
"description" text,
"in_list_view" smallint NOT NULL DEFAULT 0,
"in_standard_filter" smallint NOT NULL DEFAULT 0,
"read_only" smallint NOT NULL DEFAULT 0,
"precision" varchar(255) DEFAULT NULL,
"length" bigint NOT NULL DEFAULT 0,
"translatable" smallint NOT NULL DEFAULT 0,
PRIMARY KEY ("name")
) ;
create index on "tabDocField" ("parent");
create index on "tabDocField" ("label");
create index on "tabDocField" ("fieldtype");
create index on "tabDocField" ("fieldname");
--
-- Table structure for table "tabDocPerm"
--
DROP TABLE IF EXISTS "tabDocPerm";
CREATE TABLE "tabDocPerm" (
"name" varchar(255) NOT NULL,
"creation" timestamp(6) DEFAULT NULL,
"modified" timestamp(6) DEFAULT NULL,
"modified_by" varchar(255) DEFAULT NULL,
"owner" varchar(255) DEFAULT NULL,
"docstatus" smallint NOT NULL DEFAULT 0,
"parent" varchar(255) DEFAULT NULL,
"parentfield" varchar(255) DEFAULT NULL,
"parenttype" varchar(255) DEFAULT NULL,
"idx" bigint NOT NULL DEFAULT 0,
"permlevel" bigint DEFAULT '0',
"role" varchar(255) DEFAULT NULL,
"match" varchar(255) DEFAULT NULL,
"read" smallint NOT NULL DEFAULT 1,
"write" smallint NOT NULL DEFAULT 1,
"create" smallint NOT NULL DEFAULT 1,
"submit" smallint NOT NULL DEFAULT 0,
"cancel" smallint NOT NULL DEFAULT 0,
"delete" smallint NOT NULL DEFAULT 1,
"amend" smallint NOT NULL DEFAULT 0,
"report" smallint NOT NULL DEFAULT 1,
"export" smallint NOT NULL DEFAULT 1,
"import" smallint NOT NULL DEFAULT 0,
"share" smallint NOT NULL DEFAULT 1,
"print" smallint NOT NULL DEFAULT 1,
"email" smallint NOT NULL DEFAULT 1,
PRIMARY KEY ("name")
) ;
create index on "tabDocPerm" ("parent");
--
-- Table structure for table "tabDocType"
--
DROP TABLE IF EXISTS "tabDocType";
CREATE TABLE "tabDocType" (
"name" varchar(255) NOT NULL DEFAULT '',
"creation" timestamp(6) DEFAULT NULL,
"modified" timestamp(6) DEFAULT NULL,
"modified_by" varchar(255) DEFAULT NULL,
"owner" varchar(255) DEFAULT NULL,
"docstatus" smallint NOT NULL DEFAULT 0,
"parent" varchar(255) DEFAULT NULL,
"parentfield" varchar(255) DEFAULT NULL,
"parenttype" varchar(255) DEFAULT NULL,
"idx" bigint NOT NULL DEFAULT 0,
"search_fields" varchar(255) DEFAULT NULL,
"issingle" smallint NOT NULL DEFAULT 0,
"istable" smallint NOT NULL DEFAULT 0,
"editable_grid" smallint NOT NULL DEFAULT 1,
"track_changes" smallint NOT NULL DEFAULT 0,
"module" varchar(255) DEFAULT NULL,
"restrict_to_domain" varchar(255) DEFAULT NULL,
"app" varchar(255) DEFAULT NULL,
"autoname" varchar(255) DEFAULT NULL,
"name_case" varchar(255) DEFAULT NULL,
"title_field" varchar(255) DEFAULT NULL,
"image_field" varchar(255) DEFAULT NULL,
"timeline_field" varchar(255) DEFAULT NULL,
"sort_field" varchar(255) DEFAULT NULL,
"sort_order" varchar(255) DEFAULT NULL,
"description" text,
"colour" varchar(255) DEFAULT NULL,
"read_only" smallint NOT NULL DEFAULT 0,
"in_create" smallint NOT NULL DEFAULT 0,
"menu_index" bigint DEFAULT NULL,
"parent_node" varchar(255) DEFAULT NULL,
"smallicon" varchar(255) DEFAULT NULL,
"allow_copy" smallint NOT NULL DEFAULT 0,
"allow_rename" smallint NOT NULL DEFAULT 0,
"allow_import" smallint NOT NULL DEFAULT 0,
"hide_toolbar" smallint NOT NULL DEFAULT 0,
"hide_heading" smallint NOT NULL DEFAULT 0,
"track_seen" smallint NOT NULL DEFAULT 0,
"max_attachments" bigint NOT NULL DEFAULT 0,
"print_outline" varchar(255) DEFAULT NULL,
"read_only_onload" smallint NOT NULL DEFAULT 0,
"document_type" varchar(255) DEFAULT NULL,
"icon" varchar(255) DEFAULT NULL,
"color" varchar(255) DEFAULT NULL,
"tag_fields" varchar(255) DEFAULT NULL,
"subject" varchar(255) DEFAULT NULL,
"_last_update" varchar(32) DEFAULT NULL,
"engine" varchar(20) DEFAULT 'InnoDB',
"default_print_format" varchar(255) DEFAULT NULL,
"is_submittable" smallint NOT NULL DEFAULT 0,
"show_name_in_global_search" smallint NOT NULL DEFAULT 0,
"_user_tags" varchar(255) DEFAULT NULL,
"custom" smallint NOT NULL DEFAULT 0,
"beta" smallint NOT NULL DEFAULT 0,
"image_view" smallint NOT NULL DEFAULT 0,
"has_web_view" smallint NOT NULL DEFAULT 0,
"allow_guest_to_view" smallint NOT NULL DEFAULT 0,
"route" varchar(255) DEFAULT NULL,
"is_published_field" varchar(255) DEFAULT NULL,
PRIMARY KEY ("name")
) ;
--
-- Table structure for table "tabSeries"
--
DROP TABLE IF EXISTS "tabSeries";
CREATE TABLE "tabSeries" (
"name" varchar(100) DEFAULT NULL,
"current" bigint NOT NULL DEFAULT 0
) ;
create index on "tabSeries" ("name");
--
-- Table structure for table "tabSessions"
--
DROP TABLE IF EXISTS "tabSessions";
CREATE TABLE "tabSessions" (
"user" varchar(255) DEFAULT NULL,
"sid" varchar(255) DEFAULT NULL,
"sessiondata" text,
"ipaddress" varchar(16) DEFAULT NULL,
"lastupdate" timestamp(6) DEFAULT NULL,
"device" varchar(255) DEFAULT 'desktop',
"status" varchar(20) DEFAULT NULL
);
create index on "tabSessions" ("sid");
--
-- Table structure for table "tabSingles"
--
DROP TABLE IF EXISTS "tabSingles";
CREATE TABLE "tabSingles" (
"doctype" varchar(255) DEFAULT NULL,
"field" varchar(255) DEFAULT NULL,
"value" text
);
create index on "tabSingles" ("doctype", "field");
--
-- Table structure for table "__Auth"
--
DROP TABLE IF EXISTS "__Auth";
CREATE TABLE "__Auth" (
"doctype" VARCHAR(140) NOT NULL,
"name" VARCHAR(255) NOT NULL,
"fieldname" VARCHAR(140) NOT NULL,
"password" VARCHAR(255) NOT NULL,
"encrypted" int NOT NULL DEFAULT 0,
PRIMARY KEY ("doctype", "name", "fieldname")
);
create index on "__Auth" ("doctype", "name", "fieldname");
--
-- Table structure for table "tabFile"
--
DROP TABLE IF EXISTS "tabFile";
CREATE TABLE "tabFile" (
"name" varchar(255) NOT NULL,
"creation" timestamp(6) DEFAULT NULL,
"modified" timestamp(6) DEFAULT NULL,
"modified_by" varchar(255) DEFAULT NULL,
"owner" varchar(255) DEFAULT NULL,
"docstatus" smallint NOT NULL DEFAULT 0,
"parent" varchar(255) DEFAULT NULL,
"parentfield" varchar(255) DEFAULT NULL,
"parenttype" varchar(255) DEFAULT NULL,
"idx" bigint NOT NULL DEFAULT 0,
"file_name" varchar(255) DEFAULT NULL,
"file_url" varchar(255) DEFAULT NULL,
"module" varchar(255) DEFAULT NULL,
"attached_to_name" varchar(255) DEFAULT NULL,
"file_size" bigint NOT NULL DEFAULT 0,
"attached_to_doctype" varchar(255) DEFAULT NULL,
PRIMARY KEY ("name")
);
create index on "tabFile" ("parent");
create index on "tabFile" ("attached_to_name");
create index on "tabFile" ("attached_to_doctype");
--
-- Table structure for table "tabDefaultValue"
--
DROP TABLE IF EXISTS "tabDefaultValue";
CREATE TABLE "tabDefaultValue" (
"name" varchar(255) NOT NULL,
"creation" timestamp(6) DEFAULT NULL,
"modified" timestamp(6) DEFAULT NULL,
"modified_by" varchar(255) DEFAULT NULL,
"owner" varchar(255) DEFAULT NULL,
"docstatus" smallint NOT NULL DEFAULT 0,
"parent" varchar(255) DEFAULT NULL,
"parentfield" varchar(255) DEFAULT NULL,
"parenttype" varchar(255) DEFAULT NULL,
"idx" bigint NOT NULL DEFAULT 0,
"defvalue" text,
"defkey" varchar(255) DEFAULT NULL,
PRIMARY KEY ("name")
);
create index on "tabDefaultValue" ("parent");
create index on "tabDefaultValue" ("parent", "defkey");

View file

@ -0,0 +1,96 @@
import frappe
from frappe import _
from frappe.utils import cint, flt
from frappe.database.schema import DBTable, get_definition
class PostgresTable(DBTable):
def create(self):
add_text = ''
# columns
column_defs = self.get_column_definitions()
if column_defs: add_text += ',\n'.join(column_defs)
# index
# index_defs = self.get_index_definitions()
# TODO: set docstatus length
# create table
frappe.db.sql("""create table `%s` (
name varchar({varchar_len}) not null primary key,
creation timestamp(6),
modified timestamp(6),
modified_by varchar({varchar_len}),
owner varchar({varchar_len}),
docstatus smallint not null default '0',
parent varchar({varchar_len}),
parentfield varchar({varchar_len}),
parenttype varchar({varchar_len}),
idx bigint not null default '0',
%s)""".format(varchar_len=frappe.db.VARCHAR_LEN) % (self.table_name, add_text))
frappe.db.commit()
def alter(self):
for col in self.columns.values():
col.build_for_alter_table(self.current_columns.get(col.fieldname.lower()))
query = []
for col in self.add_column:
query.append("ADD COLUMN `{}` {}".format(col.fieldname, col.get_definition()))
for col in self.change_type:
query.append("ALTER COLUMN `{}` TYPE {}".format(col.fieldname, get_definition(col.fieldtype, precision=col.precision, length=col.length)))
for col in self.set_default:
if col.fieldname=="name":
continue
if col.fieldtype in ("Check", "Int"):
col_default = cint(col.default)
elif col.fieldtype in ("Currency", "Float", "Percent"):
col_default = flt(col.default)
elif not col.default:
col_default = "NULL"
else:
col_default = "{}".format(frappe.db.escape(col.default))
query.append("ALTER COLUMN `{}` SET DEFAULT {}".format(col.fieldname, col_default))
create_index_query = ""
for col in self.add_index:
# if index key not exists
create_index_query += 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format(
index_name=col.fieldname,
table_name=self.table_name,
field=col.fieldname)
drop_index_query = ""
for col in self.drop_index:
# primary key
if col.fieldname != 'name':
# if index key exists
if not frappe.db.has_index(self.table_name, col.fieldname):
drop_index_query += 'DROP INDEX IF EXISTS "{}" ;'.format(col.fieldname)
if query:
try:
final_alter_query = "ALTER TABLE `{}` {}".format(self.table_name, ", ".join(query))
if final_alter_query: frappe.db.sql(final_alter_query)
if create_index_query: frappe.db.sql(create_index_query)
if drop_index_query: frappe.db.sql(drop_index_query)
except Exception as e:
# sanitize
if frappe.db.is_duplicate_fieldname(e):
frappe.throw(str(e))
elif frappe.db.is_duplicate_entry(e):
fieldname = str(e).split("'")[-2]
frappe.throw(_("""{0} field cannot be set as unique in {1},
as there are non-unique existing values""".format(
fieldname, self.table_name)))
raise e
else:
raise e

View file

@ -0,0 +1,41 @@
import frappe, subprocess, os
def setup_database(force, source_sql, verbose):
root_conn = get_root_connection()
root_conn.commit()
root_conn.sql("DROP DATABASE IF EXISTS `{0}`".format(frappe.conf.db_name))
root_conn.sql("DROP USER IF EXISTS {0}".format(frappe.conf.db_name))
root_conn.sql("CREATE DATABASE `{0}`".format(frappe.conf.db_name))
root_conn.sql("CREATE user {0} password '{1}'".format(frappe.conf.db_name,
frappe.conf.db_password))
root_conn.sql("GRANT ALL PRIVILEGES ON DATABASE `{0}` TO {0}".format(frappe.conf.db_name))
# bootstrap db
subprocess.check_output(['psql', frappe.conf.db_name, '-qf',
os.path.join(os.path.dirname(__file__), 'framework_postgres.sql')])
frappe.connect()
def setup_help_database(help_db_name):
root_conn = get_root_connection()
root_conn.sql("DROP DATABASE IF EXISTS `{0}`".format(help_db_name))
root_conn.sql("DROP USER IF EXISTS {0}".format(help_db_name))
root_conn.sql("CREATE DATABASE `{0}`".format(help_db_name))
root_conn.sql("CREATE user {0} password '{1}'".format(help_db_name, help_db_name))
root_conn.sql("GRANT ALL PRIVILEGES ON DATABASE `{0}` TO {0}".format(help_db_name))
def get_root_connection(root_login='postgres', root_password=None):
import getpass
if not frappe.local.flags.root_connection:
if not root_login:
root_login = 'root'
if not root_password:
root_password = frappe.conf.get("root_password") or None
if not root_password:
root_password = getpass.getpass("Postgres root password: ")
frappe.local.flags.root_connection = frappe.database.get_db(user=root_login, password=root_password)
return frappe.local.flags.root_connection

339
frappe/database/schema.py Normal file
View file

@ -0,0 +1,339 @@
from __future__ import unicode_literals
import re
import frappe
from frappe import _
from frappe.utils import cstr, cint, flt
class InvalidColumnName(frappe.ValidationError): pass
class DBTable:
def __init__(self, doctype, meta=None):
self.doctype = doctype
self.table_name = 'tab{}'.format(doctype)
self.meta = meta or frappe.get_meta(doctype)
self.columns = {}
self.current_columns = {}
# lists for change
self.add_column = []
self.change_type = []
self.change_name = []
self.add_unique = []
self.add_index = []
self.drop_index = []
self.set_default = []
# load
self.get_columns_from_docfields()
def sync(self):
if self.is_new():
self.create()
else:
self.alter()
def create(self):
pass
def get_column_definitions(self):
column_list = [] + frappe.db.DEFAULT_COLUMNS
ret = []
for k in list(self.columns):
if k not in column_list:
d = self.columns[k].get_definition()
if d:
ret.append('`'+ k + '` ' + d)
column_list.append(k)
return ret
def get_index_definitions(self):
ret = []
for key, col in self.columns.items():
if (col.set_index
and not col.unique
and col.fieldtype in frappe.db.type_map
and frappe.db.type_map.get(col.fieldtype)[0]
not in ('text', 'longtext')):
ret.append('index `' + key + '`(`' + key + '`)')
return ret
def get_columns_from_docfields(self):
"""
get columns from docfields and custom fields
"""
fl = frappe.db.sql("SELECT * FROM `tabDocField` WHERE parent = %s", self.doctype, as_dict = 1)
lengths = {}
precisions = {}
uniques = {}
# optional fields like _comments
if not self.meta.istable:
for fieldname in frappe.db.OPTIONAL_COLUMNS:
fl.append({
"fieldname": fieldname,
"fieldtype": "Text"
})
# add _seen column if track_seen
if getattr(self.meta, 'track_seen', False):
fl.append({
'fieldname': '_seen',
'fieldtype': 'Text'
})
if (not frappe.flags.in_install_db
and (frappe.flags.in_install != "frappe"
or frappe.flags.ignore_in_install)):
custom_fl = frappe.db.sql("""
SELECT * FROM `tabCustom Field`
WHERE dt = %s AND docstatus < 2
""", (self.doctype,), as_dict=1)
if custom_fl: fl += custom_fl
# apply length, precision and unique from property setters
for ps in frappe.get_all("Property Setter",
fields=["field_name", "property", "value"],
filters={
"doc_type": self.doctype,
"doctype_or_field": "DocField",
"property": ["in", ["precision", "length", "unique"]]
}):
if ps.property=="length":
lengths[ps.field_name] = cint(ps.value)
elif ps.property=="precision":
precisions[ps.field_name] = cint(ps.value)
elif ps.property=="unique":
uniques[ps.field_name] = cint(ps.value)
for f in fl:
self.columns[f['fieldname']] = DbColumn(self,
f['fieldname'],
f['fieldtype'],
lengths.get(f["fieldname"]) or f.get('length'),
f.get('default'),
f.get('search_index'),
f.get('options'),
uniques.get(f["fieldname"],
f.get('unique')),
precisions.get(f['fieldname']) or f.get('precision'))
def validate(self):
"""Check if change in varchar length isn't truncating the columns"""
if self.is_new():
return
self.setup_table_columns()
columns = [frappe._dict({"fieldname": f, "fieldtype": "Data"}) for f in
frappe.db.STANDARD_VARCHAR_COLUMNS]
columns += self.columns.values()
for col in columns:
if len(col.fieldname) >= 64:
frappe.throw(_("Fieldname is limited to 64 characters ({0})")
.format(frappe.bold(col.fieldname)))
if 'varchar' in frappe.db.type_map.get(col.fieldtype, ()):
# validate length range
new_length = cint(col.length) or cint(frappe.db.VARCHAR_LEN)
if not (1 <= new_length <= 1000):
frappe.throw(_("Length of {0} should be between 1 and 1000").format(col.fieldname))
current_col = self.current_columns.get(col.fieldname, {})
if not current_col:
continue
current_type = self.current_columns[col.fieldname]["type"]
current_length = re.findall(r'varchar\(([\d]+)\)', current_type)
if not current_length:
# case when the field is no longer a varchar
continue
current_length = current_length[0]
if cint(current_length) != cint(new_length):
try:
# check for truncation
max_length = frappe.db.sql("""SELECT MAX(CHAR_LENGTH(`{fieldname}`)) FROM `tab{doctype}`"""
.format(fieldname=col.fieldname, doctype=self.doctype))
except frappe.db.InternalError as e:
if frappe.db.is_missing_column(e):
# Unknown column 'column_name' in 'field list'
continue
else:
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))
def is_new(self):
return self.table_name not in frappe.db.get_tables()
def setup_table_columns(self):
# TODO: figure out a way to get key data
for c in frappe.db.get_table_columns_description(self.table_name):
self.current_columns[c.name.lower()] = c
def alter(self):
pass
class DbColumn:
def __init__(self, table, fieldname, fieldtype, length, default,
set_index, options, unique, precision):
self.table = table
self.fieldname = fieldname
self.fieldtype = fieldtype
self.length = length
self.set_index = set_index
self.default = default
self.options = options
self.unique = unique
self.precision = precision
def get_definition(self, with_default=1):
column_def = get_definition(self.fieldtype, precision=self.precision, length=self.length)
if not column_def:
return column_def
if self.fieldtype in ("Check", "Int"):
default_value = cint(self.default) or 0
column_def += ' not null default {0}'.format(default_value)
elif self.fieldtype in ("Currency", "Float", "Percent"):
default_value = flt(self.default) or 0
column_def += ' not null default {0}'.format(default_value)
elif self.default and (self.default not in frappe.db.DEFAULT_SHORTCUTS) \
and not self.default.startswith(":") and column_def not in ('text', 'longtext'):
column_def += " default {}".format(frappe.db.escape(self.default))
if self.unique and (column_def not in ('text', 'longtext')):
column_def += ' unique'
return column_def
def build_for_alter_table(self, current_def):
column_type = get_definition(self.fieldtype, self.precision, self.length)
# no columns
if not column_type:
return
# to add?
if not current_def:
self.fieldname = validate_column_name(self.fieldname)
self.table.add_column.append(self)
return
# type
if ((current_def['type']) != column_type):
self.table.change_type.append(self)
# unique
if((self.unique and not current_def['unique']) and column_type not in ('text', 'longtext')):
self.table.add_unique.append(self)
# default
if (self.default_changed(current_def)
and (self.default not in frappe.db.DEFAULT_SHORTCUTS)
and not cstr(self.default).startswith(":")
and not (column_type in ['text','longtext'])):
self.table.set_default.append(self)
# index should be applied or dropped irrespective of type change
if ((current_def['index'] and not self.set_index and not self.unique)
or (current_def['unique'] and not self.unique)):
# to drop unique you have to drop index
self.table.drop_index.append(self)
elif (not current_def['index'] and self.set_index) and not (column_type in ('text', 'longtext')):
self.table.add_index.append(self)
def default_changed(self, current_def):
if "decimal" in current_def['type']:
return self.default_changed_for_decimal(current_def)
else:
return current_def['default'] != self.default
def default_changed_for_decimal(self, current_def):
try:
if current_def['default'] in ("", None) and self.default in ("", None):
# both none, empty
return False
elif current_def['default'] in ("", None):
try:
# check if new default value is valid
float(self.default)
return True
except ValueError:
return False
elif self.default in ("", None):
# new default value is empty
return True
else:
# NOTE float() raise ValueError when "" or None is passed
return float(current_def['default'])!=float(self.default)
except TypeError:
return True
def validate_column_name(n):
special_characters = re.findall(r"[\W]", n, re.UNICODE)
if special_characters:
special_characters = ", ".join('"{0}"'.format(c) for c in special_characters)
frappe.throw(_("Fieldname {0} cannot have special characters like {1}").format(
frappe.bold(cstr(n)), special_characters), frappe.db.InvalidColumnName)
return n
def validate_column_length(fieldname):
if len(fieldname) > frappe.db.MAX_COLUMN_LENGTH:
frappe.throw(_("Fieldname is limited to 64 characters ({0})").format(fieldname))
def get_definition(fieldtype, precision=None, length=None):
d = frappe.db.type_map.get(fieldtype)
# convert int to long int if the length of the int is greater than 11
if fieldtype == "Int" and length and length > 11:
d = frappe.db.type_map.get("Long Int")
if not d: return
coltype = d[0]
size = d[1] if d[1] else None
if size:
if fieldtype in ["Float", "Currency", "Percent"] and cint(precision) > 6:
size = '21,9'
if coltype == "varchar" and length:
size = length
if size is not None:
coltype = "{coltype}({size})".format(coltype=coltype, size=size)
return coltype
def add_column(doctype, column_name, fieldtype, precision=None):
if column_name in frappe.db.get_table_columns(doctype):
# already exists
return
frappe.db.commit()
frappe.db.sql("alter table `tab%s` add column %s %s" % (doctype,
column_name, get_definition(fieldtype, precision)))

View file

@ -115,7 +115,7 @@ def set_default(key, value, parent, parenttype="__default"):
select
defkey
from
tabDefaultValue
`tabDefaultValue`
where
defkey=%s and parent=%s
for update''', (key, parent)):

View file

@ -46,7 +46,7 @@ def get_events(doctype, start, end, field_map, filters=None, fields=None):
if field_map.color:
fields.append(field_map.color)
start_date = "ifnull(%s, '0000-00-00 00:00:00')" % field_map.start
start_date = "ifnull(%s, '0001-01-01 00:00:00')" % field_map.start
end_date = "ifnull(%s, '2199-12-31 00:00:00')" % field_map.end
filters += [

View file

@ -32,7 +32,7 @@ frappe.ui.form.on('Auto Repeat', {
if(frm.doc.docstatus == 1) {
let label = __('View {0}', [frm.doc.reference_doctype]);
let label = __('View {0}', [__(frm.doc.reference_doctype)]);
frm.add_custom_button(__(label),
function() {
frappe.route_options = {
@ -51,14 +51,14 @@ frappe.ui.form.on('Auto Repeat', {
}
if(frm.doc.status == 'Stopped') {
frm.add_custom_button(__("Resume"),
frm.add_custom_button(__("Restart"),
function() {
frm.events.stop_resume_auto_repeat(frm, "Resumed");
}
);
}
if(frm.doc.status!= 0){
if(frm.doc.docstatus!= 0 && !frm.doc.status.includes('Stopped', 'Cancelled') && frm.doc.next_schedule_date >= frappe.datetime.get_today()){
frappe.auto_repeat.render_schedule(frm);
}
}
@ -107,10 +107,31 @@ frappe.ui.form.on('Auto Repeat', {
}
}
});
},
preview_message: function(frm) {
if (frm.doc.message) {
frappe.call({
method: "frappe.desk.doctype.auto_repeat.auto_repeat.generate_message_preview",
args: {
reference_dt: frm.doc.reference_doctype,
reference_doc: frm.doc.reference_document,
subject: frm.doc.subject,
message: frm.doc.message
},
callback: function(r) {
if(r.message) {
frappe.msgprint(r.message.message, r.message.subject)
}
}
});
} else {
frappe.msgprint(__("Please setup a message first"), __("Message not setup"))
}
}
});
frappe.auto_repeat.render_schedule = function(frm) {
frappe.auto_repeat.render_schedule = function(frm) {
frappe.call({
method: "get_auto_repeat_schedule",
doc: frm.doc

View file

@ -143,6 +143,72 @@
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reference_party_doctype",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Reference Party Doctype",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "reference_party",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Reference Party",
"length": 0,
"no_copy": 0,
"options": "reference_party_doctype",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
@ -563,7 +629,7 @@
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
@ -595,7 +661,73 @@
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "notify_by_email",
"fieldname": "recipients",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Recipients",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval: doc.notify_by_email",
"fieldname": "get_contacts",
"fieldtype": "Button",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Get Contacts",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
@ -629,7 +761,7 @@
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
@ -663,12 +795,14 @@
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_17",
"fieldtype": "Column Break",
"default": "Please find attached {{ doc.doctype }} #{{ doc.name }}",
"depends_on": "eval: doc.notify_by_email",
"fieldname": "message",
"fieldtype": "Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@ -676,6 +810,7 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Message",
"length": 0,
"no_copy": 0,
"permlevel": 0,
@ -694,45 +829,12 @@
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "notify_by_email",
"fieldname": "recipients",
"fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Recipients",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval: doc.notify_by_email",
"fieldname": "get_contacts",
"fieldname": "preview_message",
"fieldtype": "Button",
"hidden": 0,
"ignore_user_permissions": 0,
@ -741,7 +843,7 @@
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Get Contacts",
"label": "Preview Message",
"length": 0,
"no_copy": 0,
"permlevel": 0,
@ -791,72 +893,6 @@
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"depends_on": "eval:doc.notify_by_email",
"fieldname": "section_break_20",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Message",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Please find attached {{ doc.doctype }} #{{ doc.name }}",
"fieldname": "message",
"fieldtype": "Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Message",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
@ -892,7 +928,7 @@
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
@ -965,8 +1001,8 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-07-13 15:14:34.524098",
"modified_by": "Administrator",
"modified": "2018-09-12 17:17:36.071411",
"modified_by": "faris@erpnext.com",
"module": "Desk",
"name": "Auto Repeat",
"name_case": "",
@ -1039,5 +1075,6 @@
"sort_order": "DESC",
"title_field": "reference_document",
"track_changes": 1,
"track_seen": 0
"track_seen": 0,
"track_views": 0
}

View file

@ -26,6 +26,7 @@ class AutoRepeat(Document):
self.validate_dates()
self.validate_next_schedule_date()
self.validate_email_id()
self.link_party()
validate_template(self.subject or "")
validate_template(self.message or "")
@ -124,6 +125,14 @@ class AutoRepeat(Document):
return schedule_details
def link_party(self):
reference = frappe.get_meta(self.reference_doctype)
for field in reference.fields:
if field.options in ['Customer', 'Supplier', 'Employee']:
self.reference_party_doctype = field.options
self.reference_party = frappe.db.get_value(self.reference_doctype, self.reference_document, field.fieldname)
break
def get_next_schedule_date(start_date, frequency, repeat_on_day):
mcount = month_map.get(frequency)
if mcount:
@ -376,3 +385,13 @@ def update_reference(docname, reference):
except Exception as e:
raise e
return "error"
@frappe.whitelist()
def generate_message_preview(reference_dt, reference_doc, message=None, subject=None):
doc = frappe.get_doc(reference_dt, reference_doc)
subject_preview = _("Please add a subject to your email")
msg_preview = frappe.render_template(message, {'doc': doc})
if subject:
subject_preview = frappe.render_template(subject, {'doc': doc})
return {'message': msg_preview, 'subject': subject_preview}

View file

@ -20,7 +20,7 @@ def add_custom_fields():
class TestAutoRepeat(unittest.TestCase):
def setUp(self):
if not frappe.db.sql('select name from `tabCustom Field` where name="auto_repeat"'):
if not frappe.db.sql("SELECT `name` FROM `tabCustom Field` WHERE `name`='auto_repeat'"):
add_custom_fields()
def test_daily_auto_repeat(self):

View file

@ -32,9 +32,8 @@ class Event(Document):
def get_permission_query_conditions(user):
if not user: user = frappe.session.user
return """(tabEvent.event_type='Public' or tabEvent.owner='%(user)s')""" % {
return """(`tabEvent`.`event_type`='Public' or `tabEvent`.`owner`=%(user)s)""" % {
"user": frappe.db.escape(user),
"roles": "', '".join([frappe.db.escape(r) for r in frappe.get_roles(user)])
}
def has_permission(doc, user):
@ -72,28 +71,26 @@ def get_events(start, end, user=None, for_reminder=False, filters=None):
user = frappe.session.user
if isinstance(filters, string_types):
filters = json.loads(filters)
roles = frappe.get_roles(user)
events = frappe.db.sql("""select name, subject, description, color,
events = frappe.db.sql("""select `name`, subject, description, color,
starts_on, ends_on, owner, all_day, event_type, repeat_this_event, repeat_on,repeat_till,
monday, tuesday, wednesday, thursday, friday, saturday, sunday
from tabEvent where ((
from `tabEvent` where ((
(date(starts_on) between date(%(start)s) and date(%(end)s))
or (date(ends_on) between date(%(start)s) and date(%(end)s))
or (date(starts_on) <= date(%(start)s) and date(ends_on) >= date(%(end)s))
) or (
date(starts_on) <= date(%(start)s) and repeat_this_event=1 and
ifnull(repeat_till, "3000-01-01") > date(%(start)s)
coalesce(repeat_till, '3000-01-01') > date(%(start)s)
))
{reminder_condition}
{filter_condition}
and (event_type='Public' or owner=%(user)s
or exists(select name from `tabDocShare` where
tabDocShare.share_doctype="Event" and `tabDocShare`.share_name=tabEvent.name
and tabDocShare.user=%(user)s))
`tabDocShare`.share_doctype='Event' and `tabDocShare`.share_name=`tabEvent`.`name`
and `tabDocShare`.`user`=%(user)s))
order by starts_on""".format(
filter_condition=get_filters_cond('Event', filters, []),
reminder_condition="and ifnull(send_reminder,0)=1" if for_reminder else "",
roles=", ".join('"{}"'.format(frappe.db.escape(r)) for r in roles)
reminder_condition="and coalesce(send_reminder, 0)=1" if for_reminder else ""
), {
"start": start,
"end": end,

View file

@ -29,7 +29,7 @@ def get_permission_query_conditions(user):
if user == "Administrator":
return ""
return """(`tabKanban Board`.private=0 or `tabKanban Board`.owner="{user}")""".format(user=user)
return """(`tabKanban Board`.private=0 or `tabKanban Board`.owner='{user}')""".format(user=user)
def has_permission(doc, ptype, user):
if doc.private == 0 or user == "Administrator":

View file

@ -16,8 +16,14 @@ class ToDo(Document):
def validate(self):
self._assignment = None
if self.is_new():
if self.assigned_by == self.owner:
assignment_message = frappe._("{0} self assigned this task: {1}").format(get_fullname(self.assigned_by), self.description)
else:
assignment_message = frappe._("{0} assigned {1}: {2}").format(get_fullname(self.assigned_by), get_fullname(self.owner), self.description)
self._assignment = {
"text": frappe._("{0} assigned {1}: {2}").format(get_fullname(self.assigned_by), get_fullname(self.owner), self.description),
"text": assignment_message,
"comment_type": "Assigned"
}
@ -66,12 +72,12 @@ class ToDo(Document):
"_assign", json.dumps(assignments), update_modified=False)
except Exception as e:
if e.args[0] == 1146 and frappe.flags.in_install:
if frappe.db.is_table_missing(e) and frappe.flags.in_install:
# no table
return
elif e.args[0]==1054:
from frappe.model.db_schema import add_column
elif frappe.db.is_column_missing(e):
from frappe.database.schema import add_column
add_column(self.reference_type, "_assign", "Text")
self.update_in_reference()
@ -88,7 +94,7 @@ def get_permission_query_conditions(user):
if "System Manager" in frappe.get_roles(user):
return None
else:
return """(tabToDo.owner = '{user}' or tabToDo.assigned_by = '{user}')"""\
return """(tabToDo.owner = {user} or tabToDo.assigned_by = {user})"""\
.format(user=frappe.db.escape(user))
def has_permission(doc, user):

View file

@ -18,9 +18,13 @@ def get(args=None):
get_docinfo(frappe.get_doc(args.get("doctype"), args.get("name")))
return frappe.db.sql("""select owner, description from `tabToDo`
where reference_type=%(doctype)s and reference_name=%(name)s and status="Open"
order by modified desc limit 5""", args, as_dict=True)
return frappe.db.sql("""SELECT `owner`, `description`
FROM `tabToDo`
WHERE reference_type=%(doctype)s
AND reference_name=%(name)s
AND status='Open'
ORDER BY modified DESC
LIMIT 5""", args, as_dict=True)
@frappe.whitelist()
def add(args=None):
@ -36,9 +40,12 @@ def add(args=None):
if not args:
args = frappe.local.form_dict
if frappe.db.sql("""select owner from `tabToDo`
where reference_type=%(doctype)s and reference_name=%(name)s and status="Open"
and owner=%(assign_to)s""", args):
if frappe.db.sql("""SELECT `owner`
FROM `tabToDo`
WHERE `reference_type`=%(doctype)s
AND `reference_name`=%(name)s
AND `status`='Open'
AND `owner`=%(assign_to)s""", args):
frappe.throw(_("Already in user's To Do list"), DuplicateToDoError)
else:
@ -47,6 +54,9 @@ def add(args=None):
# if args.get("re_assign"):
# remove_from_todo_if_already_assigned(args['doctype'], args['name'])
if not args.get('description'):
args['description'] = _('Assignment')
d = frappe.get_doc({
"doctype":"ToDo",
"owner": args['assign_to'],
@ -160,4 +170,30 @@ def notify_assignment(assigned_by, owner, doc_type, doc_name, action='CLOSE',
'notify': notify
}
arg["parenttype"] = "Assignment"
if arg and arg.get("notify"):
_notify(arg)
def _notify(args):
from frappe.utils import get_fullname, get_url
args = frappe._dict(args)
contact = args.contact
txt = args.txt
try:
if not isinstance(contact, list):
contact = [frappe.db.get_value("User", contact, "email") or contact]
frappe.sendmail(\
recipients=contact,
sender= frappe.db.get_value("User", frappe.session.user, "email"),
subject=_("New message from {0}").format(get_fullname(frappe.session.user)),
template="new_message",
args={
"from": get_fullname(frappe.session.user),
"message": txt,
"link": get_url()
},
header=[_('New Message'), 'orange'])
except frappe.OutgoingEmailError:
pass

View file

@ -138,24 +138,22 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields=
group_by=None, as_dict=True):
'''Returns list of communications for a given document'''
if not fields:
fields = '''name, communication_type,
communication_medium, comment_type, communication_date,
content, sender, sender_full_name, creation, subject, delivery_status, _liked_by,
timeline_doctype, timeline_name,
reference_doctype, reference_name,
link_doctype, link_name, read_by_recipient,
rating, "Communication" as doctype'''
fields = '''`name`, `communication_type`,`communication_medium`, `comment_type`,
`communication_date`, `content`, `sender`, `sender_full_name`,
`creation`, `subject`, `delivery_status`, `_liked_by`,
`timeline_doctype`, `timeline_name`, `reference_doctype`, `reference_name`,
`link_doctype`, `link_name`, `read_by_recipient`, `rating` '''
conditions = '''communication_type in ("Communication", "Comment", "Feedback")
conditions = '''communication_type in ('Communication', 'Comment', 'Feedback')
and (
(reference_doctype=%(doctype)s and reference_name=%(name)s)
or (
(timeline_doctype=%(doctype)s and timeline_name=%(name)s)
and (
communication_type="Communication"
communication_type='Communication'
or (
communication_type="Comment"
and comment_type in ("Created", "Updated", "Submitted", "Cancelled", "Deleted")
communication_type='Comment'
and comment_type in ('Created', 'Updated', 'Submitted', 'Cancelled', 'Deleted')
)))
)'''
@ -165,12 +163,12 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields=
conditions+= ' and creation > {0}'.format(after)
if doctype=='User':
conditions+= ' and not (reference_doctype="User" and communication_type="Communication")'
conditions+= " and not (reference_doctype='User' and communication_type='Communication')"
communications = frappe.db.sql("""select {fields}
from tabCommunication
from `tabCommunication`
where {conditions} {group_by}
order by creation desc limit %(start)s, %(limit)s""".format(
order by creation desc LIMIT %(limit)s OFFSET %(start)s""".format(
fields = fields, conditions=conditions, group_by=group_by or ""),
{ "doctype": doctype, "name": name, "start": frappe.utils.cint(start), "limit": limit },
as_dict=as_dict)
@ -178,8 +176,8 @@ def get_communication_data(doctype, name, start=0, limit=20, after=None, fields=
return communications
def get_assignments(dt, dn):
cl = frappe.db.sql("""select name, owner, description from `tabToDo`
where reference_type=%(doctype)s and reference_name=%(name)s and status="Open"
cl = frappe.db.sql("""select `name`, owner, description from `tabToDo`
where reference_type=%(doctype)s and reference_name=%(name)s and status='Open'
order by modified desc limit 5""", {
"doctype": dt,
"name": dn

View file

@ -31,8 +31,7 @@ def validate_link():
frappe.response['message'] = 'Ok'
return
valid_value = frappe.db.sql("select name from `tab%s` where name=%s" % (frappe.db.escape(options),
'%s'), (value,))
valid_value = frappe.db.get_all(options, filters=dict(name=value), as_list=1, limit=1)
if valid_value:
valid_value = valid_value[0][0]

View file

@ -6,7 +6,7 @@ from __future__ import unicode_literals
"""Allow adding of likes to documents"""
import frappe, json
from frappe.model.db_schema import add_column
from frappe.database.schema import add_column
from frappe import _
from frappe.utils import get_link_to_form
@ -54,8 +54,8 @@ def _toggle_like(doctype, name, add, user=None):
frappe.db.set_value(doctype, name, "_liked_by", json.dumps(liked_by), update_modified=False)
except Exception as e:
if isinstance(e.args, (tuple, list)) and e.args and e.args[0]==1054:
except frappe.db.ProgrammingError as e:
if frappe.db.is_column_missing(e):
add_column(doctype, "_liked_by", "Text")
_toggle_like(doctype, name, add, user)
else:

View file

@ -224,7 +224,7 @@ def get_last_modified(doctype):
try:
last_modified = frappe.get_all(doctype, fields=["max(modified)"], as_list=True, limit_page_length=1)[0][0]
except Exception as e:
if e.args[0]==1146:
if frappe.db.is_table_missing(e):
last_modified = None
else:
raise

View file

@ -1,5 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe
@ -10,9 +10,6 @@ from frappe.utils import cint
import frappe.defaults
from six import text_type
# imports - third-party imports
import pymysql
def get_sql_tables(q):
if q.find('WHERE') != -1:
tl = q.split('FROM')[1].split('WHERE')[0].split(',')
@ -78,16 +75,16 @@ def add_match_conditions(q, tl):
q = q[0] + condition_st + '(' + ' OR '.join(sl) + ') ' + condition_end + q[1]
else:
q = q + condition_st + '(' + ' OR '.join(sl) + ')'
return q
def guess_type(m):
"""
Returns fieldtype depending on the MySQLdb Description
"""
if m in pymysql.NUMBER:
if frappe.db.is_type_number(m):
return 'Currency'
elif m in pymysql.DATE:
elif m in frappe.is_type_datetime(m):
return 'Date'
else:
return 'Data'
@ -97,7 +94,7 @@ def build_description_simple():
for m in frappe.db.get_description():
colnames.append(m[0])
coltypes.append(guess_type[m[0]])
coltypes.append(guess_type[m[1]])
coloptions.append('')
colwidths.append('100')
@ -178,7 +175,7 @@ def runquery(q='', ret=0, from_export=0):
meta = get_sql_meta(tl)
q = add_match_conditions(q, tl)
# replace special variables
q = q.replace('__user', frappe.session.user)
q = q.replace('__today', frappe.utils.nowdate())
@ -264,9 +261,9 @@ def add_limit_to_query(query, args):
if args.get('limit_page_length'):
query += """
limit %(limit_start)s, %(limit_page_length)s"""
import frappe.utils
args['limit_start'] = frappe.utils.cint(args.get('limit_start'))
args['limit_page_length'] = frappe.utils.cint(args.get('limit_page_length'))
return query, args

View file

@ -260,13 +260,14 @@ def add_total_row(result, columns, meta = None):
total_row = [""]*len(columns)
has_percent = []
for i, col in enumerate(columns):
fieldtype, options = None, None
fieldtype, options, fieldname = None, None, None
if isinstance(col, string_types):
if meta:
# get fieldtype from the meta
field = meta.get_field(col)
if field:
fieldtype = meta.get_field(col).fieldtype
fieldname = meta.get_field(col).fieldname
else:
col = col.split(":")
if len(col) > 1:
@ -278,19 +279,21 @@ def add_total_row(result, columns, meta = None):
fieldtype = "Data"
else:
fieldtype = col.get("fieldtype")
fieldname = col.get("fieldname")
options = col.get("options")
for row in result:
if fieldtype in ["Currency", "Int", "Float", "Percent"] and flt(row[i]):
total_row[i] = flt(total_row[i]) + flt(row[i])
cell = row.get(fieldname) if isinstance(row, dict) else row[i]
if fieldtype in ["Currency", "Int", "Float", "Percent"] and flt(cell):
total_row[i] = flt(total_row[i]) + flt(cell)
if fieldtype == "Percent" and i not in has_percent:
has_percent.append(i)
if fieldtype == "Time" and row[i]:
if fieldtype == "Time" and cell:
if not total_row[i]:
total_row[i]=timedelta(hours=0,minutes=0,seconds=0)
total_row[i] = total_row[i] + row[i]
total_row[i] = total_row[i] + cell
if fieldtype=="Link" and options == "Currency":

View file

@ -11,9 +11,6 @@ from frappe.model.db_query import DatabaseQuery
from frappe import _
from six import text_type, string_types, StringIO
# imports - third-party imports
import pymysql
@frappe.whitelist()
@frappe.read_only()
def get():
@ -235,11 +232,11 @@ def delete_items():
@frappe.whitelist()
@frappe.read_only()
def get_sidebar_stats(stats, doctype, filters=[]):
cat_tags = frappe.db.sql("""select tag.parent as category, tag.tag_name as tag
from `tabTag Doc Category` as docCat
INNER JOIN tabTag as tag on tag.parent = docCat.parent
where docCat.tagdoc=%s
ORDER BY tag.parent asc,tag.idx""",doctype,as_dict=1)
cat_tags = frappe.db.sql("""select `tag`.parent as `category`, `tag`.tag_name as `tag`
from `tabTag Doc Category` as `docCat`
INNER JOIN `tabTag` as `tag` on `tag`.parent = `docCat`.parent
where `docCat`.tagdoc=%s
ORDER BY `tag`.parent asc, `tag`.idx""", doctype, as_dict=1)
return {"defined_cat":cat_tags, "stats":get_stats(stats, doctype, filters)}
@ -254,7 +251,7 @@ def get_stats(stats, doctype, filters=[]):
try:
columns = frappe.db.get_table_columns(doctype)
except pymysql.InternalError:
except frappe.db.InternalError:
# raised when _user_tags column is added on the fly
columns = []
@ -273,10 +270,10 @@ def get_stats(stats, doctype, filters=[]):
else:
stats[tag] = tagcount
except frappe.SQLError:
except frappe.db.SQLError:
# does not work for child tables
pass
except pymysql.InternalError:
except frappe.db.InternalError:
# raised when _user_tags column is added on the fly
pass
return stats

View file

@ -20,7 +20,7 @@ def sanitize_searchfield(searchfield):
if len(searchfield) == 1:
# do not allow special characters to pass as searchfields
regex = re.compile('^.*[=;*,\'"$\-+%#@()_].*')
regex = re.compile(r'^.*[=;*,\'"$\-+%#@()_].*')
if regex.match(searchfield):
_raise_exception(searchfield)
@ -43,7 +43,7 @@ def sanitize_searchfield(searchfield):
_raise_exception(searchfield)
else:
regex = re.compile('^.*[=;*,\'"$\-+%#@()].*')
regex = re.compile(r'^.*[=;*,\'"$\-+%#@()].*')
if any(regex.match(f) for f in searchfield.split()):
_raise_exception(searchfield)
@ -126,14 +126,15 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
formatted_fields = ['`tab%s`.`%s`' % (meta.name, f.strip()) for f in fields]
# find relevance as location of search term from the beginning of string `name`. used for sorting results.
formatted_fields.append("""locate("{_txt}", `tab{doctype}`.`name`) as `_relevance`""".format(
_txt=frappe.db.escape((txt or "").replace("%", "")), doctype=frappe.db.escape(doctype)))
formatted_fields.append("""locate({_txt}, `tab{doctype}`.`name`) as `_relevance`""".format(
_txt=frappe.db.escape((txt or "").replace("%", "")), doctype=doctype))
# In order_by, `idx` gets second priority, because it stores link count
from frappe.model.db_query import get_order_by
order_by_based_on_meta = get_order_by(doctype, meta)
order_by = "if(_relevance, _relevance, 99999), {0}, `tab{1}`.idx desc".format(order_by_based_on_meta, doctype)
# 2 is the index of _relevance column
order_by = "2 , {0}, `tab{1}`.idx desc".format(order_by_based_on_meta, doctype)
ignore_permissions = True if doctype == "DocType" else (cint(ignore_user_permissions) and has_permission(doctype))

View file

@ -33,7 +33,7 @@ def check_user_tags(dt):
try:
frappe.db.sql("select `_user_tags` from `tab%s` limit 1" % dt)
except Exception as e:
if e.args[0] == 1054:
if frappe.db.is_column_missing(e):
DocTags(dt).setup()
@frappe.whitelist()
@ -62,11 +62,11 @@ def get_tags(doctype, txt, cat_tags):
try:
for _user_tags in frappe.db.sql_list("""select DISTINCT `_user_tags`
from `tab{0}`
where _user_tags like '%{1}%'
limit 50""".format(frappe.db.escape(doctype), frappe.db.escape(txt))):
where _user_tags like '{1}'
limit 50""".format(doctype, frappe.db.escape('%' + txt + '%'))):
tags.extend(_user_tags[1:].split(","))
except Exception as e:
if e.args[0]!=1054: raise
if not frappe.db.is_column_missing(e): raise
return sorted(filter(lambda t: t and txt.lower() in t.lower(), list(set(tags))))
class DocTags:
@ -112,7 +112,7 @@ class DocTags:
doc= frappe.get_doc(self.dt, dn)
update_global_search(doc)
except Exception as e:
if e.args[0]==1054:
if frappe.db.is_column_missing(e):
if not tags:
# no tags, nothing to do
return
@ -123,5 +123,5 @@ class DocTags:
def setup(self):
"""adds the _user_tags column if not exists"""
from frappe.model.db_schema import add_column
from frappe.database.schema import add_column
add_column(self.dt, "_user_tags", "Data")

View file

@ -26,7 +26,7 @@ def get_contact_list(txt, page_length=20):
where name like %(txt)s
%(condition)s
limit %(page_length)s
""", {'txt': "%%%s%%" % frappe.db.escape(txt),
""", {'txt': frappe.db.escape('%' + txt + '%'),
'condition': match_conditions, 'page_length': page_length}, as_dict=True)
out = filter(None, out)

View file

@ -342,9 +342,10 @@ class EmailAccount(Document):
raise SentEmailInInbox
if email.message_id:
names = frappe.db.sql("""select distinct name from tabCommunication
where message_id='{message_id}'
order by creation desc limit 1""".format(
# https://stackoverflow.com/a/18367248
names = frappe.db.sql("""SELECT DISTINCT `name`, `creation` FROM `tabCommunication`
WHERE `message_id`='{message_id}'
ORDER BY `creation` DESC LIMIT 1""".format(
message_id=email.message_id
), as_dict=True)
@ -462,7 +463,7 @@ class EmailAccount(Document):
# try and match by subject and sender
# if sent by same sender with same subject,
# append it to old coversation
subject = frappe.as_unicode(strip(re.sub("(^\s*(fw|fwd|wg)[^:]*:|\s*(re|aw)[^:]*:\s*)*",
subject = frappe.as_unicode(strip(re.sub(r"(^\s*(fw|fwd|wg)[^:]*:|\s*(re|aw)[^:]*:\s*)*",
"", email.subject, 0, flags=re.IGNORECASE)))
parent = frappe.db.get_all(self.append_to, filters={
@ -604,7 +605,7 @@ class EmailAccount(Document):
return
flags = frappe.db.sql("""select name, communication, uid, action from
`tabEmail Flag Queue` where is_completed=0 and email_account='{email_account}'
`tabEmail Flag Queue` where is_completed=0 and email_account={email_account}
""".format(email_account=frappe.db.escape(self.name)), as_dict=True)
uid_list = { flag.get("uid", None): flag.get("action", "Read") for flag in flags }

View file

@ -45,13 +45,13 @@ class TestEmailAccount(unittest.TestCase):
comm = frappe.get_doc("Communication", {"sender": "test_sender@example.com"})
comm.db_set("creation", datetime.now() - timedelta(seconds = 30 * 60))
frappe.db.sql("delete from `tabEmail Queue`")
frappe.db.sql("DELETE FROM `tabEmail Queue`")
notify_unreplied()
self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": comm.reference_doctype,
"reference_name": comm.reference_name, "status":"Not Sent"}))
def test_incoming_with_attach(self):
frappe.db.sql("delete from tabCommunication where sender='test_sender@example.com'")
frappe.db.sql("DELETE FROM `tabCommunication` WHERE sender='test_sender@example.com'")
existing_file = frappe.get_doc({'doctype': 'File', 'file_name': 'erpnext-conf-14.png'})
frappe.delete_doc("File", existing_file.name)
existing_file.delete_file_from_filesystem()

View file

@ -252,12 +252,21 @@ def get_list_context(context=None):
def get_newsletter_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"):
email_group_list = frappe.db.sql('''select eg.name from `tabEmail Group` eg, `tabEmail Group Member` egm
where egm.unsubscribed=0 and eg.name=egm.email_group and egm.email = %s''', frappe.session.user)
if email_group_list:
return frappe.db.sql('''select n.name, n.subject, n.message, n.modified
from `tabNewsletter` n, `tabNewsletter Email Group` neg
where n.name = neg.parent and n.email_sent=1 and n.published=1 and neg.email_group in %s
order by n.modified desc limit {0}, {1}
'''.format(limit_start, limit_page_length), [email_group_list], as_dict=1)
email_group_list = frappe.db.sql('''SELECT eg.name
FROM `tabEmail Group` eg, `tabEmail Group Member` egm
WHERE egm.unsubscribed=0
AND eg.name=egm.email_group
AND egm.email = %s''', frappe.session.user)
email_group_list = [d[0] for d in email_group_list]
if email_group_list:
return frappe.db.sql('''SELECT n.name, n.subject, n.message, n.modified
FROM `tabNewsletter` n, `tabNewsletter Email Group` neg
WHERE n.name = neg.parent
AND n.email_sent=1
AND n.published=1
AND neg.email_group in ({0})
ORDER BY n.modified DESC LIMIT {1} OFFSET {2}
'''.format(','.join(['%s'] * len(email_group_list)),
limit_page_length, limit_start), email_group_list, as_dict=1)

View file

@ -8,16 +8,12 @@ import json, os
from frappe import _
from frappe.model.document import Document
from frappe.core.doctype.role.role import get_emails_from_role
from frappe.utils import validate_email_add, nowdate, parse_val, is_html
from frappe.utils import validate_email_add, nowdate, parse_val, is_html, add_to_date
from frappe.utils.jinja import validate_template
from frappe.modules.utils import export_module_json, get_doc_module
from six import string_types
from frappe.integrations.doctype.slack_webhook_url.slack_webhook_url import send_slack_message
# imports - third-party imports
import pymysql
from pymysql.constants import ER
class Notification(Document):
def onload(self):
'''load message'''
@ -91,11 +87,19 @@ def get_context(context):
if self.event=="Days After":
diff_days = -diff_days
for name in frappe.db.sql_list("""select name from `tab{0}` where
DATE(`{1}`) = ADDDATE(DATE(%s), INTERVAL %s DAY)""".format(self.document_type,
self.date_changed), (nowdate(), diff_days or 0)):
reference_date = add_to_date(nowdate(), days=diff_days)
reference_date_start = reference_date + ' 00:00:00.000000'
reference_date_end = reference_date + ' 23:59:59.000000'
doc = frappe.get_doc(self.document_type, name)
doc_list = frappe.get_all(self.document_type,
fields='name',
filters=[
{ self.date_changed: ('>=', reference_date_start) },
{ self.date_changed: ('<=', reference_date_end) }
])
for d in doc_list:
doc = frappe.get_doc(self.document_type, d.name)
if self.condition and not frappe.safe_eval(self.condition, None, get_context(doc)):
continue
@ -246,9 +250,14 @@ def trigger_notifications(doc, method=None):
return
if method == "daily":
for alert in frappe.db.sql_list("""select name from `tabNotification`
where event in ('Days Before', 'Days After') and enabled=1"""):
alert = frappe.get_doc("Notification", alert)
doc_list = frappe.get_all('Notification',
filters={
'event': ('in', ('Days Before', 'Days After')),
'enabled': 1
})
for d in doc_list:
alert = frappe.get_doc("Notification", d.name)
for doc in alert.get_documents_for_today():
evaluate_alert(doc, alert, alert.event)
frappe.db.commit()
@ -268,12 +277,13 @@ def evaluate_alert(doc, alert, event):
if event=="Value Change" and not doc.is_new():
try:
db_value = frappe.db.get_value(doc.doctype, doc.name, alert.value_changed)
except pymysql.InternalError as e:
if e.args[0]== ER.BAD_FIELD_ERROR:
except Exception as e:
if frappe.db.is_missing_column(e):
alert.db_set('enabled', 0)
frappe.log_error('Notification {0} has been disabled due to missing field'.format(alert.name))
return
else:
raise
db_value = parse_val(db_value)
if (doc.get(alert.value_changed) == db_value) or \
(not db_value and not doc.get(alert.value_changed)):

View file

@ -252,12 +252,12 @@ def check_email_limit(recipients):
EmailLimitCrossedError)
def get_emails_sent_this_month():
return frappe.db.sql("""select count(name) from `tabEmail Queue` where
status='Sent' and MONTH(creation)=MONTH(CURDATE())""")[0][0]
return frappe.db.sql("""SELECT COUNT(`name`) FROM `tabEmail Queue` WHERE
`status`='Sent' AND EXTRACT(MONTH FROM `creation`) = EXTRACT(MONTH FROM NOW())""")[0][0]
def get_emails_sent_today():
return frappe.db.sql("""select count(name) from `tabEmail Queue` where
status='Sent' and creation>DATE_SUB(NOW(), INTERVAL 24 HOUR)""")[0][0]
return frappe.db.sql("""SELECT COUNT(`name`) FROM `tabEmail Queue` WHERE
`status`='Sent' AND `creation` > (NOW() - INTERVAL '24' HOUR)""")[0][0]
def get_unsubscribe_message(unsubscribe_message, expose_recipients):
if unsubscribe_message:
@ -483,8 +483,8 @@ def prepare_message(email, recipient, recipients_list):
return ""
# Parse "Email Account" from "Email Sender"
email_account = email.sender.rsplit('<',1)[0]
if frappe.conf.use_ssl and frappe.db.get_value("Email Account", {"name": email_account}, "track_email_status"):
email_account = get_outgoing_email_account(raise_exception_not_set=False, sender=email.sender)
if frappe.conf.use_ssl and email_account.track_email_status:
# Using SSL => Publically available domain => Email Read Reciept Possible
message = message.replace("<!--email open check-->", quopri.encodestring('<img src="https://{}/api/method/frappe.core.doctype.communication.email.mark_email_as_seen?name={}"/>'.format(frappe.local.site, email.communication).encode()).decode())
else:
@ -554,17 +554,21 @@ def clear_outbox():
Note: Used separate query to avoid deadlock
"""
email_queues = frappe.db.sql_list("""select name from `tabEmail Queue`
where priority=0 and datediff(now(), modified) > 31""")
email_queues = frappe.db.sql_list("""SELECT `name` FROM `tabEmail Queue`
WHERE `priority`=0 AND `modified` < (NOW() - INTERVAL '31' DAY)""")
if email_queues:
frappe.db.sql("""delete from `tabEmail Queue` where name in (%s)"""
% ','.join(['%s']*len(email_queues)), tuple(email_queues))
frappe.db.sql("""DELETE FROM `tabEmail Queue` WHERE `name` IN ({0})""".format(
','.join(['%s']*len(email_queues)
)), tuple(email_queues))
frappe.db.sql("""delete from `tabEmail Queue Recipient` where parent in (%s)"""
% ','.join(['%s']*len(email_queues)), tuple(email_queues))
frappe.db.sql("""DELETE FROM `tabEmail Queue Recipient` WHERE `parent` IN ({0})""".format(
','.join(['%s']*len(email_queues)
)), tuple(email_queues))
frappe.db.sql("""
update `tabEmail Queue`
set status='Expired'
where datediff(curdate(), modified) > 7 and status='Not Sent' and (send_after is null or send_after < %(now)s)""", { 'now': now_datetime() })
UPDATE `tabEmail Queue`
SET `status`='Expired'
WHERE `modified` < (NOW() - INTERVAL '7' DAY)
AND `status`='Not Sent'
AND (`send_after` IS NULL OR `send_after` < %(now)s)""", { 'now': now_datetime() })

View file

@ -6,10 +6,6 @@ from __future__ import unicode_literals
# BEWARE don't put anything in this file except exceptions
from werkzeug.exceptions import NotFound
# imports - third-party imports
from pymysql import ProgrammingError as SQLError, Error
# from pymysql import OperationalError as DatabaseOperationalError
class ValidationError(Exception):
http_status_code = 417
@ -46,7 +42,7 @@ class Redirect(Exception):
class CSRFTokenError(Exception):
http_status_code = 400
class ImproperDBConfigurationError(Error):
class ImproperDBConfigurationError(Exception):
"""
Used when frappe detects that database or tables are not properly
configured
@ -84,3 +80,4 @@ class RetryBackgroundJobError(Exception): pass
class DocumentLockedError(ValidationError): pass
class CircularLinkingError(ValidationError): pass
class SecurityException(Exception): pass
class InvalidColumnName(ValidationError): pass

View file

@ -282,8 +282,6 @@ class FrappeClient(object):
return params
def post_process(self, response):
response.raise_for_status()
try:
rjson = response.json()
except ValueError:

View file

@ -43,6 +43,7 @@
"currency": "EUR",
"currency_fraction": "Cent",
"currency_fraction_units": 100,
"smallest_currency_fraction_value": 0.01,
"currency_symbol": "\u20ac",
"number_format": "#,###.##",
"timezones": [
@ -176,6 +177,7 @@
"currency": "EUR",
"currency_fraction": "Cent",
"currency_fraction_units": 100,
"smallest_currency_fraction_value": 0.01,
"currency_symbol": "\u20ac",
"number_format": "#,###.##",
"timezones": [
@ -252,6 +254,7 @@
"currency": "EUR",
"currency_fraction": "Cent",
"currency_fraction_units": 100,
"smallest_currency_fraction_value": 0.01,
"currency_symbol": "\u20ac",
"number_format": "#,###.##",
"timezones": [
@ -860,6 +863,7 @@
"currency": "EUR",
"currency_fraction": "Cent",
"currency_fraction_units": 100,
"smallest_currency_fraction_value": 0.01,
"currency_symbol": "\u20ac",
"number_format": "#,###.##",
"timezones": [
@ -871,6 +875,7 @@
"currency": "EUR",
"currency_fraction": "Cent",
"currency_fraction_units": 100,
"smallest_currency_fraction_value": 0.01,
"currency_symbol": "\u20ac",
"number_format": "# ###,##",
"date_format": "dd/mm/yyyy",
@ -937,6 +942,7 @@
"currency": "EUR",
"currency_fraction": "Cent",
"currency_fraction_units": 100,
"smallest_currency_fraction_value": 0.01,
"currency_symbol": "\u20ac",
"number_format": "#,###.##",
"timezones": [
@ -971,6 +977,7 @@
"currency": "EUR",
"currency_fraction": "Cent",
"currency_fraction_units": 100,
"smallest_currency_fraction_value": 0.01,
"currency_symbol": "\u20ac",
"number_format": "#,###.##",
"timezones": [
@ -1195,6 +1202,7 @@
"currency": "EUR",
"currency_fraction": "Cent",
"currency_fraction_units": 100,
"smallest_currency_fraction_value": 0.01,
"currency_symbol": "\u20ac",
"number_format": "#,###.##",
"timezones": [
@ -1228,6 +1236,7 @@
"currency": "EUR",
"currency_fraction": "Cent",
"currency_fraction_units": 100,
"smallest_currency_fraction_value": 0.01,
"currency_symbol": "\u20ac",
"number_format": "#.###,##",
"date_format": "dd/mm/yyyy",
@ -1463,6 +1472,7 @@
"currency": "EUR",
"currency_fraction": "Cent",
"currency_fraction_units": 100,
"smallest_currency_fraction_value": 0.01,
"currency_symbol": "\u20ac",
"number_format": "#,###.##",
"timezones": [
@ -1642,6 +1652,7 @@
"currency": "EUR",
"currency_fraction": "Cent",
"currency_fraction_units": 100,
"smallest_currency_fraction_value": 0.01,
"currency_symbol": "\u20ac",
"number_format": "#,###.##",
"timezones": [
@ -1668,6 +1679,7 @@
"currency": "EUR",
"currency_fraction": "Cent",
"currency_fraction_units": 100,
"smallest_currency_fraction_value": 0.01,
"currency_symbol": "\u20ac",
"number_format": "#,###.##",
"timezones": [
@ -1756,6 +1768,7 @@
"currency": "EUR",
"currency_fraction": "Cent",
"currency_fraction_units": 100,
"smallest_currency_fraction_value": 0.01,
"currency_symbol": "\u20ac",
"number_format": "#,###.##",
"timezones": [
@ -1978,6 +1991,7 @@
"currency": "EUR",
"currency_fraction": "Cent",
"currency_fraction_units": 100,
"smallest_currency_fraction_value": 0.01,
"currency_symbol": "\u20ac",
"number_format": "#,###.##",
"timezones": [
@ -2110,6 +2124,7 @@
"currency": "EUR",
"currency_fraction": "Cent",
"currency_fraction_units": 100,
"smallest_currency_fraction_value": 0.01,
"currency_symbol": "\u20ac",
"number_format": "#,###.##",
"timezones": [
@ -2203,6 +2218,7 @@
"currency": "EUR",
"currency_fraction": "Cent",
"currency_fraction_units": 100,
"smallest_currency_fraction_value": 0.01,
"currency_symbol": "\u20ac",
"number_format": "#,###.##",
"timezones": [
@ -2214,6 +2230,7 @@
"currency": "EUR",
"currency_fraction": "Cent",
"currency_fraction_units": 100,
"smallest_currency_fraction_value": 0.01,
"currency_symbol": "\u20ac",
"number_format": "#,###.##",
"timezones": [
@ -2279,6 +2296,7 @@
"currency": "EUR",
"currency_fraction": "Cent",
"currency_fraction_units": 100,
"smallest_currency_fraction_value": 0.01,
"currency_symbol": "\u20ac",
"number_format": "#,###.##",
"timezones": [

View file

@ -1,193 +1,208 @@
{
"allow_copy": 0,
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:country_name",
"beta": 0,
"creation": "2013-01-19 10:23:30",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 0,
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:country_name",
"beta": 0,
"creation": "2013-01-19 10:23:30",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 0,
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "country_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Country Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "country_name",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "country_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Country Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "country_name",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "date_format",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Date Format",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "date_format",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Date Format",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "time_zones",
"fieldtype": "Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Time Zones",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "time_zones",
"fieldtype": "Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Time Zones",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "code",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Code",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "code",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Code",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-globe",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
"modified": "2016-12-29 14:40:34.951894",
"modified_by": "Administrator",
"module": "Geo",
"name": "Country",
"owner": "Administrator",
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-globe",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
"modified": "2018-08-29 06:37:32.303570",
"modified_by": "Administrator",
"module": "Geo",
"name": "Country",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 0,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"is_custom": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 0,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 1,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 1,
"share": 1,
"submit": 0,
"write": 1
},
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"is_custom": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "country_name",
"sort_order": "ASC",
"track_changes": 1,
"track_seen": 0
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "country_name",
"sort_order": "ASC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

View file

@ -1,322 +1,345 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "field:currency_name",
"beta": 0,
"creation": "2013-01-28 10:06:02",
"custom": 0,
"description": "**Currency** Master",
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 0,
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:currency_name",
"beta": 0,
"creation": "2013-01-28 10:06:02",
"custom": 0,
"description": "**Currency** Master",
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 0,
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "currency_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Currency Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "currency_name",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "currency_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Currency Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "currency_name",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 1
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "enabled",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Enabled",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "enabled",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Enabled",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Sub-currency. For e.g. \"Cent\"",
"fieldname": "fraction",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Fraction",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Sub-currency. For e.g. \"Cent\"",
"fieldname": "fraction",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Fraction",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "1 Currency = [?] Fraction\nFor e.g. 1 USD = 100 Cent",
"fieldname": "fraction_units",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Fraction Units",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "1 Currency = [?] Fraction\nFor e.g. 1 USD = 100 Cent",
"fieldname": "fraction_units",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Fraction Units",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Smallest circulating fraction unit (coin). For e.g. 1 cent for USD and it should be entered as 0.01",
"fieldname": "smallest_currency_fraction_value",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Smallest Currency Fraction Value",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Smallest circulating fraction unit (coin). For e.g. 1 cent for USD and it should be entered as 0.01",
"fieldname": "smallest_currency_fraction_value",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Smallest Currency Fraction Value",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "A symbol for this currency. For e.g. $",
"fieldname": "symbol",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Symbol",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "A symbol for this currency. For e.g. $",
"fieldname": "symbol",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Symbol",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "How should this currency be formatted? If not set, will use system defaults",
"fieldname": "number_format",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Number Format",
"length": 0,
"no_copy": 0,
"options": "\n#,###.##\n#.###,##\n# ###.##\n# ###,##\n#'###.##\n#, ###.##\n#,##,###.##\n#,###.###\n#.###\n#,###",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "How should this currency be formatted? If not set, will use system defaults",
"fieldname": "number_format",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Number Format",
"length": 0,
"no_copy": 0,
"options": "\n#,###.##\n#.###,##\n# ###.##\n# ###,##\n#'###.##\n#, ###.##\n#,##,###.##\n#,###.###\n#.###\n#,###",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-bitcoin",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-12-29 14:40:39.187557",
"modified_by": "Administrator",
"module": "Geo",
"name": "Currency",
"owner": "Administrator",
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-bitcoin",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-08-29 06:37:19.908254",
"modified_by": "Administrator",
"module": "Geo",
"name": "Currency",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"is_custom": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 1,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"is_custom": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Accounts User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Accounts User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
},
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"is_custom": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Sales User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Sales User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
},
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"is_custom": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Purchase User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "Purchase User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

5
frappe/hooks.py Executable file → Normal file
View file

@ -12,7 +12,7 @@ source_link = "https://github.com/frappe/frappe"
app_license = "MIT"
develop_version = '11.x.x-develop'
staging_version = '11.0.0-beta'
staging_version = '11.0.1'
app_email = "info@frappe.io"
@ -167,7 +167,8 @@ scheduler_events = {
"frappe.utils.scheduler.restrict_scheduler_events_if_dormant",
"frappe.email.doctype.auto_email_report.auto_email_report.send_daily",
"frappe.core.doctype.feedback_request.feedback_request.delete_feedback_request",
"frappe.core.doctype.activity_log.activity_log.clear_authentication_logs"
"frappe.core.doctype.activity_log.activity_log.clear_authentication_logs",
"frappe.utils.change_log.check_for_update"
],
"daily_long": [
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily",

View file

@ -8,104 +8,44 @@ from __future__ import unicode_literals, print_function
from six.moves import input
import os, json, sys, subprocess, shutil
import os, json, subprocess, shutil
import frappe
import frappe.database
import getpass
import importlib
from frappe import _
from frappe.model.db_schema import DbManager
from frappe.model.sync import sync_for
from frappe.utils.fixtures import sync_fixtures
from frappe.website import render
from frappe.desk.doctype.desktop_icon.desktop_icon import sync_from_app
from frappe.utils.password import create_auth_table
from frappe.utils.global_search import setup_global_search_table
from frappe.modules.utils import sync_customizations
from frappe.database import setup_database
def install_db(root_login="root", root_password=None, db_name=None, source_sql=None,
admin_password=None, verbose=True, force=0, site_config=None, reinstall=False):
make_conf(db_name, site_config=site_config)
frappe.flags.in_install_db = True
if reinstall:
frappe.connect(db_name=db_name)
dbman = DbManager(frappe.local.db)
dbman.create_database(db_name)
admin_password=None, verbose=True, force=0, site_config=None, reinstall=False,
db_type=None):
if not db_type:
db_type = frappe.conf.db_type or 'mariadb'
else:
frappe.local.db = get_root_connection(root_login, root_password)
frappe.local.session = frappe._dict({'user':'Administrator'})
create_database_and_user(force, verbose)
make_conf(db_name, site_config=site_config, db_type=db_type)
frappe.flags.in_install_db = True
frappe.flags.root_login = root_login
frappe.flags.root_password = root_password
setup_database(force, source_sql, verbose)
frappe.conf.admin_password = frappe.conf.admin_password or admin_password
frappe.connect(db_name=db_name)
check_if_ready_for_barracuda()
import_db_from_sql(source_sql, verbose)
if not 'tabDefaultValue' in frappe.db.get_tables():
print('''Database not installed, this can due to lack of permission, or that the database name exists.
Check your mysql root password, or use --force to reinstall''')
sys.exit(1)
remove_missing_apps()
create_auth_table()
setup_global_search_table()
create_user_settings_table()
frappe.db.create_auth_table()
frappe.db.create_global_search_table()
frappe.db.create_user_settings_table()
frappe.flags.in_install_db = False
def create_database_and_user(force, verbose):
db_name = frappe.local.conf.db_name
dbman = DbManager(frappe.local.db)
if force or (db_name not in dbman.get_database_list()):
dbman.delete_user(db_name)
dbman.drop_database(db_name)
else:
raise Exception("Database %s already exists" % (db_name,))
dbman.create_user(db_name, frappe.conf.db_password)
if verbose: print("Created user %s" % db_name)
dbman.create_database(db_name)
if verbose: print("Created database %s" % db_name)
dbman.grant_all_privileges(db_name, db_name)
dbman.flush_privileges()
if verbose: print("Granted privileges to user %s and database %s" % (db_name, db_name))
# close root connection
frappe.db.close()
def create_user_settings_table():
frappe.db.sql_ddl("""create table if not exists __UserSettings (
`user` VARCHAR(180) NOT NULL,
`doctype` VARCHAR(180) NOT NULL,
`data` TEXT,
UNIQUE(user, doctype)
) ENGINE=InnoDB DEFAULT CHARSET=utf8""")
def import_db_from_sql(source_sql, verbose):
if verbose: print("Starting database import...")
db_name = frappe.conf.db_name
if not source_sql:
source_sql = os.path.join(os.path.dirname(frappe.__file__), 'data', 'Framework.sql')
DbManager(frappe.local.db).restore_database(db_name, source_sql, db_name, frappe.conf.db_password)
if verbose: print("Imported from database %s" % source_sql)
def get_root_connection(root_login='root', root_password=None):
if not frappe.local.flags.root_connection:
if root_login:
if not root_password:
root_password = frappe.conf.get("root_password") or None
if not root_password:
root_password = getpass.getpass("MySQL root password: ")
frappe.local.flags.root_connection = frappe.database.Database(user=root_login, password=root_password)
return frappe.local.flags.root_connection
def install_app(name, verbose=False, set_as_patched=True):
frappe.flags.in_install = name
frappe.flags.ignore_in_install = False
@ -117,7 +57,7 @@ def install_app(name, verbose=False, set_as_patched=True):
# install pre-requisites
if app_hooks.required_apps:
for app in app_hooks.required_apps:
install_app(app)
install_app(app, verbose=verbose)
frappe.flags.in_install = name
frappe.clear_cache()
@ -257,14 +197,14 @@ def init_singles():
doc.flags.ignore_validate=True
doc.save()
def make_conf(db_name=None, db_password=None, site_config=None):
def make_conf(db_name=None, db_password=None, site_config=None, db_type=None):
site = frappe.local.site
make_site_config(db_name, db_password, site_config)
make_site_config(db_name, db_password, site_config, db_type=db_type)
sites_path = frappe.local.sites_path
frappe.destroy()
frappe.init(site, sites_path=sites_path)
def make_site_config(db_name=None, db_password=None, site_config=None):
def make_site_config(db_name=None, db_password=None, site_config=None, db_type=None):
frappe.create_folder(os.path.join(frappe.local.site_path))
site_file = get_site_config_path()
@ -272,6 +212,9 @@ def make_site_config(db_name=None, db_password=None, site_config=None):
if not (site_config and isinstance(site_config, dict)):
site_config = get_conf_params(db_name, db_password)
if db_type:
site_config['db_type'] = db_type
with open(site_file, "w") as f:
f.write(json.dumps(site_config, indent=1, sort_keys=True))
@ -300,7 +243,7 @@ def update_site_config(key, value, validate=True, site_config_path=None):
with open(site_config_path, "w") as f:
f.write(json.dumps(site_config, indent=1, sort_keys=True))
if hasattr(frappe.local, "conf"):
frappe.local.conf[key] = value
@ -353,47 +296,6 @@ def remove_missing_apps():
installed_apps.remove(app)
frappe.db.set_global("installed_apps", json.dumps(installed_apps))
def check_if_ready_for_barracuda():
mariadb_variables = frappe._dict(frappe.db.sql("""show variables"""))
mariadb_minor_version = int(mariadb_variables.get('version').split('-')[0].split('.')[1])
if mariadb_minor_version < 3:
check_database(mariadb_variables, {
"innodb_file_format": "Barracuda",
"innodb_file_per_table": "ON",
"innodb_large_prefix": "ON"
})
check_database(mariadb_variables, {
"character_set_server": "utf8mb4",
"collation_server": "utf8mb4_unicode_ci"
})
def check_database(mariadb_variables, variables_dict):
mariadb_minor_version = int(mariadb_variables.get('version').split('-')[0].split('.')[1])
for key, value in variables_dict.items():
if mariadb_variables.get(key) != value:
site = frappe.local.site
msg = ("Creation of your site - {x} failed because MariaDB is not properly {sep}"
"configured to use the Barracuda storage engine. {sep}"
"Please add the settings below to MariaDB's my.cnf, restart MariaDB then {sep}"
"run `bench new-site {x}` again.{sep2}"
"").format(x=site, sep2="\n"*2, sep="\n")
if mariadb_minor_version < 3:
print_db_config(msg, expected_config_for_barracuda_2)
else:
print_db_config(msg, expected_config_for_barracuda_3)
raise frappe.exceptions.ImproperDBConfigurationError(
reason="MariaDB default file format is not Barracuda"
)
def print_db_config(explanation, config_text):
print("="*80)
print(explanation)
print(config_text)
print("="*80)
def extract_sql_gzip(sql_gz_path):
try:
subprocess.check_call(['gzip', '-d', '-v', '-f', sql_gz_path])
@ -421,25 +323,4 @@ def extract_tar_files(site_name, file_path, folder_name):
finally:
frappe.destroy()
return tar_path
expected_config_for_barracuda_2 = """[mysqld]
innodb-file-format=barracuda
innodb-file-per-table=1
innodb-large-prefix=1
character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
[mysql]
default-character-set = utf8mb4
"""
expected_config_for_barracuda_3 = """[mysqld]
character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
[mysql]
default-character-set = utf8mb4
"""
return tar_path

View file

@ -106,7 +106,7 @@ def backup_to_dropbox(upload_db_backup=True):
dropbox_client = dropbox.Dropbox(dropbox_settings['access_token'])
if not upload_db_backup:
if upload_db_backup:
backup = new_backup(ignore_files=True)
filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db))
upload_file_to_dropbox(filename, "/database", dropbox_client)
@ -133,10 +133,12 @@ def upload_from_folder(path, is_private, dropbox_folder, dropbox_client, did_not
path = text_type(path)
for f in frappe.get_all("File", filters={"is_folder": 0, "is_private": is_private,
"uploaded_to_dropbox": 0}, fields=['file_url', 'name']):
"uploaded_to_dropbox": 0}, fields=['file_url', 'name', 'file_name']):
if is_private:
filename = f.file_url.replace('/private/files/', '')
else:
if not f.file_url:
f.file_url = '/files/' + f.file_name;
filename = f.file_url.replace('/files/', '')
filepath = os.path.join(path, filename)

View file

@ -3,11 +3,11 @@
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
import googlemaps
import datetime
class GoogleMapsSettings(Document):
def validate(self):
@ -18,24 +18,14 @@ class GoogleMapsSettings(Document):
frappe.throw(_("Home Address is required"))
def get_client(self):
if not self.enabled:
frappe.throw(_("Google Maps integration is not enabled"))
import googlemaps
try:
client = googlemaps.Client(key=self.client_key)
except Exception as e:
frappe.throw(e.message)
return client
def round_timedelta(td, period):
"""Round timedelta"""
period_seconds = period.total_seconds()
half_period_seconds = period_seconds / 2
remainder = td.total_seconds() % period_seconds
if remainder >= half_period_seconds:
return datetime.timedelta(seconds=td.total_seconds() + (period_seconds - remainder))
else:
return datetime.timedelta(seconds=td.total_seconds() - remainder)
def format_address(address):
"""Customer Address format """
address = frappe.get_doc('Address', address)
return '{}, {}, {}, {}'.format(address.address_line1, address.city, address.pincode, address.country)

View file

@ -207,7 +207,7 @@ def update_space_usage():
files_size += get_folder_size(frappe.get_site_path("private", "files"))
backup_size = get_folder_size(frappe.get_site_path("private", "backups"))
database_size = get_database_size()
database_size = frappe.db.get_database_size()
usage = {
'files_size': flt(files_size, 2),
@ -225,13 +225,3 @@ def get_folder_size(path):
if os.path.exists(path):
return flt(subprocess.check_output(['du', '-ms', path]).split()[0], 2)
def get_database_size():
'''Returns approximate database size in MB'''
db_name = frappe.conf.db_name
# This query will get the database size in MB
db_size = frappe.db.sql('''
SELECT table_schema "database_name", sum( data_length + index_length ) / 1024 / 1024 "database_size"
FROM information_schema.TABLES WHERE table_schema = %s GROUP BY table_schema''', db_name, as_dict=True)
return flt(db_size[0].get('database_size'), 2)

View file

@ -6,6 +6,34 @@ from __future__ import unicode_literals
import frappe
import json
data_fieldtypes = (
'Currency',
'Int',
'Long Int',
'Float',
'Percent',
'Check',
'Small Text',
'Long Text',
'Code',
'Text Editor',
'Date',
'Datetime',
'Time',
'Text',
'Data',
'Link',
'Dynamic Link',
'Password',
'Select',
'Read Only',
'Attach',
'Attach Image',
'Signature',
'Color',
'Barcode',
'Geolocation'
)
no_value_fields = ('Section Break', 'Column Break', 'HTML', 'Table', 'Button', 'Image',
'Fold', 'Heading')
@ -14,31 +42,12 @@ default_fields = ('doctype','name','owner','creation','modified','modified_by',
'parent','parentfield','parenttype','idx','docstatus')
optional_fields = ("_user_tags", "_comments", "_assign", "_liked_by", "_seen")
def copytables(srctype, src, srcfield, tartype, tar, tarfield, srcfields, tarfields=[]):
if not tarfields:
tarfields = srcfields
l = []
data = src.get(srcfield)
for d in data:
newrow = tar.append(tarfield)
newrow.idx = d.idx
for i in range(len(srcfields)):
newrow.set(tarfields[i], d.get(srcfields[i]))
l.append(newrow)
return l
def db_exists(dt, dn):
return frappe.db.exists(dt, dn)
def delete_fields(args_dict, delete=0):
"""
Delete a field.
* Deletes record from `tabDocField`
* If not single doctype: Drops column from table
* If single, deletes record from `tabSingles`
args_dict = { dt: [field names] }
"""
import frappe.utils
@ -65,4 +74,4 @@ def delete_fields(args_dict, delete=0):
query = "ALTER TABLE `tab%s` " % dt + \
", ".join(["DROP COLUMN `%s`" % f for f in fields if f in existing_fields])
frappe.db.commit()
frappe.db.sql(query)
frappe.db.sql(query)

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