Merge branch 'develop' of https://github.com/frappe/frappe into custom_append_to
This commit is contained in:
commit
d9c490fa29
46 changed files with 933 additions and 2868 deletions
63
cypress/integration/depends_on.js
Normal file
63
cypress/integration/depends_on.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
context('Depends On', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk');
|
||||
});
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk');
|
||||
cy.window().its('frappe').then(frappe => {
|
||||
frappe.call('frappe.tests.ui_test_helpers.create_doctype', {
|
||||
name: 'Test Depends On',
|
||||
fields: [
|
||||
{
|
||||
"label": "Test Field",
|
||||
"fieldname": "test_field",
|
||||
"fieldtype": "Data",
|
||||
},
|
||||
{
|
||||
"label": "Dependant Field",
|
||||
"fieldname": "dependant_field",
|
||||
"fieldtype": "Data",
|
||||
"mandatory_depends_on": "eval:doc.test_field=='Some Value'",
|
||||
"read_only_depends_on": "eval:doc.test_field=='Some Other Value'",
|
||||
},
|
||||
{
|
||||
"label": "Display Dependant Field",
|
||||
"fieldname": "display_dependant_field",
|
||||
"fieldtype": "Data",
|
||||
'depends_on': "eval:doc.test_field=='Value'"
|
||||
},
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should set the field as mandatory depending on other fields value', () => {
|
||||
cy.new_form('Test Depends On');
|
||||
cy.fill_field('test_field', 'Some Value');
|
||||
cy.get('button.primary-action').contains('Save').click();
|
||||
cy.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('be.visible');
|
||||
cy.get('body').click();
|
||||
cy.fill_field('test_field', 'Random value');
|
||||
cy.get('button.primary-action').contains('Save').click();
|
||||
cy.get('.msgprint-dialog .modal-title').contains('Missing Fields').should('not.be.visible');
|
||||
});
|
||||
it('should set the field as read only depending on other fields value', () => {
|
||||
cy.new_form('Test Depends On');
|
||||
cy.fill_field('dependant_field', 'Some Value');
|
||||
cy.fill_field('test_field', 'Some Other Value');
|
||||
cy.get('body').click();
|
||||
cy.get('.control-input [data-fieldname="dependant_field"]').should('be.disabled');
|
||||
cy.fill_field('test_field', 'Random Value');
|
||||
cy.get('body').click();
|
||||
cy.get('.control-input [data-fieldname="dependant_field"]').should('not.be.disabled');
|
||||
});
|
||||
it('should display the field depending on other fields value', () => {
|
||||
cy.new_form('Test Depends On');
|
||||
cy.get('.control-input [data-fieldname="display_dependant_field"]').should('not.be.visible');
|
||||
cy.get('.control-input [data-fieldname="test_field"]').clear();
|
||||
cy.fill_field('test_field', 'Value');
|
||||
cy.get('body').click();
|
||||
cy.get('.control-input [data-fieldname="display_dependant_field"]').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
|
@ -321,10 +321,19 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, indicator=None,
|
|||
return
|
||||
|
||||
if as_table and type(msg) in (list, tuple):
|
||||
out.msg = '<table border="1px" style="border-collapse: collapse" cellpadding="2px">' + ''.join(['<tr>'+''.join(['<td>%s</td>' % c for c in r])+'</tr>' for r in msg]) + '</table>'
|
||||
|
||||
if flags.print_messages and out.msg:
|
||||
print("Message: " + repr(out.msg).encode("utf-8"))
|
||||
table_rows = ''
|
||||
for row in msg:
|
||||
table_row_data = ''
|
||||
for data in row:
|
||||
table_row_data += '<td>{}</td>'.format(data)
|
||||
table_rows += '<tr>{}</tr>'.format(table_row_data)
|
||||
|
||||
out.message = '''<table class="table table-bordered"
|
||||
style="margin: 0;">{}</table>'''.format(table_rows)
|
||||
|
||||
if flags.print_messages and out.message:
|
||||
print("Message: " + repr(out.message).encode("utf-8"))
|
||||
|
||||
if title:
|
||||
out.title = title
|
||||
|
|
@ -363,7 +372,6 @@ def throw(msg, exc=ValidationError, title=None):
|
|||
msgprint(msg, raise_exception=exc, title=title, indicator='red')
|
||||
|
||||
def emit_js(js, user=False, **kwargs):
|
||||
from frappe.realtime import publish_realtime
|
||||
if user == False:
|
||||
user = session.user
|
||||
publish_realtime('eval_js', js, user=user, **kwargs)
|
||||
|
|
@ -767,8 +775,8 @@ def get_meta_module(doctype):
|
|||
import frappe.modules
|
||||
return frappe.modules.load_doctype_module(doctype)
|
||||
|
||||
def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reload=False,
|
||||
ignore_permissions=False, flags=None, ignore_on_trash=False, ignore_missing=True):
|
||||
def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None,
|
||||
for_reload=False, ignore_permissions=False, flags=None, ignore_on_trash=False, ignore_missing=True):
|
||||
"""Delete a document. Calls `frappe.model.delete_doc.delete_doc`.
|
||||
|
||||
:param doctype: DocType of document to be delete.
|
||||
|
|
@ -805,8 +813,8 @@ def reload_doc(module, dt=None, dn=None, force=False, reset_permissions=False):
|
|||
|
||||
def rename_doc(*args, **kwargs):
|
||||
"""Rename a document. Calls `frappe.model.rename_doc.rename_doc`"""
|
||||
from frappe.model.rename_doc import rename_doc
|
||||
return rename_doc(*args, **kwargs)
|
||||
from frappe.model.rename_doc import rename_doc as _rename_doc
|
||||
return _rename_doc(*args, **kwargs)
|
||||
|
||||
def get_module(modulename):
|
||||
"""Returns a module object for given Python module name using `importlib.import_module`."""
|
||||
|
|
@ -814,11 +822,11 @@ def get_module(modulename):
|
|||
|
||||
def scrub(txt):
|
||||
"""Returns sluggified string. e.g. `Sales Order` becomes `sales_order`."""
|
||||
return txt.replace(' ','_').replace('-', '_').lower()
|
||||
return txt.replace(' ', '_').replace('-', '_').lower()
|
||||
|
||||
def unscrub(txt):
|
||||
"""Returns titlified string. e.g. `sales_order` becomes `Sales Order`."""
|
||||
return txt.replace('_',' ').replace('-', ' ').title()
|
||||
return txt.replace('_', ' ').replace('-', ' ').title()
|
||||
|
||||
def get_module_path(module, *joins):
|
||||
"""Get the path of the given module name.
|
||||
|
|
@ -980,7 +988,8 @@ def setup_module_map():
|
|||
if not (local.app_modules and local.module_app):
|
||||
local.module_app, local.app_modules = {}, {}
|
||||
for app in get_all_apps(True):
|
||||
if app=="webnotes": app="frappe"
|
||||
if app == "webnotes":
|
||||
app = "frappe"
|
||||
local.app_modules.setdefault(app, [])
|
||||
for module in get_module_list(app):
|
||||
module = scrub(module)
|
||||
|
|
@ -999,7 +1008,10 @@ def get_file_items(path, raise_not_found=False, ignore_empty_lines=True):
|
|||
if content:
|
||||
content = frappe.utils.strip(content)
|
||||
|
||||
return [p.strip() for p in content.splitlines() if (not ignore_empty_lines) or (p.strip() and not p.startswith("#"))]
|
||||
return [
|
||||
p.strip() for p in content.splitlines()
|
||||
if (not ignore_empty_lines) or (p.strip() and not p.startswith("#"))
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
|
|
@ -1161,8 +1173,8 @@ def compare(val1, condition, val2):
|
|||
import frappe.utils
|
||||
return frappe.utils.compare(val1, condition, val2)
|
||||
|
||||
def respond_as_web_page(title, html, success=None, http_status_code=None,
|
||||
context=None, indicator_color=None, primary_action='/', primary_label = None, fullpage=False,
|
||||
def respond_as_web_page(title, html, success=None, http_status_code=None, context=None,
|
||||
indicator_color=None, primary_action='/', primary_label = None, fullpage=False,
|
||||
width=None, template='message'):
|
||||
"""Send response as a web page with a message rather than JSON. Used to show permission errors etc.
|
||||
|
||||
|
|
@ -1351,7 +1363,8 @@ def format(*args, **kwargs):
|
|||
import frappe.utils.formatters
|
||||
return frappe.utils.formatters.format_value(*args, **kwargs)
|
||||
|
||||
def get_print(doctype=None, name=None, print_format=None, style=None, html=None, as_pdf=False, doc=None, output = None, no_letterhead = 0, password=None):
|
||||
def get_print(doctype=None, name=None, print_format=None, style=None,
|
||||
html=None, as_pdf=False, doc=None, output=None, no_letterhead=0, password=None):
|
||||
"""Get Print Format for given document.
|
||||
|
||||
:param doctype: DocType of document.
|
||||
|
|
@ -1382,7 +1395,8 @@ def get_print(doctype=None, name=None, print_format=None, style=None, html=None,
|
|||
else:
|
||||
return html
|
||||
|
||||
def attach_print(doctype, name, file_name=None, print_format=None, style=None, html=None, doc=None, lang=None, print_letterhead=True, password=None):
|
||||
def attach_print(doctype, name, file_name=None, print_format=None,
|
||||
style=None, html=None, doc=None, lang=None, print_letterhead=True, password=None):
|
||||
from frappe.utils import scrub_urls
|
||||
|
||||
if not file_name: file_name = name
|
||||
|
|
@ -1398,16 +1412,28 @@ def attach_print(doctype, name, file_name=None, print_format=None, style=None, h
|
|||
|
||||
no_letterhead = not print_letterhead
|
||||
|
||||
kwargs = dict(
|
||||
print_format=print_format,
|
||||
style=style,
|
||||
html=html,
|
||||
doc=doc,
|
||||
no_letterhead=no_letterhead,
|
||||
password=password
|
||||
)
|
||||
|
||||
content = ''
|
||||
if int(print_settings.send_print_as_pdf or 0):
|
||||
out = {
|
||||
"fname": file_name + ".pdf",
|
||||
"fcontent": get_print(doctype, name, print_format=print_format, style=style, html=html, as_pdf=True, doc=doc, no_letterhead=no_letterhead, password=password)
|
||||
}
|
||||
ext = ".pdf"
|
||||
kwargs["as_pdf"] = True
|
||||
content = get_print(doctype, name, **kwargs)
|
||||
else:
|
||||
out = {
|
||||
"fname": file_name + ".html",
|
||||
"fcontent": scrub_urls(get_print(doctype, name, print_format=print_format, style=style, html=html, doc=doc, no_letterhead=no_letterhead, password=password)).encode("utf-8")
|
||||
}
|
||||
ext = ".html"
|
||||
content = scrub_urls(get_print(doctype, name, **kwargs)).encode('utf-8')
|
||||
|
||||
out = {
|
||||
"fname": file_name + ext,
|
||||
"fcontent": content
|
||||
}
|
||||
|
||||
local.flags.ignore_print_permissions = False
|
||||
#reset lang to original local lang
|
||||
|
|
@ -1526,7 +1552,12 @@ def log_error(message=None, title=None):
|
|||
method=title)).insert(ignore_permissions=True)
|
||||
|
||||
def get_desk_link(doctype, name):
|
||||
return '<a href="#Form/{0}/{1}" style="font-weight: bold;">{2} {1}</a>'.format(doctype, name, _(doctype))
|
||||
html = '<a href="#Form/{doctype}/{name}" style="font-weight: bold;">{doctype_local} {name}</a>'
|
||||
return html.format(
|
||||
doctype=doctype,
|
||||
name=name,
|
||||
doctype_local=_(doctype)
|
||||
)
|
||||
|
||||
def bold(text):
|
||||
return '<b>{0}</b>'.format(text)
|
||||
|
|
@ -1545,10 +1576,9 @@ def safe_eval(code, eval_globals=None, eval_locals=None):
|
|||
|
||||
if not eval_globals:
|
||||
eval_globals = {}
|
||||
|
||||
eval_globals['__builtins__'] = {}
|
||||
|
||||
eval_globals.update(whitelisted_globals)
|
||||
|
||||
return eval(code, eval_globals, eval_locals)
|
||||
|
||||
def get_system_settings(key):
|
||||
|
|
@ -1560,9 +1590,11 @@ def get_active_domains():
|
|||
from frappe.core.doctype.domain_settings.domain_settings import get_active_domains
|
||||
return get_active_domains()
|
||||
|
||||
def get_version(doctype, name, limit = None, head = False, raise_err = True):
|
||||
def get_version(doctype, name, limit=None, head=False, raise_err=True):
|
||||
'''
|
||||
Returns a list of version information of a given DocType (Applicable only if DocType has changes tracked).
|
||||
Returns a list of version information of a given DocType.
|
||||
|
||||
Note: Applicable only if DocType has changes tracked.
|
||||
|
||||
Example
|
||||
>>> frappe.get_version('User', 'foobar@gmail.com')
|
||||
|
|
@ -1575,34 +1607,29 @@ def get_version(doctype, name, limit = None, head = False, raise_err = True):
|
|||
}
|
||||
]
|
||||
'''
|
||||
meta = get_meta(doctype)
|
||||
meta = get_meta(doctype)
|
||||
if meta.track_changes:
|
||||
names = db.sql("""
|
||||
SELECT name from tabVersion
|
||||
WHERE ref_doctype = '{doctype}' AND docname = '{name}'
|
||||
{order_by}
|
||||
{limit}
|
||||
""".format(
|
||||
doctype = doctype,
|
||||
name = name,
|
||||
order_by = 'ORDER BY creation' if head else '',
|
||||
limit = 'LIMIT {limit}'.format(limit = limit) if limit else ''
|
||||
))
|
||||
names = db.get_all('Version', filters={
|
||||
'ref_doctype': doctype,
|
||||
'docname': name,
|
||||
'order_by': 'creation' if head else None,
|
||||
'limit': limit
|
||||
}, as_list=1)
|
||||
|
||||
from frappe.chat.util import squashify, dictify, safe_json_loads
|
||||
|
||||
versions = [ ]
|
||||
versions = []
|
||||
|
||||
for name in names:
|
||||
name = squashify(name)
|
||||
doc = get_doc('Version', name)
|
||||
doc = get_doc('Version', name)
|
||||
|
||||
data = doc.data
|
||||
data = safe_json_loads(data)
|
||||
data = dictify(dict(
|
||||
version = data,
|
||||
user = doc.owner,
|
||||
creation = doc.creation
|
||||
version=data,
|
||||
user=doc.owner,
|
||||
creation=doc.creation
|
||||
))
|
||||
|
||||
versions.append(data)
|
||||
|
|
@ -1610,16 +1637,14 @@ def get_version(doctype, name, limit = None, head = False, raise_err = True):
|
|||
return versions
|
||||
else:
|
||||
if raise_err:
|
||||
raise ValueError('{doctype} has no versions tracked.'.format(
|
||||
doctype = doctype
|
||||
))
|
||||
raise ValueError(_('{0} has no versions tracked.').format(doctype))
|
||||
|
||||
@whitelist(allow_guest = True)
|
||||
@whitelist(allow_guest=True)
|
||||
def ping():
|
||||
return "pong"
|
||||
|
||||
|
||||
def safe_encode(param, encoding = 'utf-8'):
|
||||
def safe_encode(param, encoding='utf-8'):
|
||||
try:
|
||||
param = param.encode(encoding)
|
||||
except Exception:
|
||||
|
|
@ -1627,7 +1652,7 @@ def safe_encode(param, encoding = 'utf-8'):
|
|||
return param
|
||||
|
||||
|
||||
def safe_decode(param, encoding = 'utf-8'):
|
||||
def safe_decode(param, encoding='utf-8'):
|
||||
try:
|
||||
param = param.decode(encoding)
|
||||
except Exception:
|
||||
|
|
@ -1638,9 +1663,9 @@ def parse_json(val):
|
|||
from frappe.utils import parse_json
|
||||
return parse_json(val)
|
||||
|
||||
def mock(type, size = 1, locale = 'en'):
|
||||
results = [ ]
|
||||
faker = Faker(locale)
|
||||
def mock(type, size=1, locale='en'):
|
||||
results = []
|
||||
faker = Faker(locale)
|
||||
if not type in dir(faker):
|
||||
raise ValueError('Not a valid mock type.')
|
||||
else:
|
||||
|
|
@ -1649,7 +1674,4 @@ def mock(type, size = 1, locale = 'en'):
|
|||
results.append(data)
|
||||
|
||||
from frappe.chat.util import squashify
|
||||
|
||||
results = squashify(results)
|
||||
|
||||
return results
|
||||
return squashify(results)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from frappe.desk.form import assign_to
|
|||
from frappe.utils.jinja import validate_template
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from frappe.utils.user import get_system_managers
|
||||
from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_day, get_first_day
|
||||
from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_day, get_first_day, month_diff
|
||||
from frappe.model.document import Document
|
||||
from frappe.core.doctype.communication.email import make
|
||||
from frappe.utils.background_jobs import get_jobs
|
||||
|
|
@ -48,7 +48,7 @@ class AutoRepeat(Document):
|
|||
if self.disabled:
|
||||
self.next_schedule_date = None
|
||||
else:
|
||||
self.next_schedule_date = get_next_schedule_date(self.start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, self.end_date)
|
||||
self.next_schedule_date = get_next_schedule_date(self.start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, self.end_date)
|
||||
|
||||
def unlink_if_applicable(self):
|
||||
if self.status == 'Completed' or self.disabled:
|
||||
|
|
@ -107,27 +107,27 @@ class AutoRepeat(Document):
|
|||
end_date = getdate(self.end_date)
|
||||
|
||||
if not self.end_date:
|
||||
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day)
|
||||
next_date = get_next_schedule_date(start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day)
|
||||
row = {
|
||||
"reference_document": self.reference_document,
|
||||
"frequency": self.frequency,
|
||||
"next_scheduled_date": start_date
|
||||
"next_scheduled_date": next_date
|
||||
}
|
||||
schedule_details.append(row)
|
||||
start_date = get_next_schedule_date(start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day)
|
||||
|
||||
if self.end_date:
|
||||
start_date = get_next_schedule_date(
|
||||
start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, for_full_schedule=True)
|
||||
while (getdate(start_date) < getdate(end_date)):
|
||||
next_date = get_next_schedule_date(
|
||||
start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, for_full_schedule=True)
|
||||
|
||||
while (getdate(next_date) < getdate(end_date)):
|
||||
row = {
|
||||
"reference_document" : self.reference_document,
|
||||
"frequency" : self.frequency,
|
||||
"next_scheduled_date" : start_date
|
||||
"next_scheduled_date" : next_date
|
||||
}
|
||||
schedule_details.append(row)
|
||||
start_date = get_next_schedule_date(
|
||||
start_date, self.frequency, self.repeat_on_day, self.repeat_on_last_day, end_date, for_full_schedule=True)
|
||||
next_date = get_next_schedule_date(
|
||||
next_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, end_date, for_full_schedule=True)
|
||||
|
||||
return schedule_details
|
||||
|
||||
|
|
@ -268,8 +268,12 @@ class AutoRepeat(Document):
|
|||
)
|
||||
|
||||
|
||||
def get_next_schedule_date(start_date, frequency, repeat_on_day, repeat_on_last_day=False, end_date=None, for_full_schedule=False):
|
||||
month_count = month_map.get(frequency)
|
||||
def get_next_schedule_date(schedule_date, frequency, start_date, repeat_on_day=None, repeat_on_last_day=False, end_date=None, for_full_schedule=False):
|
||||
if month_map.get(frequency):
|
||||
month_count = month_map.get(frequency) + month_diff(schedule_date, start_date) - 1
|
||||
else:
|
||||
month_count = 0
|
||||
|
||||
day_count = 0
|
||||
if month_count and repeat_on_last_day:
|
||||
next_date = get_next_date(start_date, month_count, 31)
|
||||
|
|
@ -288,7 +292,9 @@ def get_next_schedule_date(start_date, frequency, repeat_on_day, repeat_on_last_
|
|||
# next schedule date should be after or on current date
|
||||
if not for_full_schedule:
|
||||
while getdate(next_date) < getdate(today()):
|
||||
next_date = get_next_date(next_date, month_count, day_count)
|
||||
if month_count:
|
||||
month_count += month_map.get(frequency)
|
||||
next_date = get_next_date(start_date, month_count, day_count)
|
||||
|
||||
return next_date
|
||||
|
||||
|
|
@ -316,8 +322,7 @@ def create_repeated_entries(data):
|
|||
|
||||
if schedule_date == current_date and not doc.disabled:
|
||||
doc.create_documents()
|
||||
schedule_date = get_next_schedule_date(schedule_date, doc.frequency, doc.repeat_on_day, doc.repeat_on_last_day, doc.end_date)
|
||||
|
||||
schedule_date = get_next_schedule_date(schedule_date, doc.frequency, doc.start_date, doc.repeat_on_day, doc.repeat_on_last_day, doc.end_date)
|
||||
if schedule_date and not doc.disabled:
|
||||
frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# MIT License. See license.txt
|
||||
|
||||
from __future__ import unicode_literals, print_function
|
||||
import os, frappe, json, shutil, re, warnings
|
||||
import os, frappe, json, shutil, re, warnings, tempfile
|
||||
from os.path import exists as path_exists, join as join_path, abspath, isdir
|
||||
from distutils.spawn import find_executable
|
||||
from six import iteritems, text_type
|
||||
|
|
@ -12,6 +12,51 @@ from frappe.utils.minify import JavascriptMinify
|
|||
Build the `public` folders and setup languages
|
||||
"""
|
||||
|
||||
|
||||
def symlink(target, link_name, overwrite=False):
|
||||
'''
|
||||
Create a symbolic link named link_name pointing to target.
|
||||
If link_name exists then FileExistsError is raised, unless overwrite=True.
|
||||
When trying to overwrite a directory, IsADirectoryError is raised.
|
||||
|
||||
Source: https://stackoverflow.com/a/55742015/10309266
|
||||
'''
|
||||
|
||||
if not overwrite:
|
||||
os.symlink(target, linkname)
|
||||
return
|
||||
|
||||
# os.replace() may fail if files are on different filesystems
|
||||
link_dir = os.path.dirname(link_name)
|
||||
|
||||
# Create link to target with temporary filename
|
||||
while True:
|
||||
temp_link_name = tempfile.mktemp(dir=link_dir)
|
||||
|
||||
# os.* functions mimic as closely as possible system functions
|
||||
# The POSIX symlink() returns EEXIST if link_name already exists
|
||||
# https://pubs.opengroup.org/onlinepubs/9699919799/functions/symlink.html
|
||||
try:
|
||||
os.symlink(target, temp_link_name)
|
||||
break
|
||||
except FileExistsError:
|
||||
pass
|
||||
|
||||
# Replace link_name with temp_link_name
|
||||
try:
|
||||
# Pre-empt os.replace on a directory with a nicer message
|
||||
if os.path.isdir(link_name):
|
||||
raise IsADirectoryError("Cannot symlink over existing directory: '{}'".format(link_name))
|
||||
try:
|
||||
os.replace(temp_link_name, link_name)
|
||||
except AttributeError:
|
||||
os.renames(temp_link_name, link_name)
|
||||
except:
|
||||
if os.path.islink(temp_link_name):
|
||||
os.remove(temp_link_name)
|
||||
raise
|
||||
|
||||
|
||||
app_paths = None
|
||||
def setup():
|
||||
global app_paths
|
||||
|
|
@ -118,7 +163,7 @@ def make_asset_dirs(make_copy=False, restore=False):
|
|||
else:
|
||||
shutil.rmtree(target)
|
||||
try:
|
||||
os.symlink(source, target)
|
||||
symlink(source, target, overwrite=True)
|
||||
except OSError:
|
||||
print('Cannot link {} to {}'.format(source, target))
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
# imports - standard imports
|
||||
import json
|
||||
|
||||
# imports - module imports
|
||||
from frappe.model.document import Document
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe import _
|
||||
import frappe
|
||||
|
||||
# imports - frappe module imports
|
||||
from frappe.chat import authenticate
|
||||
from frappe.chat import authenticate
|
||||
from frappe.core.doctype.version.version import get_diff
|
||||
from frappe.chat.doctype.chat_message import chat_message
|
||||
from frappe.chat.doctype.chat_message import chat_message
|
||||
from frappe.chat.util import (
|
||||
safe_json_loads,
|
||||
dictify,
|
||||
|
|
@ -22,13 +19,14 @@ from frappe.chat.util import (
|
|||
|
||||
session = frappe.session
|
||||
|
||||
def is_direct(owner, other, bidirectional = False):
|
||||
|
||||
def is_direct(owner, other, bidirectional=False):
|
||||
def get_room(owner, other):
|
||||
room = frappe.get_all('Chat Room', filters = [
|
||||
['Chat Room', 'type' , 'in', ('Direct', 'Visitor')],
|
||||
['Chat Room', 'owner', '=' , owner],
|
||||
['Chat Room User', 'user' , '=' , other]
|
||||
], distinct = True)
|
||||
room = frappe.get_all('Chat Room', filters=[
|
||||
['Chat Room', 'type', 'in', ('Direct', 'Visitor')],
|
||||
['Chat Room', 'owner', '=', owner],
|
||||
['Chat Room User', 'user', '=', other]
|
||||
], distinct=True)
|
||||
|
||||
return room
|
||||
|
||||
|
|
@ -38,7 +36,8 @@ def is_direct(owner, other, bidirectional = False):
|
|||
|
||||
return exists
|
||||
|
||||
def get_chat_room_user_set(users, filter_ = None):
|
||||
|
||||
def get_chat_room_user_set(users, filter_=None):
|
||||
seen, uset = set(), list()
|
||||
|
||||
for u in users:
|
||||
|
|
@ -48,12 +47,13 @@ def get_chat_room_user_set(users, filter_ = None):
|
|||
|
||||
return uset
|
||||
|
||||
|
||||
class ChatRoom(Document):
|
||||
def validate(self):
|
||||
if self.is_new():
|
||||
users = get_chat_room_user_set(self.users, filter_ = lambda u: u.user != session.user)
|
||||
users = get_chat_room_user_set(self.users, filter_=lambda u: u.user != session.user)
|
||||
self.update(dict(
|
||||
users = users
|
||||
users=users
|
||||
))
|
||||
|
||||
if self.type == "Direct":
|
||||
|
|
@ -63,7 +63,7 @@ class ChatRoom(Document):
|
|||
other = squashify(self.users)
|
||||
|
||||
if self.is_new():
|
||||
if is_direct(self.owner, other.user, bidirectional = True):
|
||||
if is_direct(self.owner, other.user, bidirectional=True):
|
||||
frappe.throw(_('Direct room with {0} already exists.').format(other.user))
|
||||
|
||||
if self.type == "Group" and not self.room_name:
|
||||
|
|
@ -74,40 +74,44 @@ class ChatRoom(Document):
|
|||
before = self.get_doc_before_save()
|
||||
if not before: return
|
||||
|
||||
after = self
|
||||
diff = dictify(get_diff(before, after))
|
||||
after = self
|
||||
diff = dictify(get_diff(before, after))
|
||||
if diff:
|
||||
update = { }
|
||||
update = {}
|
||||
for changed in diff.changed:
|
||||
field, old, new = changed
|
||||
|
||||
if field == 'last_message':
|
||||
new = chat_message.get(new)
|
||||
|
||||
update.update({ field: new })
|
||||
update.update({field: new})
|
||||
|
||||
if diff.added or diff.removed:
|
||||
update.update(dict(users = [u.user for u in self.users]))
|
||||
update.update(dict(users=[u.user for u in self.users]))
|
||||
|
||||
update = dict(room = self.name, data = update)
|
||||
update = dict(room=self.name, data=update)
|
||||
|
||||
frappe.publish_realtime('frappe.chat.room:update', update, room = self.name, after_commit = True)
|
||||
frappe.publish_realtime('frappe.chat.room:update', update, room=self.name,
|
||||
after_commit=True)
|
||||
|
||||
@frappe.whitelist(allow_guest = True)
|
||||
def get(user, rooms = None, fields = None, filters = None):
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get(user=None, token=None, rooms=None, fields=None, filters=None):
|
||||
# There is this horrible bug out here.
|
||||
# Looks like if frappe.call sends optional arguments (not in right order), the argument turns to an empty string.
|
||||
# Looks like if frappe.call sends optional arguments (not in right order),
|
||||
# the argument turns to an empty string.
|
||||
# I'm not even going to think searching for it.
|
||||
# Hence, the hack was get_if_empty (previous assign_if_none)
|
||||
# - Achilles Rasquinha achilles@frappe.io
|
||||
authenticate(user)
|
||||
data = user or token
|
||||
authenticate(data)
|
||||
|
||||
rooms, fields, filters = safe_json_loads(rooms, fields, filters)
|
||||
|
||||
rooms = listify(get_if_empty(rooms, [ ]))
|
||||
fields = listify(get_if_empty(fields, [ ]))
|
||||
rooms = listify(get_if_empty(rooms, []))
|
||||
fields = listify(get_if_empty(fields, []))
|
||||
|
||||
const = [ ] # constraints
|
||||
const = [] # constraints
|
||||
if rooms:
|
||||
const.append(['Chat Room', 'name', 'in', rooms])
|
||||
if filters:
|
||||
|
|
@ -117,24 +121,24 @@ def get(user, rooms = None, fields = None, filters = None):
|
|||
const.append(filters)
|
||||
|
||||
default = ['name', 'type', 'room_name', 'creation', 'owner', 'avatar']
|
||||
handle = ['users', 'last_message']
|
||||
handle = ['users', 'last_message']
|
||||
|
||||
param = [f for f in fields if f not in handle]
|
||||
param = [f for f in fields if f not in handle]
|
||||
|
||||
rooms = frappe.get_all('Chat Room',
|
||||
or_filters = [
|
||||
['Chat Room', 'owner', '=', user],
|
||||
['Chat Room User', 'user', '=', user]
|
||||
],
|
||||
filters = const,
|
||||
fields = param + ['name'] if param else default,
|
||||
distinct = True
|
||||
)
|
||||
rooms = frappe.get_all('Chat Room',
|
||||
or_filters=[
|
||||
['Chat Room', 'owner', '=', frappe.session.user],
|
||||
['Chat Room User', 'user', '=', frappe.session.user]
|
||||
],
|
||||
filters=const,
|
||||
fields=param + ['name'] if param else default,
|
||||
distinct=True
|
||||
)
|
||||
|
||||
if not fields or 'users' in fields:
|
||||
for i, r in enumerate(rooms):
|
||||
droom = frappe.get_doc('Chat Room', r.name)
|
||||
rooms[i]['users'] = [ ]
|
||||
rooms[i]['users'] = []
|
||||
|
||||
for duser in droom.users:
|
||||
rooms[i]['users'].append(duser.user)
|
||||
|
|
@ -151,46 +155,47 @@ def get(user, rooms = None, fields = None, filters = None):
|
|||
|
||||
return rooms
|
||||
|
||||
@frappe.whitelist(allow_guest = True)
|
||||
def create(kind, owner, users = None, name = None):
|
||||
authenticate(owner)
|
||||
|
||||
users = safe_json_loads(users)
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def create(kind, token, users=None, name=None):
|
||||
authenticate(token)
|
||||
|
||||
users = safe_json_loads(users)
|
||||
create = True
|
||||
|
||||
if kind == 'Visitor':
|
||||
room = squashify(frappe.db.sql("""
|
||||
SELECT name
|
||||
FROM `tabChat Room`
|
||||
WHERE owner = "{owner}"
|
||||
""".format(owner = owner), as_dict = True))
|
||||
WHERE owner=%s
|
||||
""", (frappe.session.user), as_dict=True))
|
||||
|
||||
if room:
|
||||
room = frappe.get_doc('Chat Room', room.name)
|
||||
room = frappe.get_doc('Chat Room', room.name)
|
||||
create = False
|
||||
|
||||
if create:
|
||||
room = frappe.new_doc('Chat Room')
|
||||
room.type = kind
|
||||
room.owner = owner
|
||||
room = frappe.new_doc('Chat Room')
|
||||
room.type = kind
|
||||
room.owner = frappe.session.user
|
||||
room.room_name = name
|
||||
|
||||
dusers = [ ]
|
||||
dusers = []
|
||||
|
||||
if kind != 'Visitor':
|
||||
if users:
|
||||
users = listify(users)
|
||||
users = listify(users)
|
||||
for user in users:
|
||||
duser = frappe.new_doc('Chat Room User')
|
||||
duser = frappe.new_doc('Chat Room User')
|
||||
duser.user = user
|
||||
dusers.append(duser)
|
||||
|
||||
room.users = dusers
|
||||
else:
|
||||
dsettings = frappe.get_single('Website Settings')
|
||||
dsettings = frappe.get_single('Website Settings')
|
||||
room.room_name = dsettings.chat_room_name
|
||||
|
||||
users = [user for user in room.users] if hasattr(room, 'users') else [ ]
|
||||
users = [user for user in room.users] if hasattr(room, 'users') else []
|
||||
|
||||
for user in dsettings.chat_operators:
|
||||
if user.user not in users:
|
||||
|
|
@ -199,24 +204,26 @@ def create(kind, owner, users = None, name = None):
|
|||
chat_room_user = {"doctype": "Chat Room User", "user": user.user}
|
||||
room.append('users', chat_room_user)
|
||||
|
||||
room.save(ignore_permissions = True)
|
||||
room.save(ignore_permissions=True)
|
||||
|
||||
room = get(owner, rooms = room.name)
|
||||
users = [room.owner] + [u for u in room.users]
|
||||
room = get(token=token, rooms=room.name)
|
||||
if room:
|
||||
users = [room.owner] + [u for u in room.users]
|
||||
|
||||
for u in users:
|
||||
frappe.publish_realtime('frappe.chat.room:create', room, user = u, after_commit = True)
|
||||
for user in users:
|
||||
frappe.publish_realtime('frappe.chat.room:create', room, user=user, after_commit=True)
|
||||
|
||||
return room
|
||||
|
||||
@frappe.whitelist(allow_guest = True)
|
||||
def history(room, user, fields = None, limit = 10, start = None, end = None):
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def history(room, user, fields=None, limit=10, start=None, end=None):
|
||||
if frappe.get_doc('Chat Room', room).type != 'Visitor':
|
||||
authenticate(user)
|
||||
|
||||
fields = safe_json_loads(fields)
|
||||
|
||||
mess = chat_message.history(room, limit = limit, start = start, end = end)
|
||||
mess = squashify(mess)
|
||||
mess = chat_message.history(room, limit=limit, start=start, end=end)
|
||||
mess = squashify(mess)
|
||||
|
||||
return dictify(mess)
|
||||
return dictify(mess)
|
||||
|
|
|
|||
|
|
@ -460,6 +460,15 @@ def run_tests(context, app=None, module=None, doctype=None, test=(),
|
|||
tests = test
|
||||
|
||||
site = get_site(context)
|
||||
|
||||
allow_tests = frappe.get_conf(site).allow_tests
|
||||
|
||||
if not (allow_tests or os.environ.get('CI')):
|
||||
click.secho('Testing is disabled for the site!', bold=True)
|
||||
click.secho('You can enable tests by entering following command:')
|
||||
click.secho('bench --site {0} set-config allow_tests true'.format(site), fg='green')
|
||||
return
|
||||
|
||||
frappe.init(site=site)
|
||||
|
||||
frappe.flags.skip_before_tests = skip_before_tests
|
||||
|
|
|
|||
|
|
@ -351,16 +351,26 @@ def get_contacts(email_strings):
|
|||
email = get_email_without_link(email)
|
||||
contact_name = get_contact_name(email)
|
||||
|
||||
if not contact_name:
|
||||
contact = frappe.get_doc({
|
||||
"doctype": "Contact",
|
||||
"first_name": frappe.unscrub(email.split("@")[0]),
|
||||
})
|
||||
contact.add_email(email_id=email, is_primary=True)
|
||||
contact.insert(ignore_permissions=True)
|
||||
contact_name = contact.name
|
||||
if not contact_name and email:
|
||||
email_parts = email.split("@")
|
||||
first_name = frappe.unscrub(email_parts[0])
|
||||
|
||||
contacts.append(contact_name)
|
||||
try:
|
||||
contact_name = '{0}-{1}'.format(first_name, email_parts[1]) if first_name == 'Contact' else first_name
|
||||
contact = frappe.get_doc({
|
||||
"doctype": "Contact",
|
||||
"first_name": contact_name,
|
||||
"name": contact_name
|
||||
})
|
||||
contact.add_email(email_id=email, is_primary=True)
|
||||
contact.insert(ignore_permissions=True)
|
||||
contact_name = contact.name
|
||||
except Exception:
|
||||
traceback = frappe.get_traceback()
|
||||
frappe.log_error(traceback)
|
||||
|
||||
if contact_name:
|
||||
contacts.append(contact_name)
|
||||
|
||||
return contacts
|
||||
|
||||
|
|
|
|||
|
|
@ -238,8 +238,9 @@ def get_recipients_cc_and_bcc(doc, recipients, cc, bcc, fetched_from_email_accou
|
|||
return recipients, cc, bcc
|
||||
|
||||
def remove_administrator_from_email_list(email_list):
|
||||
if 'Administrator' in email_list:
|
||||
email_list.remove('Administrator')
|
||||
administrator_email = list(filter(lambda emails: "Administrator" in emails, email_list))
|
||||
if administrator_email:
|
||||
email_list.remove(administrator_email[0])
|
||||
|
||||
def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None):
|
||||
"""Prepare to make multipart MIME Email
|
||||
|
|
@ -304,27 +305,12 @@ def set_incoming_outgoing_accounts(doc):
|
|||
doc.incoming_email_account = frappe.db.get_value("Email Account",
|
||||
{"append_to": doc.reference_doctype, }, "email_id")
|
||||
|
||||
doc.outgoing_email_account = frappe.db.get_value("Email Account",
|
||||
{"append_to": doc.reference_doctype, "enable_outgoing": 1},
|
||||
["email_id", "always_use_account_email_id_as_sender", "name",
|
||||
"always_use_account_name_as_sender_name"], as_dict=True)
|
||||
|
||||
if not doc.incoming_email_account:
|
||||
doc.incoming_email_account = frappe.db.get_value("Email Account",
|
||||
{"default_incoming": 1, "enable_incoming": 1}, "email_id")
|
||||
|
||||
if not doc.outgoing_email_account:
|
||||
# if from address is not the default email account
|
||||
doc.outgoing_email_account = frappe.db.get_value("Email Account",
|
||||
{"email_id": doc.sender, "enable_outgoing": 1},
|
||||
["email_id", "always_use_account_email_id_as_sender", "name",
|
||||
"send_unsubscribe_message", "always_use_account_name_as_sender_name"], as_dict=True) or frappe._dict()
|
||||
|
||||
if not doc.outgoing_email_account:
|
||||
doc.outgoing_email_account = frappe.db.get_value("Email Account",
|
||||
{"default_outgoing": 1, "enable_outgoing": 1},
|
||||
["email_id", "always_use_account_email_id_as_sender", "name",
|
||||
"send_unsubscribe_message", "always_use_account_name_as_sender_name"],as_dict=True) or frappe._dict()
|
||||
doc.outgoing_email_account = frappe.email.smtp.get_outgoing_email_account(raise_exception_not_set=False,
|
||||
append_to=doc.doctype, sender=doc.sender)
|
||||
|
||||
if doc.sent_or_received == "Sent":
|
||||
doc.db_set("email_account", doc.outgoing_email_account.name)
|
||||
|
|
@ -543,4 +529,4 @@ def mark_email_as_seen(name=None):
|
|||
|
||||
frappe.response["type"] = 'binary'
|
||||
frappe.response["filename"] = "imaginary_pixel.png"
|
||||
frappe.response["filecontent"] = buffered_obj.getvalue()
|
||||
frappe.response["filecontent"] = buffered_obj.getvalue()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -907,7 +907,7 @@ def validate_fields(meta):
|
|||
|
||||
def check_illegal_depends_on_conditions(docfield):
|
||||
''' assignment operation should not be allowed in the depends on condition.'''
|
||||
depends_on_fields = ["depends_on", "collapsible_depends_on"]
|
||||
depends_on_fields = ["depends_on", "collapsible_depends_on", "mandatory_depends_on", "read_only_depends_on"]
|
||||
for field in depends_on_fields:
|
||||
depends_on = docfield.get(field, None)
|
||||
if depends_on and ("=" in depends_on) and \
|
||||
|
|
|
|||
|
|
@ -96,14 +96,19 @@ class TestDocType(unittest.TestCase):
|
|||
def test_all_depends_on_fields_conditions(self):
|
||||
import re
|
||||
|
||||
docfields = frappe.get_all("DocField", or_filters={
|
||||
docfields = frappe.get_all("DocField",
|
||||
or_filters={
|
||||
"ifnull(depends_on, '')": ("!=", ''),
|
||||
"ifnull(collapsible_depends_on, '')": ("!=", '')
|
||||
}, fields=["parent", "depends_on", "collapsible_depends_on", "fieldname", "fieldtype"])
|
||||
"ifnull(collapsible_depends_on, '')": ("!=", ''),
|
||||
"ifnull(mandatory_depends_on, '')": ("!=", ''),
|
||||
"ifnull(read_only_depends_on, '')": ("!=", '')
|
||||
},
|
||||
fields=["parent", "depends_on", "collapsible_depends_on", "mandatory_depends_on",\
|
||||
"read_only_depends_on", "fieldname", "fieldtype"])
|
||||
|
||||
pattern = """[\w\.:_]+\s*={1}\s*[\w\.@'"]+"""
|
||||
for field in docfields:
|
||||
for depends_on in ["depends_on", "collapsible_depends_on"]:
|
||||
for depends_on in ["depends_on", "collapsible_depends_on", "mandatory_depends_on", "read_only_depends_on"]:
|
||||
condition = field.get(depends_on)
|
||||
if condition:
|
||||
self.assertFalse(re.match(pattern, condition))
|
||||
|
|
|
|||
|
|
@ -368,10 +368,10 @@ class User(Document):
|
|||
(tab, field, '%s', field, '%s'), (new_name, old_name))
|
||||
|
||||
if frappe.db.exists("Chat Profile", old_name):
|
||||
frappe.rename_doc("Chat Profile", old_name, new_name, force=True)
|
||||
frappe.rename_doc("Chat Profile", old_name, new_name, force=True, show_alert=False)
|
||||
|
||||
if frappe.db.exists("Notification Settings", old_name):
|
||||
frappe.rename_doc("Notification Settings", old_name, new_name, force=True)
|
||||
frappe.rename_doc("Notification Settings", old_name, new_name, force=True, show_alert=False)
|
||||
|
||||
# set email
|
||||
frappe.db.sql("""UPDATE `tabUser`
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"creation": "2013-01-10 16:34:01",
|
||||
"description": "Adds a custom field to a DocType",
|
||||
|
|
@ -24,10 +25,8 @@
|
|||
"collapsible_depends_on",
|
||||
"default",
|
||||
"depends_on",
|
||||
"description",
|
||||
"permlevel",
|
||||
"width",
|
||||
"columns",
|
||||
"mandatory_depends_on",
|
||||
"read_only_depends_on",
|
||||
"properties",
|
||||
"reqd",
|
||||
"unique",
|
||||
|
|
@ -46,7 +45,11 @@
|
|||
"report_hide",
|
||||
"search_index",
|
||||
"ignore_xss_filter",
|
||||
"translatable"
|
||||
"translatable",
|
||||
"description",
|
||||
"permlevel",
|
||||
"width",
|
||||
"columns"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -349,11 +352,24 @@
|
|||
"fieldname": "length",
|
||||
"fieldtype": "Int",
|
||||
"label": "Length"
|
||||
},
|
||||
{
|
||||
"fieldname": "mandatory_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Mandatory Depends On",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"fieldname": "read_only_depends_on",
|
||||
"fieldtype": "Code",
|
||||
"label": "Read Only Depends On",
|
||||
"length": 255
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-glass",
|
||||
"idx": 1,
|
||||
"modified": "2019-09-11 12:57:19.268934",
|
||||
"links": [],
|
||||
"modified": "2019-12-12 21:31:08.209996",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Custom Field",
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ docfield_properties = {
|
|||
'report_hide': 'Check',
|
||||
'allow_on_submit': 'Check',
|
||||
'translatable': 'Check',
|
||||
'mandatory_depends_on': 'Data',
|
||||
'read_only_depends_on': 'Data',
|
||||
'depends_on': 'Data',
|
||||
'description': 'Text',
|
||||
'default': 'Text',
|
||||
|
|
@ -71,7 +73,8 @@ docfield_properties = {
|
|||
'columns': 'Int',
|
||||
'remember_last_selected_value': 'Check',
|
||||
'allow_bulk_edit': 'Check',
|
||||
'auto_repeat': 'Link'
|
||||
'auto_repeat': 'Link',
|
||||
'allow_in_quick_entry': 'Check'
|
||||
}
|
||||
|
||||
allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'),
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -40,6 +40,8 @@ CREATE TABLE `tabDocField` (
|
|||
`show_preview_popup` int(1) NOT NULL DEFAULT 0,
|
||||
`trigger` varchar(255) DEFAULT NULL,
|
||||
`collapsible_depends_on` text,
|
||||
`mandatory_depends_on` text,
|
||||
`read_only_depends_on` text,
|
||||
`depends_on` text,
|
||||
`permlevel` int(11) NOT NULL DEFAULT 0,
|
||||
`ignore_user_permissions` int(1) NOT NULL DEFAULT 0,
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ class PostgresDatabase(Database):
|
|||
from information_schema.tables
|
||||
where table_catalog='{0}'
|
||||
and table_type = 'BASE TABLE'
|
||||
and table_schema='public'""".format(frappe.conf.db_name))]
|
||||
and table_schema='{1}'""".format(frappe.conf.db_name, frappe.conf.get("db_schema", "public")))]
|
||||
|
||||
def format_date(self, date):
|
||||
if not date:
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ CREATE TABLE "tabDocField" (
|
|||
"show_preview_popup" smallint NOT NULL DEFAULT 0,
|
||||
"trigger" varchar(255) DEFAULT NULL,
|
||||
"collapsible_depends_on" text,
|
||||
"mandatory_depends_on" text,
|
||||
"read_only_depends_on" text,
|
||||
"depends_on" text,
|
||||
"permlevel" bigint NOT NULL DEFAULT 0,
|
||||
"ignore_user_permissions" smallint NOT NULL DEFAULT 0,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:email_account_name",
|
||||
"creation": "2014-09-11 12:04:34.163728",
|
||||
|
|
@ -21,6 +22,7 @@
|
|||
"use_imap",
|
||||
"email_server",
|
||||
"use_ssl",
|
||||
"append_emails_to_sent_folder",
|
||||
"incoming_port",
|
||||
"attachment_limit",
|
||||
"append_to",
|
||||
|
|
@ -37,6 +39,7 @@
|
|||
"enable_outgoing",
|
||||
"smtp_server",
|
||||
"use_tls",
|
||||
"use_ssl_for_outgoing",
|
||||
"smtp_port",
|
||||
"default_outgoing",
|
||||
"always_use_account_email_id_as_sender",
|
||||
|
|
@ -389,10 +392,25 @@
|
|||
"fieldname": "incoming_port",
|
||||
"fieldtype": "Data",
|
||||
"label": "Port"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!doc.domain && doc.enable_outgoing",
|
||||
"fieldname": "append_emails_to_sent_folder",
|
||||
"fieldtype": "Check",
|
||||
"label": "Append Emails to Sent Folder"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!doc.domain && doc.enable_outgoing",
|
||||
"fieldname": "use_ssl_for_outgoing",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use SSL for Outgoing"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-inbox",
|
||||
"modified": "2019-08-31 18:01:15.568831",
|
||||
"links": [],
|
||||
"modified": "2019-12-18 15:56:39.744520",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Email Account",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import imaplib
|
|||
import re
|
||||
import json
|
||||
import socket
|
||||
import time
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import validate_email_address, cint, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html
|
||||
|
|
@ -116,7 +117,8 @@ class EmailAccount(Document):
|
|||
fields = [
|
||||
"name as domain", "use_imap", "email_server",
|
||||
"use_ssl", "smtp_server", "use_tls",
|
||||
"smtp_port", "incoming_port"
|
||||
"smtp_port", "incoming_port", "append_emails_to_sent_folder",
|
||||
"use_ssl_for_outgoing"
|
||||
]
|
||||
return frappe.db.get_value("Email Domain", domain[1], fields, as_dict=True)
|
||||
except Exception:
|
||||
|
|
@ -128,11 +130,12 @@ class EmailAccount(Document):
|
|||
if not self.smtp_server:
|
||||
frappe.throw(_("{0} is required").format("SMTP Server"))
|
||||
|
||||
server = SMTPServer(login = getattr(self, "login_id", None) \
|
||||
or self.email_id,
|
||||
server = self.smtp_server,
|
||||
port = cint(self.smtp_port),
|
||||
use_tls = cint(self.use_tls)
|
||||
server = SMTPServer(
|
||||
login = getattr(self, "login_id", None) or self.email_id,
|
||||
server=self.smtp_server,
|
||||
port=cint(self.smtp_port),
|
||||
use_tls=cint(self.use_tls),
|
||||
use_ssl=cint(self.use_ssl_for_outgoing)
|
||||
)
|
||||
if self.password and not self.no_smtp_authentication:
|
||||
server.password = self.get_password()
|
||||
|
|
@ -244,13 +247,13 @@ class EmailAccount(Document):
|
|||
exceptions = []
|
||||
seen_status = []
|
||||
uid_reindexed = False
|
||||
email_server = None
|
||||
|
||||
if frappe.local.flags.in_test:
|
||||
incoming_mails = test_mails
|
||||
else:
|
||||
email_sync_rule = self.build_email_sync_rule()
|
||||
|
||||
email_server = None
|
||||
try:
|
||||
email_server = self.get_incoming_server(in_receive=True, email_sync_rule=email_sync_rule)
|
||||
except Exception:
|
||||
|
|
@ -290,7 +293,7 @@ class EmailAccount(Document):
|
|||
|
||||
else:
|
||||
frappe.db.commit()
|
||||
if communication:
|
||||
if communication and hasattr(communication, "_attachments"):
|
||||
attachments = [d.file_name for d in communication._attachments]
|
||||
communication.notify(attachments=attachments, fetched_from_email_account=True)
|
||||
|
||||
|
|
@ -302,7 +305,7 @@ class EmailAccount(Document):
|
|||
raise Exception(frappe.as_json(exceptions))
|
||||
|
||||
def handle_bad_emails(self, email_server, uid, raw, reason):
|
||||
if cint(email_server.settings.use_imap):
|
||||
if email_server and cint(email_server.settings.use_imap):
|
||||
import email
|
||||
try:
|
||||
mail = email.message_from_string(raw)
|
||||
|
|
@ -636,6 +639,24 @@ class EmailAccount(Document):
|
|||
if frappe.db.exists("Email Account", {"enable_automatic_linking": 1, "name": ('!=', self.name)}):
|
||||
frappe.throw(_("Automatic Linking can be activated only for one Email Account."))
|
||||
|
||||
|
||||
def append_email_to_sent_folder(self, message):
|
||||
|
||||
email_server = None
|
||||
try:
|
||||
email_server = self.get_incoming_server(in_receive=True)
|
||||
except Exception:
|
||||
frappe.log_error(title=_("Error while connecting to email account {0}").format(self.name))
|
||||
|
||||
if not email_server:
|
||||
return
|
||||
|
||||
email_server.connect()
|
||||
|
||||
if email_server.imap:
|
||||
email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_append_to(doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None):
|
||||
txt = txt if txt else ""
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"actions": [],
|
||||
"autoname": "field:domain_name",
|
||||
"creation": "2016-03-29 10:50:48.848239",
|
||||
"doctype": "DocType",
|
||||
|
|
@ -18,6 +19,8 @@
|
|||
"outgoing_mail_settings",
|
||||
"smtp_server",
|
||||
"use_tls",
|
||||
"use_ssl_for_outgoing",
|
||||
"append_emails_to_sent_folder",
|
||||
"smtp_port"
|
||||
],
|
||||
"fields": [
|
||||
|
|
@ -30,7 +33,7 @@
|
|||
"fieldtype": "Data",
|
||||
"label": "domain name",
|
||||
"read_only": 1,
|
||||
"unique": 0
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "email_id",
|
||||
|
|
@ -106,10 +109,23 @@
|
|||
"fieldname": "incoming_port",
|
||||
"fieldtype": "Data",
|
||||
"label": "Port"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "append_emails_to_sent_folder",
|
||||
"fieldtype": "Check",
|
||||
"label": "Append Emails to Sent Folder"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_ssl_for_outgoing",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use SSL for Outgoing"
|
||||
}
|
||||
],
|
||||
"icon": "icon-inbox",
|
||||
"modified": "2019-10-09 17:56:48.834704",
|
||||
"links": [],
|
||||
"modified": "2019-12-18 15:57:34.445308",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Email",
|
||||
"name": "Email Domain",
|
||||
|
|
@ -127,4 +143,4 @@
|
|||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ from __future__ import unicode_literals
|
|||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import validate_email_address ,cint
|
||||
from frappe.utils import validate_email_address ,cint, cstr
|
||||
import imaplib,poplib,smtplib
|
||||
from frappe.email.utils import get_port
|
||||
|
||||
|
|
@ -49,9 +49,16 @@ class EmailDomain(Document):
|
|||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if self.use_tls and not self.smtp_port:
|
||||
self.smtp_port = 587
|
||||
sess = smtplib.SMTP((self.smtp_server or "").encode('utf-8'), cint(self.smtp_port) or None)
|
||||
if self.use_ssl_for_outgoing:
|
||||
if not self.smtp_port:
|
||||
self.smtp_port = 465
|
||||
|
||||
sess = smtplib.SMTP_SSL((self.smtp_server or "").encode('utf-8'),
|
||||
cint(self.smtp_port) or None)
|
||||
else:
|
||||
if self.use_tls and not self.smtp_port:
|
||||
self.smtp_port = 587
|
||||
sess = smtplib.SMTP(cstr(self.smtp_server or ""), cint(self.smtp_port) or None)
|
||||
sess.quit()
|
||||
except Exception:
|
||||
frappe.throw(_("Outgoing email account not correct"))
|
||||
|
|
@ -73,6 +80,8 @@ class EmailDomain(Document):
|
|||
email_account.set("attachment_limit",self.attachment_limit)
|
||||
email_account.set("smtp_server",self.smtp_server)
|
||||
email_account.set("smtp_port",self.smtp_port)
|
||||
email_account.set("use_ssl_for_outgoing", self.use_ssl_for_outgoing)
|
||||
email_account.set("append_emails_to_sent_folder", self.append_emails_to_sent_folder)
|
||||
email_account.save()
|
||||
except Exception as e:
|
||||
frappe.msgprint(email_account.name)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import frappe
|
|||
import sys
|
||||
from six.moves import html_parser as HTMLParser
|
||||
import smtplib, quopri, json
|
||||
from frappe import msgprint, _, safe_decode
|
||||
from frappe import msgprint, _, safe_decode, safe_encode
|
||||
from frappe.email.smtp import SMTPServer, get_outgoing_email_account
|
||||
from frappe.email.email_body import get_email, get_formatted_html, add_attachment
|
||||
from frappe.utils.verified_command import get_signed_params, verify_request
|
||||
|
|
@ -380,7 +380,7 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals
|
|||
for update''', email, as_dict=True)[0]
|
||||
|
||||
recipients_list = frappe.db.sql('''select name, recipient, status from
|
||||
`tabEmail Queue Recipient` where parent=%s''',email.name,as_dict=1)
|
||||
`tabEmail Queue Recipient` where parent=%s''', email.name, as_dict=1)
|
||||
|
||||
if frappe.are_emails_muted():
|
||||
frappe.msgprint(_("Emails are muted"))
|
||||
|
|
@ -401,8 +401,16 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals
|
|||
frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit)
|
||||
|
||||
try:
|
||||
message = None
|
||||
|
||||
if not frappe.flags.in_test:
|
||||
if not smtpserver: smtpserver = SMTPServer()
|
||||
if not smtpserver:
|
||||
smtpserver = SMTPServer()
|
||||
|
||||
# to avoid always using default email account for outgoing
|
||||
if getattr(frappe.local, "outgoing_email_account", None):
|
||||
frappe.local.outgoing_email_account = {}
|
||||
|
||||
smtpserver.setup_email_account(email.reference_doctype, sender=email.sender)
|
||||
|
||||
for recipient in recipients_list:
|
||||
|
|
@ -417,8 +425,10 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals
|
|||
frappe.db.sql("""update `tabEmail Queue Recipient` set status='Sent', modified=%s where name=%s""",
|
||||
(now_datetime(), recipient.name), auto_commit=auto_commit)
|
||||
|
||||
email_sent_to_any_recipient = any("Sent" == s.status for s in recipients_list)
|
||||
|
||||
#if all are sent set status
|
||||
if any("Sent" == s.status for s in recipients_list):
|
||||
if email_sent_to_any_recipient:
|
||||
frappe.db.sql("""update `tabEmail Queue` set status='Sent', modified=%s where name=%s""",
|
||||
(now_datetime(), email.name), auto_commit=auto_commit)
|
||||
else:
|
||||
|
|
@ -430,6 +440,9 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals
|
|||
if email.communication:
|
||||
frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit)
|
||||
|
||||
if smtpserver.append_emails_to_sent_folder and email_sent_to_any_recipient:
|
||||
smtpserver.email_account.append_email_to_sent_folder(encode(message))
|
||||
|
||||
except (smtplib.SMTPServerDisconnected,
|
||||
smtplib.SMTPConnectError,
|
||||
smtplib.SMTPHeloError,
|
||||
|
|
@ -439,7 +452,7 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals
|
|||
|
||||
# bad connection/timeout, retry later
|
||||
|
||||
if any("Sent" == s.status for s in recipients_list):
|
||||
if email_sent_to_any_recipient:
|
||||
frappe.db.sql("""update `tabEmail Queue` set status='Partially Sent', modified=%s where name=%s""",
|
||||
(now_datetime(), email.name), auto_commit=auto_commit)
|
||||
else:
|
||||
|
|
@ -459,7 +472,7 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False, from_test=Fals
|
|||
frappe.db.sql("""update `tabEmail Queue` set status='Not Sent', modified=%s, retry=retry+1 where name=%s""",
|
||||
(now_datetime(), email.name), auto_commit=auto_commit)
|
||||
else:
|
||||
if any("Sent" == s.status for s in recipients_list):
|
||||
if email_sent_to_any_recipient:
|
||||
frappe.db.sql("""update `tabEmail Queue` set status='Partially Errored', error=%s where name=%s""",
|
||||
(text_type(e), email.name), auto_commit=auto_commit)
|
||||
else:
|
||||
|
|
@ -550,7 +563,7 @@ def prepare_message(email, recipient, recipients_list):
|
|||
print_format_file.update({"parent": message})
|
||||
add_attachment(**print_format_file)
|
||||
|
||||
return message.as_string()
|
||||
return safe_encode(message.as_string())
|
||||
|
||||
def clear_outbox():
|
||||
"""Remove low priority older than 31 days in Outbox and expire mails not sent for 7 days.
|
||||
|
|
|
|||
|
|
@ -480,7 +480,7 @@ class Email:
|
|||
"""Detect chartset."""
|
||||
charset = part.get_content_charset()
|
||||
if not charset:
|
||||
charset = chardet.detect(cstr(part))['encoding']
|
||||
charset = chardet.detect(safe_encode(cstr(part)))['encoding']
|
||||
|
||||
return charset
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import smtplib
|
|||
import email.utils
|
||||
import _socket, sys
|
||||
from frappe import _
|
||||
from frappe.utils import cint, parse_addr
|
||||
from frappe.utils import cint, cstr, parse_addr
|
||||
|
||||
def send(email, append_to=None, retry=1):
|
||||
"""Deprecated: Send the message or add it to Outbox Email"""
|
||||
|
|
@ -41,6 +41,8 @@ def get_outgoing_email_account(raise_exception_not_set=True, append_to=None, sen
|
|||
try getting settings from `site_config.json`."""
|
||||
|
||||
sender_email_id = None
|
||||
_email_account = None
|
||||
|
||||
if sender:
|
||||
sender_email_id = parse_addr(sender)[1]
|
||||
|
||||
|
|
@ -52,35 +54,38 @@ def get_outgoing_email_account(raise_exception_not_set=True, append_to=None, sen
|
|||
or frappe.local.outgoing_email_account.get("default")):
|
||||
email_account = None
|
||||
|
||||
if append_to:
|
||||
# append_to is only valid when enable_incoming is checked
|
||||
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})
|
||||
|
||||
# in case of multiple Email Accounts with same append_to
|
||||
# narrow it down based on email_id
|
||||
email_account = _get_email_account({
|
||||
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,
|
||||
"email_id": sender_email_id
|
||||
})
|
||||
}, cache=True)
|
||||
|
||||
# else find the first Email Account with append_to
|
||||
if not email_account:
|
||||
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 and sender_email_id:
|
||||
# check if the sender has email account with enable_outgoing
|
||||
email_account = _get_email_account({"enable_outgoing": 1, "email_id": sender_email_id})
|
||||
|
||||
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)
|
||||
|
|
@ -152,16 +157,19 @@ def _get_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, append_to=None):
|
||||
def __init__(self, login=None, password=None, server=None, port=None, use_tls=None, use_ssl=None, append_to=None):
|
||||
# get defaults from mail settings
|
||||
|
||||
self._sess = None
|
||||
self.email_account = None
|
||||
self.server = None
|
||||
self.append_emails_to_sent_folder = None
|
||||
|
||||
if server:
|
||||
self.server = server
|
||||
self.port = port
|
||||
self.use_tls = cint(use_tls)
|
||||
self.use_ssl = cint(use_ssl)
|
||||
self.login = login
|
||||
self.password = password
|
||||
|
||||
|
|
@ -183,6 +191,8 @@ class SMTPServer:
|
|||
self.port = self.email_account.smtp_port
|
||||
self.use_tls = self.email_account.use_tls
|
||||
self.sender = self.email_account.email_id
|
||||
self.use_ssl = self.email_account.use_ssl_for_outgoing
|
||||
self.append_emails_to_sent_folder = self.email_account.append_emails_to_sent_folder
|
||||
self.always_use_account_email_id_as_sender = cint(self.email_account.get("always_use_account_email_id_as_sender"))
|
||||
self.always_use_account_name_as_sender_name = cint(self.email_account.get("always_use_account_name_as_sender_name"))
|
||||
|
||||
|
|
@ -199,11 +209,18 @@ class SMTPServer:
|
|||
raise frappe.OutgoingEmailError(err_msg)
|
||||
|
||||
try:
|
||||
if self.use_tls and not self.port:
|
||||
self.port = 587
|
||||
if self.use_ssl:
|
||||
if not self.port:
|
||||
self.smtp_port = 465
|
||||
|
||||
self._sess = smtplib.SMTP((self.server or "").encode('utf-8'),
|
||||
cint(self.port) or None)
|
||||
self._sess = smtplib.SMTP_SSL((self.server or "").encode('utf-8'),
|
||||
cint(self.port) or None)
|
||||
else:
|
||||
if self.use_tls and not self.port:
|
||||
self.port = 587
|
||||
|
||||
self._sess = smtplib.SMTP(cstr(self.server or ""),
|
||||
cint(self.port) or None)
|
||||
|
||||
if not self._sess:
|
||||
err_msg = _('Could not connect to outgoing email server')
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import unittest, os, base64
|
||||
from frappe import safe_decode
|
||||
from frappe.email.receive import Email
|
||||
from frappe.email.email_body import (replace_filename_with_cid,
|
||||
get_email, inline_style_in_html, get_header)
|
||||
get_email, inline_style_in_html, get_header)
|
||||
from frappe.email.queue import prepare_message, get_email_queue
|
||||
from six import PY3
|
||||
|
||||
|
|
@ -57,7 +58,7 @@ This is the text version of this email
|
|||
formatted='<h1>' + uni_chr1 + 'abcd' + uni_chr2 + '</h1>',
|
||||
text_content='whatever')
|
||||
result = prepare_message(email=email, recipient='test@test.com', recipients_list=[])
|
||||
self.assertTrue("<h1>=EA=80=80abcd=DE=B4</h1>" in result)
|
||||
self.assertTrue(b"<h1>=EA=80=80abcd=DE=B4</h1>" in result)
|
||||
|
||||
def test_prepare_message_returns_cr_lf(self):
|
||||
email = get_email_queue(
|
||||
|
|
@ -67,7 +68,8 @@ This is the text version of this email
|
|||
content='<h1>\n this is a test of newlines\n' + '</h1>',
|
||||
formatted='<h1>\n this is a test of newlines\n' + '</h1>',
|
||||
text_content='whatever')
|
||||
result = prepare_message(email=email, recipient='test@test.com', recipients_list=[])
|
||||
result = safe_decode(prepare_message(email=email,
|
||||
recipient='test@test.com', recipients_list=[]))
|
||||
if PY3:
|
||||
self.assertTrue(result.count('\n') == result.count("\r"))
|
||||
else:
|
||||
|
|
@ -81,9 +83,10 @@ This is the text version of this email
|
|||
subject='Test Subject',
|
||||
content='<h1>Whatever</h1>',
|
||||
text_content='whatever',
|
||||
message_id= "a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" +
|
||||
".really.long.message.id.that.should.not.wrap.unti")
|
||||
result = prepare_message(email=email, recipient='test@test.com', recipients_list=[])
|
||||
message_id="a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" +
|
||||
".really.long.message.id.that.should.not.wrap.unti")
|
||||
result = safe_decode(prepare_message(email=email, recipient='test@test.com',
|
||||
recipients_list=[]))
|
||||
self.assertTrue(
|
||||
"a.really.long.message.id.that.should.not.wrap.until.998.if.it.does.then.exchange.will.break" +
|
||||
".really.long.message.id.that.should.not.wrap.unti" in result)
|
||||
|
|
|
|||
|
|
@ -106,13 +106,12 @@ def get_webhook_headers(doc, webhook):
|
|||
|
||||
def get_webhook_data(doc, webhook):
|
||||
data = {}
|
||||
doc = doc.as_dict(convert_dates_to_str=True)
|
||||
|
||||
if webhook.webhook_data:
|
||||
for w in webhook.webhook_data:
|
||||
value = doc.get(w.fieldname)
|
||||
if isinstance(value, datetime.datetime):
|
||||
value = frappe.utils.get_datetime_str(value)
|
||||
data[w.key] = value
|
||||
data = {w.key: doc.get(w.fieldname) for w in webhook.webhook_data}
|
||||
elif webhook.webhook_json:
|
||||
data = frappe.render_template(webhook.webhook_json, get_context(doc))
|
||||
data = json.loads(data)
|
||||
|
||||
return data
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ class BaseDocument(object):
|
|||
doc["doctype"] = self.doctype
|
||||
for df in self.meta.get_table_fields():
|
||||
children = self.get(df.fieldname) or []
|
||||
doc[df.fieldname] = [d.as_dict(no_nulls=no_nulls) for d in children]
|
||||
doc[df.fieldname] = [d.as_dict(convert_dates_to_str=convert_dates_to_str, no_nulls=no_nulls) for d in children]
|
||||
|
||||
if no_nulls:
|
||||
for k in list(doc):
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ def update_document_title(doctype, docname, title_field=None, old_title=None, ne
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=False, ignore_if_exists=False):
|
||||
def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=False, ignore_if_exists=False, show_alert=True):
|
||||
"""
|
||||
Renames a doc(dt, old) to doc(dt, new) and
|
||||
updates all linked fields of type "Link"
|
||||
|
|
@ -99,7 +99,9 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F
|
|||
|
||||
frappe.clear_cache()
|
||||
frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype', doctype=doctype)
|
||||
frappe.msgprint(_('Document renamed from {0} to {1}').format(bold(old), bold(new)), alert=True, indicator='green')
|
||||
|
||||
if show_alert:
|
||||
frappe.msgprint(_('Document renamed from {0} to {1}').format(bold(old), bold(new)), alert=True, indicator='green')
|
||||
|
||||
return new
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,18 @@ def apply_workflow(doc, action):
|
|||
|
||||
return doc
|
||||
|
||||
@frappe.whitelist()
|
||||
def can_cancel_document(doc):
|
||||
doc = frappe.get_doc(frappe.parse_json(doc))
|
||||
workflow = get_workflow(doc.doctype)
|
||||
for state_doc in workflow.states:
|
||||
if state_doc.doc_status == '2':
|
||||
for transition in workflow.transitions:
|
||||
if transition.next_state == state_doc.state:
|
||||
return False
|
||||
return True
|
||||
return True
|
||||
|
||||
def validate_workflow(doc):
|
||||
'''Validate Workflow State and Transition for the current user.
|
||||
|
||||
|
|
|
|||
|
|
@ -718,7 +718,7 @@ frappe.chat.room.create = function (kind, owner, users, name, fn) {
|
|||
|
||||
return new Promise(resolve => {
|
||||
frappe.call("frappe.chat.doctype.chat_room.chat_room.create",
|
||||
{ kind: kind, owner: owner || frappe.session.user, users: users, name: name },
|
||||
{ kind: kind, token: owner || frappe.session.user, users: users, name: name },
|
||||
r => {
|
||||
let room = r.message
|
||||
room = { ...room, creation: new frappe.datetime.datetime(room.creation) }
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@ frappe.ui.form.ControlTime = frappe.ui.form.ControlDate.extend({
|
|||
&& ((this.last_value && this.last_value !== this.value)
|
||||
|| (!this.datepicker.selectedDates.length))) {
|
||||
|
||||
var date_obj = frappe.datetime.moment_to_date_obj(moment(value, frappe.sys_defaults['time_format']));
|
||||
let time_format = frappe.sys_defaults.time_format || 'HH:mm:ss';
|
||||
var date_obj = frappe.datetime.moment_to_date_obj(moment(value, time_format));
|
||||
this.datepicker.selectDate(date_obj);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ frappe.form.formatters = {
|
|||
},
|
||||
Check: function(value) {
|
||||
if(value) {
|
||||
return '<i class="octicon octicon-check" style="margin-right: 3px;"></i>';
|
||||
return '<i class="fa fa-check" style="margin-right: 3px;"></i>';
|
||||
} else {
|
||||
return '<i class="fa fa-square disabled-check"></i>';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -451,27 +451,27 @@ frappe.ui.form.Layout = Class.extend({
|
|||
// build dependants' dictionary
|
||||
var has_dep = false;
|
||||
|
||||
for(var fkey in this.fields_list) {
|
||||
for (var fkey in this.fields_list) {
|
||||
var f = this.fields_list[fkey];
|
||||
f.dependencies_clear = true;
|
||||
if(f.df.depends_on) {
|
||||
if (f.df.depends_on || f.df.mandatory_depends_on || f.df.read_only_depends_on) {
|
||||
has_dep = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!has_dep)return;
|
||||
if (!has_dep) return;
|
||||
|
||||
// show / hide based on values
|
||||
for(var i=me.fields_list.length-1;i>=0;i--) {
|
||||
for (var i=me.fields_list.length-1;i>=0;i--) {
|
||||
var f = me.fields_list[i];
|
||||
f.guardian_has_value = true;
|
||||
if(f.df.depends_on) {
|
||||
if (f.df.depends_on) {
|
||||
// evaluate guardian
|
||||
|
||||
f.guardian_has_value = this.evaluate_depends_on_value(f.df.depends_on);
|
||||
|
||||
// show / hide
|
||||
if(f.guardian_has_value) {
|
||||
if (f.guardian_has_value) {
|
||||
if(f.df.hidden_due_to_dependency) {
|
||||
f.df.hidden_due_to_dependency = false;
|
||||
f.refresh();
|
||||
|
|
@ -483,10 +483,28 @@ frappe.ui.form.Layout = Class.extend({
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (f.df.mandatory_depends_on) {
|
||||
this.set_dependant_property(f.df.mandatory_depends_on, f.df.fieldname, 'reqd');
|
||||
}
|
||||
|
||||
if (f.df.read_only_depends_on) {
|
||||
this.set_dependant_property(f.df.read_only_depends_on, f.df.fieldname, 'read_only');
|
||||
}
|
||||
}
|
||||
|
||||
this.refresh_section_count();
|
||||
},
|
||||
set_dependant_property: function(condition, fieldname, property) {
|
||||
let set_property = this.evaluate_depends_on_value(condition);
|
||||
if (this.frm) {
|
||||
if (set_property) {
|
||||
this.frm.set_df_property(fieldname, property, 1);
|
||||
} else {
|
||||
this.frm.set_df_property(fieldname, property, 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
evaluate_depends_on_value: function(expression) {
|
||||
var out = null;
|
||||
var doc = this.doc;
|
||||
|
|
|
|||
|
|
@ -105,7 +105,17 @@ frappe.ui.form.States = Class.extend({
|
|||
});
|
||||
}
|
||||
});
|
||||
this.setup_btn(added);
|
||||
if (!added) {
|
||||
//call function and clear cancel button if Cancel doc state is defined in the workfloe
|
||||
frappe.xcall('frappe.model.workflow.can_cancel_document', {doc: this.frm.doc}).then((can_cancel) => {
|
||||
if (!can_cancel) {
|
||||
this.frm.page.clear_secondary_action();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.setup_btn(added);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
},
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ frappe.views.ListGroupBy = class ListGroupBy {
|
|||
this.render_dropdown_items(field_count_list, dropdown);
|
||||
this.sidebar.setup_dropdown_search(dropdown, '.group-by-value');
|
||||
} else {
|
||||
dropdown.find('.group-by-loading').hide();
|
||||
dropdown.find('.group-by-loading').html(`${__("No filters found")}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ export default {
|
|||
|
||||
user_section = [
|
||||
{
|
||||
fieldname: 'user_section',
|
||||
fieldtype: 'Section Break',
|
||||
depends_on: doc => doc.setup_for === user_value
|
||||
}
|
||||
|
|
@ -134,6 +135,7 @@ export default {
|
|||
|
||||
global_section = [
|
||||
{
|
||||
fieldname: 'global_section',
|
||||
fieldtype: 'Section Break',
|
||||
depends_on: doc => doc.setup_for === 'Everyone'
|
||||
}
|
||||
|
|
@ -188,8 +190,11 @@ export default {
|
|||
update_global_modules(d) {
|
||||
let blocked_modules = [];
|
||||
for (let category of this.module_categories) {
|
||||
let unchecked_options = d.get_field(`global:${category}`).get_unchecked_options();
|
||||
blocked_modules = blocked_modules.concat(unchecked_options);
|
||||
let field = d.get_field(`global:${category}`);
|
||||
if (field) {
|
||||
let unchecked_options = field.get_unchecked_options();
|
||||
blocked_modules = blocked_modules.concat(unchecked_options);
|
||||
}
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@
|
|||
|
||||
.navbar-form .awesomplete {
|
||||
margin-left: -15px;
|
||||
width: 300px;
|
||||
width: 370px;
|
||||
|
||||
@media (max-width: @screen-md) {
|
||||
width: 280px;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<h3>{{ _('Top Performer') }} 🏆 </h3>
|
||||
<p> {{ frappe.get_fullname(top_performer.user) }}
|
||||
<span class="text-muted">
|
||||
{{ frappe._('gained {0} points').format(frappe.utils.cint(top_performer.energy_points)) }}
|
||||
{{ _('gained {0} points').format(frappe.utils.cint(top_performer.energy_points)) }}
|
||||
</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
<h3>{{ _('Top Reviewer') }} ❤️ </h3>
|
||||
<p> {{ frappe.get_fullname(top_reviewer.user) }}
|
||||
<span class="text-muted">
|
||||
{{ frappe._('gave {0} points').format(frappe.utils.cint(top_reviewer.given_points)) }}
|
||||
{{ _('gave {0} points').format(frappe.utils.cint(top_reviewer.given_points)) }}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
|
|
@ -24,9 +24,9 @@
|
|||
<table class='table table-bordered'>
|
||||
<tr>
|
||||
<th> # </th>
|
||||
<th style='width: 70%'>{{ frappe._('User') }}</th>
|
||||
<th style='width: 15%'>{{ frappe._('Energy Points') }}</th>
|
||||
<th style='width: 15%'>{{ frappe._('Points Given') }}</th>
|
||||
<th style='width: 70%'>{{ _('User') }}</th>
|
||||
<th style='width: 15%'>{{ _('Energy Points') }}</th>
|
||||
<th style='width: 15%'>{{ _('Points Given') }}</th>
|
||||
</tr>
|
||||
{% for user in standings %}
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -49,6 +49,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.disabled-check {
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.data-field {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
|
|
|
|||
|
|
@ -75,6 +75,23 @@ def create_contact_phone_nos_records():
|
|||
doc.append('phone_nos', {'phone': '123456{}'.format(index)})
|
||||
doc.insert()
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_doctype(name, fields):
|
||||
fields = frappe.parse_json(fields)
|
||||
if frappe.db.exists('DocType', name):
|
||||
return
|
||||
frappe.get_doc({
|
||||
"doctype": "DocType",
|
||||
"module": "Core",
|
||||
"custom": 1,
|
||||
"fields": fields,
|
||||
"permissions": [{
|
||||
"role": "System Manager",
|
||||
"read": 1
|
||||
}],
|
||||
"name": name
|
||||
}).insert()
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_contact_records():
|
||||
if frappe.db.get_all('Contact', {'first_name': 'Test Form Contact 1'}):
|
||||
|
|
|
|||
|
|
@ -142,6 +142,8 @@ def start_worker(queue=None, quiet = False):
|
|||
with Connection(redis_connection):
|
||||
queues = get_queue_list(queue)
|
||||
logging_level = "INFO"
|
||||
if quiet:
|
||||
logging_level = "WARNING"
|
||||
Worker(queues, name=get_worker_name(queue)).work(logging_level = logging_level)
|
||||
|
||||
def get_worker_name(queue):
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ def get_jenv():
|
|||
set_filters(jenv)
|
||||
|
||||
jenv.globals.update(get_safe_globals())
|
||||
jenv.globals.update(get_jenv_customization('methods'))
|
||||
|
||||
frappe.local.jenv = jenv
|
||||
|
||||
|
|
@ -124,4 +125,27 @@ def set_filters(jenv):
|
|||
jenv.filters["flt"] = flt
|
||||
jenv.filters["abs_url"] = abs_url
|
||||
|
||||
if frappe.flags.in_setup_help: return
|
||||
if frappe.flags.in_setup_help:
|
||||
return
|
||||
|
||||
jenv.filters.update(get_jenv_customization('filters'))
|
||||
|
||||
|
||||
def get_jenv_customization(customization_type):
|
||||
'''Returns a dict with filter/method name as key and definition as value'''
|
||||
|
||||
import frappe
|
||||
|
||||
out = {}
|
||||
if not getattr(frappe.local, "site", None):
|
||||
return out
|
||||
|
||||
values = frappe.get_hooks("jenv", {}).get(customization_type)
|
||||
if not values:
|
||||
return out
|
||||
|
||||
for value in values:
|
||||
fn_name, fn_string = value.split(":")
|
||||
out[fn_name] = frappe.get_attr(fn_string)
|
||||
|
||||
return out
|
||||
|
|
|
|||
|
|
@ -2,14 +2,24 @@
|
|||
# MIT License. See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import pdfkit, os, frappe
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
from distutils.version import LooseVersion
|
||||
from frappe.utils import scrub_urls, get_wkhtmltopdf_version
|
||||
from frappe import _
|
||||
import six, re, io
|
||||
|
||||
import pdfkit
|
||||
import six
|
||||
from bs4 import BeautifulSoup
|
||||
from PyPDF2 import PdfFileReader, PdfFileWriter
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import get_wkhtmltopdf_version, scrub_urls
|
||||
|
||||
PDF_CONTENT_ERRORS = ["ContentNotFoundError", "ContentOperationNotPermittedError",
|
||||
"UnknownContentError", "RemoteHostClosedError"]
|
||||
|
||||
|
||||
def get_pdf(html, options=None, output=None):
|
||||
html = scrub_urls(html)
|
||||
html, options = prepare_options(html, options)
|
||||
|
|
@ -30,20 +40,14 @@ def get_pdf(html, options=None, output=None):
|
|||
# https://pythonhosted.org/PyPDF2/PdfFileReader.html
|
||||
# create in-memory binary streams from filedata and create a PdfFileReader object
|
||||
reader = PdfFileReader(io.BytesIO(filedata))
|
||||
|
||||
except IOError as e:
|
||||
if ("ContentNotFoundError" in e.message
|
||||
or "ContentOperationNotPermittedError" in e.message
|
||||
or "UnknownContentError" in e.message
|
||||
or "RemoteHostClosedError" in e.message):
|
||||
except OSError as e:
|
||||
if any([error in str(e) for error in PDF_CONTENT_ERRORS]):
|
||||
if not filedata:
|
||||
frappe.throw(_("PDF generation failed because of broken image links"))
|
||||
|
||||
# allow pdfs with missing images if file got created
|
||||
if filedata:
|
||||
if output: # output is a PdfFileWriter object
|
||||
output.appendPagesFromReader(reader)
|
||||
|
||||
else:
|
||||
frappe.throw(_("PDF generation failed because of broken image links"))
|
||||
if output: # output is a PdfFileWriter object
|
||||
output.appendPagesFromReader(reader)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
|
@ -66,6 +70,7 @@ def get_pdf(html, options=None, output=None):
|
|||
|
||||
return filedata
|
||||
|
||||
|
||||
def get_file_data_from_writer(writer_obj):
|
||||
|
||||
# https://docs.python.org/3/library/io.html
|
||||
|
|
@ -112,6 +117,7 @@ def prepare_options(html, options):
|
|||
|
||||
return html, options
|
||||
|
||||
|
||||
def read_options_from_html(html):
|
||||
options = {}
|
||||
soup = BeautifulSoup(html, "html5lib")
|
||||
|
|
@ -132,6 +138,7 @@ def read_options_from_html(html):
|
|||
|
||||
return soup.prettify(), options
|
||||
|
||||
|
||||
def prepare_header_footer(soup):
|
||||
options = {}
|
||||
|
||||
|
|
@ -174,6 +181,7 @@ def prepare_header_footer(soup):
|
|||
|
||||
return options
|
||||
|
||||
|
||||
def cleanup(fname, options):
|
||||
if os.path.exists(fname):
|
||||
os.remove(fname)
|
||||
|
|
@ -182,6 +190,7 @@ def cleanup(fname, options):
|
|||
if options.get(key) and os.path.exists(options[key]):
|
||||
os.remove(options[key])
|
||||
|
||||
|
||||
def toggle_visible_pdf(soup):
|
||||
for tag in soup.find_all(attrs={"class": "visible-pdf"}):
|
||||
# remove visible-pdf class to unhide
|
||||
|
|
|
|||
|
|
@ -48,11 +48,9 @@ def get_safe_globals():
|
|||
# make available limited methods of frappe
|
||||
json=json,
|
||||
dict=dict,
|
||||
_dict=frappe._dict,
|
||||
frappe=frappe._dict(
|
||||
_=frappe._,
|
||||
_dict=frappe._dict,
|
||||
flags=frappe.flags,
|
||||
|
||||
format=frappe.format_value,
|
||||
format_value=frappe.format_value,
|
||||
date_format=date_format,
|
||||
|
|
|
|||
|
|
@ -34,8 +34,7 @@ passlib==1.7.1
|
|||
pdfkit==0.6.1
|
||||
Pillow==6.2.1
|
||||
premailer==3.6.1
|
||||
psycopg2-binary==2.7.5
|
||||
psycopg2==2.7.5
|
||||
psycopg2-binary==2.8.4
|
||||
pyasn1==0.4.7
|
||||
Pygments==2.2.0
|
||||
PyJWT==1.7.1
|
||||
|
|
@ -64,4 +63,4 @@ urllib3==1.25.7
|
|||
watchdog==0.8.0
|
||||
Werkzeug==0.16.0
|
||||
xlrd==1.2.0
|
||||
zxcvbn-python==4.4.24
|
||||
zxcvbn-python==4.4.24
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue