Merge branch 'develop' into repr_doctype

This commit is contained in:
Suraj Shetty 2021-05-06 12:45:01 +05:30 committed by GitHub
commit 8d491348a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 405 additions and 297 deletions

View file

@ -42,7 +42,7 @@ rules:
- id: frappe-translation-python-splitting
pattern-either:
- pattern: _(...) + ... + _(...)
- pattern: _(...) + _(...)
- pattern: _("..." + "...")
- pattern-regex: '_\([^\)]*\\\s*' # lines broken by `\`
- pattern-regex: '_\(\s*\n' # line breaks allowed by python for using ( )

View file

@ -10,11 +10,10 @@ be used to build database driven apps.
Read the documentation: https://frappeframework.com/docs
"""
from __future__ import unicode_literals, print_function
from six import iteritems, binary_type, text_type, string_types, PY2
from six import iteritems, binary_type, text_type, string_types
from werkzeug.local import Local, release_local
import os, sys, importlib, inspect, json
import os, sys, importlib, inspect, json, warnings
import typing
from past.builtins import cmp
import click
@ -27,19 +26,14 @@ from .utils.lazy_loader import lazy_import
# Lazy imports
faker = lazy_import('faker')
# Harmless for Python 3
# For Python 2 set default encoding to utf-8
if PY2:
reload(sys)
sys.setdefaultencoding("utf-8")
__version__ = '14.0.0-dev'
__title__ = "Frappe Framework"
local = Local()
controllers = {}
warnings.simplefilter('always', DeprecationWarning)
warnings.simplefilter('always', PendingDeprecationWarning)
class _dict(dict):
"""dict like object that exposes keys as attributes"""

View file

@ -185,7 +185,7 @@ def make_form_dict(request):
args = request.form or request.args
if not isinstance(args, dict):
frappe.throw("Invalid request arguments")
frappe.throw(_("Invalid request arguments"))
try:
frappe.local.form_dict = frappe._dict({ k:v[0] if isinstance(v, (list, tuple)) else v \
@ -296,7 +296,7 @@ def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=No
from werkzeug.serving import run_simple
patch_werkzeug_reloader()
if profile:
if profile or os.environ.get('USE_PROFILER'):
application = ProfilerMiddleware(application, sort_by=('cumtime', 'calls'))
if not os.environ.get('NO_STATICS'):

View file

@ -254,9 +254,7 @@ def list_apps(context, format):
frappe.destroy()
if format == "json":
import json
click.echo(json.dumps(summary_dict))
click.echo(frappe.as_json(summary_dict))
@click.command('add-system-manager')
@click.argument('email')

View file

@ -96,22 +96,54 @@ def destroy_all_sessions(context, reason=None):
raise SiteNotSpecifiedError
@click.command('show-config')
@click.option("--format", "-f", type=click.Choice(["text", "json"]), default="text")
@pass_context
def show_config(context):
"print configuration file"
print("\t\033[92m{:<50}\033[0m \033[92m{:<15}\033[0m".format('Config','Value'))
sites_path = os.path.join(frappe.utils.get_bench_path(), 'sites')
site_path = context.sites[0]
configuration = frappe.get_site_config(sites_path=sites_path, site_path=site_path)
print_config(configuration)
def show_config(context, format):
"Print configuration file to STDOUT in speified format"
if not context.sites:
raise SiteNotSpecifiedError
def print_config(config):
for conf, value in config.items():
if isinstance(value, dict):
print_config(value)
else:
print("\t{:<50} {:<15}".format(conf, value))
sites_config = {}
sites_path = os.getcwd()
from frappe.utils.commands import render_table
def transform_config(config, prefix=None):
prefix = f"{prefix}." if prefix else ""
site_config = []
for conf, value in config.items():
if isinstance(value, dict):
site_config += transform_config(value, prefix=f"{prefix}{conf}")
else:
log_value = json.dumps(value) if isinstance(value, list) else value
site_config += [[f"{prefix}{conf}", log_value]]
return site_config
for site in context.sites:
frappe.init(site)
if len(context.sites) != 1 and format == "text":
if context.sites.index(site) != 0:
click.echo()
click.secho(f"Site {site}", fg="yellow")
configuration = frappe.get_site_config(sites_path=sites_path, site_path=site)
if format == "text":
data = transform_config(configuration)
data.insert(0, ['Config','Value'])
render_table(data)
if format == "json":
sites_config[site] = configuration
frappe.destroy()
if format == "json":
click.echo(frappe.as_json(sites_config))
@click.command('reset-perms')
@ -470,6 +502,7 @@ def console(context):
locals()[app] = __import__(app)
except ModuleNotFoundError:
failed_to_import.append(app)
all_apps.remove(app)
print("Apps in this namespace:\n{}".format(", ".join(all_apps)))
if failed_to_import:
@ -657,20 +690,27 @@ def make_app(destination, app_name):
@click.command('set-config')
@click.argument('key')
@click.argument('value')
@click.option('-g', '--global', 'global_', is_flag = True, default = False, help = 'Set Global Site Config')
@click.option('--as-dict', is_flag=True, default=False)
@click.option('-g', '--global', 'global_', is_flag=True, default=False, help='Set value in bench config')
@click.option('-p', '--parse', is_flag=True, default=False, help='Evaluate as Python Object')
@click.option('--as-dict', is_flag=True, default=False, help='Legacy: Evaluate as Python Object')
@pass_context
def set_config(context, key, value, global_ = False, as_dict=False):
def set_config(context, key, value, global_=False, parse=False, as_dict=False):
"Insert/Update a value in site_config.json"
from frappe.installer import update_site_config
import ast
if as_dict:
from frappe.utils.commands import warn
warn("--as-dict will be deprecated in v14. Use --parse instead", category=PendingDeprecationWarning)
parse = as_dict
if parse:
import ast
value = ast.literal_eval(value)
if global_:
sites_path = os.getcwd() # big assumption.
sites_path = os.getcwd()
common_site_config_path = os.path.join(sites_path, 'common_site_config.json')
update_site_config(key, value, validate = False, site_config_path = common_site_config_path)
update_site_config(key, value, validate=False, site_config_path=common_site_config_path)
else:
for site in context.sites:
frappe.init(site=site)
@ -727,50 +767,6 @@ def rebuild_global_search(context, static_pages=False):
if not context.sites:
raise SiteNotSpecifiedError
@click.command('auto-deploy')
@click.argument('app')
@click.option('--migrate', is_flag=True, default=False, help='Migrate after pulling')
@click.option('--restart', is_flag=True, default=False, help='Restart after migration')
@click.option('--remote', default='upstream', help='Remote, default is "upstream"')
@pass_context
def auto_deploy(context, app, migrate=False, restart=False, remote='upstream'):
'''Pull and migrate sites that have new version'''
from frappe.utils.gitutils import get_app_branch
from frappe.utils import get_sites
branch = get_app_branch(app)
app_path = frappe.get_app_path(app)
# fetch
subprocess.check_output(['git', 'fetch', remote, branch], cwd = app_path)
# get diff
if subprocess.check_output(['git', 'diff', '{0}..{1}/{0}'.format(branch, remote)], cwd = app_path):
print('Updates found for {0}'.format(app))
if app=='frappe':
# run bench update
import shlex
subprocess.check_output(shlex.split('bench update --no-backup'), cwd = '..')
else:
updated = False
subprocess.check_output(['git', 'pull', '--rebase', remote, branch],
cwd = app_path)
# find all sites with that app
for site in get_sites():
frappe.init(site)
if app in frappe.get_installed_apps():
print('Updating {0}'.format(site))
updated = True
subprocess.check_output(['bench', '--site', site, 'clear-cache'], cwd = '..')
if migrate:
subprocess.check_output(['bench', '--site', site, 'migrate'], cwd = '..')
frappe.destroy()
if updated or restart:
subprocess.check_output(['bench', 'restart'], cwd = '..')
else:
print('No Updates')
commands = [
build,

View file

@ -272,22 +272,13 @@ def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None)
doc.attachments.append(a)
def set_incoming_outgoing_accounts(doc):
doc.incoming_email_account = doc.outgoing_email_account = None
from frappe.email.doctype.email_account.email_account import EmailAccount
incoming_email_account = EmailAccount.find_incoming(
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype)
doc.incoming_email_account = incoming_email_account.email_id if incoming_email_account else None
if not doc.incoming_email_account and doc.sender:
doc.incoming_email_account = frappe.db.get_value("Email Account",
{"email_id": doc.sender, "enable_incoming": 1}, "email_id")
if not doc.incoming_email_account and doc.reference_doctype:
doc.incoming_email_account = frappe.db.get_value("Email Account",
{"append_to": doc.reference_doctype, }, "email_id")
if not doc.incoming_email_account:
doc.incoming_email_account = frappe.db.get_value("Email Account",
{"default_incoming": 1, "enable_incoming": 1}, "email_id")
doc.outgoing_email_account = frappe.email.smtp.get_outgoing_email_account(raise_exception_not_set=False,
append_to=doc.doctype, sender=doc.sender)
doc.outgoing_email_account = EmailAccount.find_outgoing(
match_by_email=doc.sender, match_by_doctype=doc.reference_doctype)
if doc.sent_or_received == "Sent":
doc.db_set("email_account", doc.outgoing_email_account.name)

View file

@ -377,10 +377,17 @@ def handle_duration_fieldtype_values(result, columns):
if fieldtype == "Duration":
for entry in range(0, len(result)):
val_in_seconds = result[entry][i]
if val_in_seconds:
duration_val = format_duration(val_in_seconds)
result[entry][i] = duration_val
row = result[entry]
if isinstance(row, dict):
val_in_seconds = row[col.fieldname]
if val_in_seconds:
duration_val = format_duration(val_in_seconds)
row[col.fieldname] = duration_val
else:
val_in_seconds = row[i]
if val_in_seconds:
duration_val = format_duration(val_in_seconds)
row[i] = duration_val
return result

View file

@ -8,9 +8,14 @@ import re
import json
import socket
import time
from frappe import _
import functools
import email.utils
from frappe import _, are_emails_muted
from frappe.model.document import Document
from frappe.utils import validate_email_address, cint, cstr, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html, add_days
from frappe.utils import (validate_email_address, cint, cstr, get_datetime,
DATE_FORMAT, strip, comma_or, sanitize_html, add_days, parse_addr)
from frappe.utils.user import is_system_user
from frappe.utils.jinja import render_template
from frappe.email.smtp import SMTPServer
@ -21,17 +26,40 @@ from datetime import datetime, timedelta
from frappe.desk.form import assign_to
from frappe.utils.user import get_system_managers
from frappe.utils.background_jobs import enqueue, get_jobs
from frappe.core.doctype.communication.email import set_incoming_outgoing_accounts
from frappe.utils.html_utils import clean_email_html
from frappe.utils.error import raise_error_on_no_output
from frappe.email.utils import get_port
OUTGOING_EMAIL_ACCOUNT_MISSING = _("Please setup default Email Account from Setup > Email > Email Account")
class SentEmailInInbox(Exception):
pass
class InvalidEmailCredentials(frappe.ValidationError):
pass
def cache_email_account(cache_name):
def decorator_cache_email_account(func):
@functools.wraps(func)
def wrapper_cache_email_account(*args, **kwargs):
if not hasattr(frappe.local, cache_name):
setattr(frappe.local, cache_name, {})
cached_accounts = getattr(frappe.local, cache_name)
match_by = list(kwargs.values()) + ['default']
matched_accounts = list(filter(None, [cached_accounts.get(key) for key in match_by]))
if matched_accounts:
return matched_accounts[0]
matched_accounts = func(*args, **kwargs)
cached_accounts.update(matched_accounts or {})
return matched_accounts and list(matched_accounts.values())[0]
return wrapper_cache_email_account
return decorator_cache_email_account
class EmailAccount(Document):
DOCTYPE = 'Email Account'
def autoname(self):
"""Set name as `email_account_name` or make title from Email Address."""
if not self.email_account_name:
@ -249,6 +277,15 @@ class EmailAccount(Document):
else:
raise
@property
def _password(self):
raise_exception = not self.no_smtp_authentication
return self.get_password(raise_exception=raise_exception)
@property
def default_sender(self):
return email.utils.formataddr((self.name, self.get("email_id")))
@classmethod
def throw_invalid_credentials_exception(cls):
frappe.throw(
@ -257,6 +294,114 @@ class EmailAccount(Document):
title=_("Invalid Credentials")
)
@classmethod
def from_record(cls, record):
email_account = frappe.new_doc(cls.DOCTYPE)
email_account.update(record)
return email_account
@classmethod
def find(cls, name):
return frappe.get_doc(cls.DOCTYPE, name)
@classmethod
def find_one_by_filters(cls, **kwargs):
name = frappe.db.get_value(cls.DOCTYPE, kwargs)
return cls.find(name) if name else None
@classmethod
def find_from_config(cls):
config = cls.get_account_details_from_site_config()
return cls.from_record(config) if config else None
@classmethod
def create_dummy(cls):
return cls.from_record({"sender": "notifications@example.com"})
@classmethod
@raise_error_on_no_output(
keep_quiet = lambda: not cint(frappe.get_system_settings('setup_complete')),
error_message = OUTGOING_EMAIL_ACCOUNT_MISSING, error_type = frappe.OutgoingEmailError) # noqa
@cache_email_account('outgoing_email_account')
def find_outgoing(cls, match_by_email=None, match_by_doctype=None, _raise_error=False):
"""Find the outgoing Email account to use.
:param match_by_email: Find account using emailID
:param match_by_doctype: Find account by matching `Append To` doctype
:param _raise_error: This is used by raise_error_on_no_output decorator to raise error.
"""
if match_by_email:
match_by_email = parse_addr(match_by_email)[1]
doc = cls.find_one_by_filters(enable_outgoing=1, email_id=match_by_email)
if doc:
return {match_by_email: doc}
if match_by_doctype:
doc = cls.find_one_by_filters(enable_outgoing=1, enable_incoming=1, append_to=match_by_doctype)
if doc:
return {match_by_doctype: doc}
doc = cls.find_default_outgoing()
if doc:
return {'default': doc}
@classmethod
def find_default_outgoing(cls):
""" Find default outgoing account.
"""
doc = cls.find_one_by_filters(enable_outgoing=1, default_outgoing=1)
doc = doc or cls.find_from_config()
return doc or (are_emails_muted() and cls.create_dummy())
@classmethod
def find_incoming(cls, match_by_email=None, match_by_doctype=None):
"""Find the incoming Email account to use.
:param match_by_email: Find account using emailID
:param match_by_doctype: Find account by matching `Append To` doctype
"""
doc = cls.find_one_by_filters(enable_incoming=1, email_id=match_by_email)
if doc:
return doc
doc = cls.find_one_by_filters(enable_incoming=1, append_to=match_by_doctype)
if doc:
return doc
doc = cls.find_default_incoming()
return doc
@classmethod
def find_default_incoming(cls):
doc = cls.find_one_by_filters(enable_incoming=1, default_incoming=1)
return doc
@classmethod
def get_account_details_from_site_config(cls):
if not frappe.conf.get("mail_server"):
return {}
field_to_conf_name_map = {
'smtp_server': {'conf_names': ('mail_server',)},
'smtp_port': {'conf_names': ('mail_port',)},
'use_tls': {'conf_names': ('use_tls', 'mail_login')},
'login_id': {'conf_names': ('mail_login',)},
'email_id': {'conf_names': ('auto_email_id', 'mail_login'), 'default': 'notifications@example.com'},
'password': {'conf_names': ('mail_password',)},
'always_use_account_email_id_as_sender':
{'conf_names': ('always_use_account_email_id_as_sender',), 'default': 0},
'always_use_account_name_as_sender_name':
{'conf_names': ('always_use_account_name_as_sender_name',), 'default': 0},
'name': {'conf_names': ('email_sender_name',), 'default': 'Frappe'},
'from_site_config': {'default': True}
}
account_details = {}
for doc_field_name, d in field_to_conf_name_map.items():
conf_names, default = d.get('conf_names') or [], d.get('default')
value = [frappe.conf.get(k) for k in conf_names if frappe.conf.get(k)]
account_details[doc_field_name] = (value and value[0]) or default
return account_details
def handle_incoming_connect_error(self, description):
if test_internet():
if self.get_failed_attempts_count() > 2:
@ -642,6 +787,8 @@ class EmailAccount(Document):
def send_auto_reply(self, communication, email):
"""Send auto reply if set."""
from frappe.core.doctype.communication.email import set_incoming_outgoing_accounts
if self.enable_auto_reply:
set_incoming_outgoing_accounts(communication)
@ -653,7 +800,7 @@ class EmailAccount(Document):
frappe.sendmail(recipients = [email.from_email],
sender = self.email_id,
reply_to = communication.incoming_email_account,
subject = _("Re: ") + communication.subject,
subject = " ".join([_("Re:"), communication.subject]),
content = render_template(self.auto_reply_message or "", communication.as_dict()) or \
frappe.get_template("templates/emails/auto_reply.html").render(communication.as_dict()),
reference_doctype = communication.reference_doctype,

View file

@ -10,7 +10,8 @@
"incoming_port": "993",
"attachment_limit": "1",
"smtp_server": "smtp.test.com",
"smtp_port": "587"
"smtp_port": "587",
"password": "password"
},
{
"doctype": "Email Account",
@ -25,6 +26,7 @@
"incoming_port": "143",
"attachment_limit": "1",
"smtp_server": "smtp.test.com",
"smtp_port": "587"
"smtp_port": "587",
"password": "password"
}
]

View file

@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe, re, os
from frappe.utils.pdf import get_pdf
from frappe.email.smtp import get_outgoing_email_account
from frappe.email.doctype.email_account.email_account import EmailAccount
from frappe.utils import (get_url, scrub_urls, strip, expand_relative_urls, cint,
split_emails, to_markdown, markdown, random_string, parse_addr)
import email.utils
@ -75,7 +75,8 @@ class EMail:
self.bcc = bcc or []
self.html_set = False
self.email_account = email_account or get_outgoing_email_account(sender=sender)
self.email_account = email_account or \
EmailAccount.find_outgoing(match_by_email=sender, _raise_error=True)
def set_html(self, message, text_content = None, footer=None, print_html=None,
formatted=None, inline_images=None, header=None):
@ -249,8 +250,8 @@ class EMail:
def get_formatted_html(subject, message, footer=None, print_html=None,
email_account=None, header=None, unsubscribe_link=None, sender=None, with_container=False):
if not email_account:
email_account = get_outgoing_email_account(False, sender=sender)
email_account = email_account or EmailAccount.find_outgoing(match_by_email=sender)
signature = None
if "<!-- signature-included -->" not in message:
@ -480,4 +481,4 @@ def sanitize_email_header(str):
return str.replace('\r', '').replace('\n', '')
def get_brand_logo(email_account):
return email_account.get('brand_logo')
return email_account.get('brand_logo')

View file

@ -7,7 +7,8 @@ import sys
from six.moves import html_parser as HTMLParser
import smtplib, quopri, json
from frappe import msgprint, _, safe_decode, safe_encode, enqueue
from frappe.email.smtp import SMTPServer, get_outgoing_email_account
from frappe.email.smtp import SMTPServer
from frappe.email.doctype.email_account.email_account import EmailAccount
from frappe.email.email_body import get_email, get_formatted_html, add_attachment
from frappe.utils.verified_command import get_signed_params, verify_request
from html2text import html2text
@ -73,7 +74,9 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content=
if isinstance(send_after, int):
send_after = add_days(nowdate(), send_after)
email_account = get_outgoing_email_account(True, append_to=reference_doctype, sender=sender)
email_account = EmailAccount.find_outgoing(
match_by_doctype=reference_doctype, match_by_email=sender, _raise_error=True)
if not sender or sender == "Administrator":
sender = email_account.default_sender
@ -516,7 +519,7 @@ def prepare_message(email, recipient, recipients_list):
return ""
# Parse "Email Account" from "Email Sender"
email_account = get_outgoing_email_account(raise_exception_not_set=False, sender=email.sender)
email_account = EmailAccount.find_outgoing(match_by_email=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())

View file

@ -34,126 +34,6 @@ def send(email, append_to=None, retry=1):
_send(retry)
def get_outgoing_email_account(raise_exception_not_set=True, append_to=None, sender=None):
"""Returns outgoing email account based on `append_to` or the default
outgoing account. If default outgoing account is not found, it will
try getting settings from `site_config.json`."""
sender_email_id = None
_email_account = None
if sender:
sender_email_id = parse_addr(sender)[1]
if not getattr(frappe.local, "outgoing_email_account", None):
frappe.local.outgoing_email_account = {}
if not (frappe.local.outgoing_email_account.get(append_to)
or frappe.local.outgoing_email_account.get(sender_email_id)
or frappe.local.outgoing_email_account.get("default")):
email_account = None
if sender_email_id:
# check if the sender has an email account with enable_outgoing
email_account = _get_email_account({"enable_outgoing": 1,
"email_id": sender_email_id})
if not email_account and append_to:
# append_to is only valid when enable_incoming is checked
email_accounts = frappe.db.get_values("Email Account", {
"enable_outgoing": 1,
"enable_incoming": 1,
"append_to": append_to,
}, cache=True)
if email_accounts:
_email_account = email_accounts[0]
else:
email_account = _get_email_account({
"enable_outgoing": 1,
"enable_incoming": 1,
"append_to": append_to
})
if not email_account:
# sender don't have the outging email account
sender_email_id = None
email_account = get_default_outgoing_email_account(raise_exception_not_set=raise_exception_not_set)
if not email_account and _email_account:
# if default email account is not configured then setup first email account based on append to
email_account = _email_account
if not email_account and raise_exception_not_set and cint(frappe.db.get_single_value('System Settings', 'setup_complete')):
frappe.throw(_("Please setup default Email Account from Setup > Email > Email Account"),
frappe.OutgoingEmailError)
if email_account:
if email_account.enable_outgoing and not getattr(email_account, 'from_site_config', False):
raise_exception = True
if email_account.smtp_server in ['localhost','127.0.0.1'] or email_account.no_smtp_authentication:
raise_exception = False
email_account.password = email_account.get_password(raise_exception=raise_exception)
email_account.default_sender = email.utils.formataddr((email_account.name, email_account.get("email_id")))
frappe.local.outgoing_email_account[append_to or sender_email_id or "default"] = email_account
return frappe.local.outgoing_email_account.get(append_to) \
or frappe.local.outgoing_email_account.get(sender_email_id) \
or frappe.local.outgoing_email_account.get("default")
def get_default_outgoing_email_account(raise_exception_not_set=True):
'''conf should be like:
{
"mail_server": "smtp.example.com",
"mail_port": 587,
"use_tls": 1,
"mail_login": "emails@example.com",
"mail_password": "Super.Secret.Password",
"auto_email_id": "emails@example.com",
"email_sender_name": "Example Notifications",
"always_use_account_email_id_as_sender": 0,
"always_use_account_name_as_sender_name": 0
}
'''
email_account = _get_email_account({"enable_outgoing": 1, "default_outgoing": 1})
if email_account:
email_account.password = email_account.get_password(raise_exception=False)
if not email_account and frappe.conf.get("mail_server"):
# from site_config.json
email_account = frappe.new_doc("Email Account")
email_account.update({
"smtp_server": frappe.conf.get("mail_server"),
"smtp_port": frappe.conf.get("mail_port"),
# legacy: use_ssl was used in site_config instead of use_tls, but meant the same thing
"use_tls": cint(frappe.conf.get("use_tls") or 0) or cint(frappe.conf.get("use_ssl") or 0),
"login_id": frappe.conf.get("mail_login"),
"email_id": frappe.conf.get("auto_email_id") or frappe.conf.get("mail_login") or 'notifications@example.com',
"password": frappe.conf.get("mail_password"),
"always_use_account_email_id_as_sender": frappe.conf.get("always_use_account_email_id_as_sender", 0),
"always_use_account_name_as_sender_name": frappe.conf.get("always_use_account_name_as_sender_name", 0)
})
email_account.from_site_config = True
email_account.name = frappe.conf.get("email_sender_name") or "Frappe"
if not email_account and not raise_exception_not_set:
return None
if frappe.are_emails_muted():
# create a stub
email_account = frappe.new_doc("Email Account")
email_account.update({
"email_id": "notifications@example.com"
})
return email_account
def _get_email_account(filters):
name = frappe.db.get_value("Email Account", filters)
return frappe.get_doc("Email Account", name) if name else None
class SMTPServer:
def __init__(self, login=None, password=None, server=None, port=None, use_tls=None, use_ssl=None, append_to=None):
@ -176,17 +56,15 @@ class SMTPServer:
self.setup_email_account(append_to)
def setup_email_account(self, append_to=None, sender=None):
self.email_account = get_outgoing_email_account(raise_exception_not_set=False, append_to=append_to, sender=sender)
from frappe.email.doctype.email_account.email_account import EmailAccount
self.email_account = EmailAccount.find_outgoing(match_by_doctype=append_to, match_by_email=sender)
if self.email_account:
self.server = self.email_account.smtp_server
self.login = (getattr(self.email_account, "login_id", None) or self.email_account.email_id)
if not self.email_account.no_smtp_authentication:
if self.email_account.ascii_encode_password:
self.password = frappe.safe_encode(self.email_account.password, 'ascii')
else:
self.password = self.email_account.password
else:
if self.email_account.no_smtp_authentication or frappe.local.flags.in_test:
self.password = None
else:
self.password = self.email_account._password
self.port = self.email_account.smtp_port
self.use_tls = self.email_account.use_tls
self.sender = self.email_account.email_id

View file

@ -4,7 +4,7 @@
import unittest
import frappe
from frappe.email.smtp import SMTPServer
from frappe.email.smtp import get_outgoing_email_account
from frappe.email.doctype.email_account.email_account import EmailAccount
class TestSMTP(unittest.TestCase):
def test_smtp_ssl_session(self):
@ -33,13 +33,13 @@ class TestSMTP(unittest.TestCase):
frappe.local.outgoing_email_account = {}
# lowest preference given to email account with default incoming enabled
create_email_account(email_id="default_outgoing_enabled@gmail.com", password="***", enable_outgoing = 1, default_outgoing=1)
self.assertEqual(get_outgoing_email_account().email_id, "default_outgoing_enabled@gmail.com")
create_email_account(email_id="default_outgoing_enabled@gmail.com", password="password", enable_outgoing = 1, default_outgoing=1)
self.assertEqual(EmailAccount.find_outgoing().email_id, "default_outgoing_enabled@gmail.com")
frappe.local.outgoing_email_account = {}
# highest preference given to email account with append_to matching
create_email_account(email_id="append_to@gmail.com", password="***", enable_outgoing = 1, default_outgoing=1, append_to="Blog Post")
self.assertEqual(get_outgoing_email_account(append_to="Blog Post").email_id, "append_to@gmail.com")
create_email_account(email_id="append_to@gmail.com", password="password", enable_outgoing = 1, default_outgoing=1, append_to="Blog Post")
self.assertEqual(EmailAccount.find_outgoing(match_by_doctype="Blog Post").email_id, "append_to@gmail.com")
# add back the mail_server
frappe.conf['mail_server'] = mail_server

View file

@ -319,7 +319,7 @@ frappe.get_data_pill = (label, target_id=null, remove_action=null, image=null) =
frappe.get_modal = function(title, content) {
return $(`<div class="modal fade" style="overflow: auto;" tabindex="-1">
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div class="fill-width flex title-section">

View file

@ -90,16 +90,10 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({
});
this.$input.on("awesomplete-open", () => {
this.toggle_container_scroll('.modal-dialog', 'modal-dialog-scrollable');
this.toggle_container_scroll('.grid-form-body .form-area', 'scrollable');
this.autocomplete_open = true;
});
this.$input.on("awesomplete-close", () => {
this.toggle_container_scroll('.modal-dialog', 'modal-dialog-scrollable', true);
this.toggle_container_scroll('.grid-form-body .form-area', 'scrollable', true);
this.autocomplete_open = false;
});

View file

@ -241,16 +241,10 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({
});
this.$input.on("awesomplete-open", () => {
this.toggle_container_scroll('.modal-dialog', 'modal-dialog-scrollable');
this.toggle_container_scroll('.grid-form-body .form-area', 'scrollable');
this.autocomplete_open = true;
});
this.$input.on("awesomplete-close", () => {
this.toggle_container_scroll('.modal-dialog', 'modal-dialog-scrollable', true);
this.toggle_container_scroll('.grid-form-body .form-area', 'scrollable', true);
this.autocomplete_open = false;
});

View file

@ -66,7 +66,7 @@ export default class GridRowForm {
</div>
</div>
<div class="grid-form-body">
<div class="form-area scrollable"></div>
<div class="form-area"></div>
<div class="grid-footer-toolbar hidden-xs flex justify-between">
<div class="grid-shortcuts">
<span> ${frappe.utils.icon("keyboard", "md")} </span>

View file

@ -36,18 +36,6 @@ frappe.ui.FieldSelect = Class.extend({
var item = me.awesomplete.get_item(value);
me.$input.val(item.label);
});
this.$input.on("awesomplete-open", () => {
let modal = this.$input.parents('.modal-dialog')[0];
if (modal) {
$(modal).removeClass("modal-dialog-scrollable");
}
});
this.$input.on("awesomplete-close", () => {
let modal = this.$input.parents('.modal-dialog')[0];
if (modal) {
$(modal).addClass("modal-dialog-scrollable");
}
});
if(this.filter_fields) {
for(var i in this.filter_fields)

View file

@ -2,25 +2,50 @@ h5.modal-title {
margin: 0px !important;
}
body.modal-open {
overflow: auto;
height: auto;
min-height: 100%;
// Hack to fix incorrect padding applied by Bootstrap
body.modal-open[style^="padding-right"] {
padding-right: 12px !important;
header.navbar {
padding-right: 12px !important;
margin-right: -12px !important;
}
}
.modal {
// Same scrollbar as body
scrollbar-width: auto;
&::-webkit-scrollbar {
width: 12px;
height: 12px;
}
// Hide scrollbar on touch devices
@media(max-width: 991px) {
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
.modal-content {
border-color: var(--border-color);
}
.modal-header {
position: sticky;
top: 0;
z-index: 3;
background: inherit;
padding: var(--padding-md) var(--padding-lg);
padding-bottom: 0;
border-bottom: 0;
// padding-bottom: 0;
border-bottom: 1px solid var(--border-color);
.modal-title {
font-weight: 500;
line-height: 2em;
font-size: $font-size-lg;
max-width: calc(100% - 80px);
}
.modal-actions {
@ -60,9 +85,17 @@ body.modal-open {
}
}
.awesomplete ul {
z-index: 2;
}
.modal-footer {
position: sticky;
bottom: 0;
z-index: 1;
background: inherit;
padding: var(--padding-md) var(--padding-lg);
border-top: 0;
border-top: 1px solid var(--border-color);
justify-content: space-between;
button {

View file

@ -442,6 +442,11 @@ kbd {
/*rtl styles*/
.frappe-rtl {
text-align: right;
.modal-actions {
right: auto !important;
left: 5px;
}
input, textarea {
direction: rtl !important;
}

View file

@ -9,11 +9,6 @@ html {
}
/* Works on Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 6px;
height: 6px;
}
*::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb-color);
}
@ -23,7 +18,12 @@ html {
background: var(--scrollbar-track-color);
}
body::-webkit-scrollbar {
width: unset;
height: unset;
*::-webkit-scrollbar {
width: 6px;
height: 6px;
}
body::-webkit-scrollbar {
width: 12px;
height: 12px;
}

View file

@ -216,7 +216,7 @@ class TestCommands(BaseTestCommands):
# test 7: take a backup with frappe.conf.backup.includes
self.execute(
"bench --site {site} set-config backup '{includes}' --as-dict",
"bench --site {site} set-config backup '{includes}' --parse",
{"includes": json.dumps(backup["includes"])},
)
self.execute("bench --site {site} backup --verbose")
@ -226,7 +226,7 @@ class TestCommands(BaseTestCommands):
# test 8: take a backup with frappe.conf.backup.excludes
self.execute(
"bench --site {site} set-config backup '{excludes}' --as-dict",
"bench --site {site} set-config backup '{excludes}' --parse",
{"excludes": json.dumps(backup["excludes"])},
)
self.execute("bench --site {site} backup --verbose")
@ -376,6 +376,32 @@ class TestCommands(BaseTestCommands):
self.execute("bench --site {site} list-apps -f json")
self.assertIsInstance(json.loads(self.stdout), dict)
def test_show_config(self):
# test 1: sanity check for command
self.execute("bench --site all show-config")
self.assertEquals(self.returncode, 0)
# test 2: test keys in table text
self.execute(
"bench --site {site} set-config test_key '{second_order}' --parse",
{"second_order": json.dumps({"test_key": "test_value"})},
)
self.execute("bench --site {site} show-config")
self.assertEquals(self.returncode, 0)
self.assertIn("test_key.test_key", self.stdout.split())
self.assertIn("test_value", self.stdout.split())
# test 3: parse json format
self.execute("bench --site all show-config --format json")
self.assertEquals(self.returncode, 0)
self.assertIsInstance(json.loads(self.stdout), dict)
self.execute("bench --site {site} show-config --format json")
self.assertIsInstance(json.loads(self.stdout), dict)
self.execute("bench --site {site} show-config -f json")
self.assertIsInstance(json.loads(self.stdout), dict)
def test_get_bench_relative_path(self):
bench_path = frappe.utils.get_bench_path()
test1_path = os.path.join(bench_path, "test1.txt")

View file

@ -1,11 +1,10 @@
import functools
import requests
from terminaltables import AsciiTable
@functools.lru_cache(maxsize=1024)
def get_first_party_apps():
"""Get list of all apps under orgs: frappe. erpnext from GitHub"""
import requests
apps = []
for org in ["frappe", "erpnext"]:
req = requests.get(f"https://api.github.com/users/{org}/repos", {"type": "sources", "per_page": 200})
@ -15,6 +14,8 @@ def get_first_party_apps():
def render_table(data):
from terminaltables import AsciiTable
print(AsciiTable(data).table)
@ -49,3 +50,9 @@ def log(message, colour=''):
colour = colours.get(colour, "")
end_line = '\033[0m'
print(colour + message + end_line)
def warn(message, category=None):
from warnings import warn
warn(message=message, category=category, stacklevel=2)

View file

@ -4,12 +4,14 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import cstr, encode
import os
import sys
import inspect
import traceback
import functools
import frappe
from frappe.utils import cstr, encode
import inspect
import linecache
import pydoc
import cgitb
@ -190,3 +192,45 @@ def clear_old_snapshots():
def get_error_snapshot_path():
return frappe.get_site_path('error-snapshots')
def get_default_args(func):
"""Get default arguments of a function from its signature.
"""
signature = inspect.signature(func)
return {k: v.default
for k, v in signature.parameters.items() if v.default is not inspect.Parameter.empty}
def raise_error_on_no_output(error_message, error_type=None, keep_quiet=None):
"""Decorate any function to throw error incase of missing output.
TODO: Remove keep_quiet flag after testing and fixing sendmail flow.
:param error_message: error message to raise
:param error_type: type of error to raise
:param keep_quiet: control error raising with external factor.
:type error_message: str
:type error_type: Exception Class
:type keep_quiet: function
>>> @raise_error_on_no_output("Ingradients missing")
... def get_indradients(_raise_error=1): return
...
>>> get_indradients()
`Exception Name`: Ingradients missing
"""
def decorator_raise_error_on_no_output(func):
@functools.wraps(func)
def wrapper_raise_error_on_no_output(*args, **kwargs):
response = func(*args, **kwargs)
if callable(keep_quiet) and keep_quiet():
return response
default_kwargs = get_default_args(func)
default_raise_error = default_kwargs.get('_raise_error')
raise_error = kwargs.get('_raise_error') if '_raise_error' in kwargs else default_raise_error
if (not response) and raise_error:
frappe.throw(error_message, error_type or Exception)
return response
return wrapper_raise_error_on_no_output
return decorator_raise_error_on_no_output

View file

@ -163,11 +163,11 @@ class PersonalDataDeletionRequest(Document):
def redact_full_match_data(self, ref, email):
"""Replaces the entire field value by the values set in the anonymization_value_map"""
filter_by = ref["filter_by"]
filter_by = ref.get("filter_by", "owner")
docs = frappe.get_all(
ref["doctype"],
filters={filter_by: ("like", "%" + email + "%")},
filters={filter_by: email},
fields=["name", filter_by],
)
@ -205,7 +205,7 @@ class PersonalDataDeletionRequest(Document):
return anonymize_fields_dict
def redact_doc(self, doc, ref):
filter_by = ref["filter_by"]
filter_by = ref.get("filter_by", "owner")
meta = frappe.get_meta(ref["doctype"])
filter_by_meta = meta.get_field(filter_by)

View file

@ -21,7 +21,7 @@ $('.dropdown-menu a.dropdown-toggle').on('click', function (e) {
frappe.get_modal = function (title, content) {
return $(
`<div class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${title}</h5>