Commit 4e827056 authored by Anand Doshi's avatar Anand Doshi
Browse files

Merge branch 'develop'

parents e33a4f91 694729bb
develop v11.1.5 v11.1.4 v11.1.3 v11.1.2 v11.1.1 v11.1.0 v11.0.3 v11.0.3-beta.51 v11.0.3-beta.50 v11.0.3-beta.49 v11.0.3-beta.48 v11.0.3-beta.47 v11.0.3-beta.46 v11.0.3-beta.45 v11.0.3-beta.44 v11.0.3-beta.43 v11.0.3-beta.42 v11.0.3-beta.41 v11.0.3-beta.40 v11.0.3-beta.39 v11.0.3-beta.38 v11.0.3-beta.37 v11.0.3-beta.36 v11.0.3-beta.35 v11.0.3-beta.34 v11.0.3-beta.33 v11.0.3-beta.32 v11.0.3-beta.31 v11.0.3-beta.30 v11.0.3-beta.29 v11.0.3-beta.28 v11.0.3-beta.27 v11.0.3-beta.26 v11.0.3-beta.25 v11.0.3-beta.24 v11.0.3-beta.23 v11.0.3-beta.22 v11.0.3-beta.21 v11.0.3-beta.20 v11.0.3-beta.19 v11.0.3-beta.18 v11.0.3-beta.17 v11.0.3-beta.16 v11.0.3-beta.15 v11.0.3-beta.14 v11.0.3-beta.13 v11.0.3-beta.12 v11.0.3-beta.11 v11.0.3-beta.10 v11.0.3-beta.9 v11.0.3-beta.8 v11.0.3-beta.7 v11.0.3-beta.6 v11.0.3-beta.5 v11.0.3-beta.4 v11.0.3-beta.3 v11.0.3-beta.2 v11.0.3-beta.1 v11.0.2 v11.0.1 v11.0.0-beta v10.1.71 v10.1.70 v10.1.69 v10.1.68 v10.1.67 v10.1.66 v10.1.65 v10.1.64 v10.1.63 v10.1.62 v10.1.61 v10.1.60 v10.1.59 v10.1.58 v10.1.57 v10.1.56 v10.1.55 v10.1.54 v10.1.53 v10.1.52 v10.1.51 v10.1.50 v10.1.49 v10.1.49-beta.1 v10.1.48 v10.1.47 v10.1.46 v10.1.45 v10.1.44 v10.1.43 v10.1.42 v10.1.41 v10.1.40 v10.1.39 v10.1.38 v10.1.37 v10.1.36 v10.1.35 v10.1.34 v10.1.33 v10.1.32 v10.1.31 v10.1.30 v10.1.29 v10.1.28 v10.1.27 v10.1.26 v10.1.25 v10.1.24 v10.1.23 v10.1.22 v10.1.21 v10.1.20 v10.1.19 v10.1.18 v10.1.17 v10.1.16 v10.1.15 v10.1.14 v10.1.13 v10.1.12 v10.1.11 v10.1.10 v10.1.9 v10.1.8 v10.1.7 v10.1.6 v10.1.5 v10.1.4 v10.1.3 v10.1.2 v10.1.1 v10.1.0 v10.0.25 v10.0.24 v10.0.23 v10.0.22 v10.0.21 v10.0.20 v10.0.19 v10.0.18 v10.0.17 v10.0.16 v10.0.15 v10.0.14 v10.0.13 v10.0.12 v10.0.11 v10.0.10 v10.0.9 v10.0.8 v10.0.7 v10.0.6 v10.0.5 v10.0.4 v10.0.3 v10.0.2 v10.0.1 v10.0.0 v9.2.25 v9.2.24 v9.2.23 v9.2.22 v9.2.21 v9.2.20 v9.2.19 v9.2.18 v9.2.17 v9.2.16 v9.2.15 v9.2.14 v9.2.13 v9.2.12 v9.2.11 v9.2.10 v9.2.9 v9.2.8 v9.2.7 v9.2.6 v9.2.5 v9.2.4 v9.2.3 v9.2.2 v9.2.1 v9.2.0 v9.1.11 v9.1.10 v9.1.9 v9.1.8 v9.1.7 v9.1.6 v9.1.5 v9.1.4 v9.1.3 v9.1.2 v9.1.1 v9.1.0 v9.0.10 v9.0.9 v9.0.8 v9.0.7 v9.0.6 v9.0.5 v9.0.4 v9.0.3 v9.0.2 v9.0.1 v9.0.0 v8.10.9 v8.10.8 v8.10.7 v8.10.6 v8.10.5 v8.10.4 v8.10.3 v8.10.2 v8.10.1 v8.10.0 v8.9.4 v8.9.3 v8.9.2 v8.9.1 v8.9.0 v8.8.5 v8.8.4 v8.8.3 v8.8.2 v8.8.1 v8.8.0 v8.7.11 v8.7.10 v8.7.9 v8.7.8 v8.7.7 v8.7.6 v8.7.5 v8.7.4 v8.7.3 v8.7.2 v8.7.1 v8.7.0 v8.6.8 v8.6.7 v8.6.6 v8.6.5 v8.6.4 v8.6.3 v8.6.2 v8.6.1 v8.6.0 v8.5.8 v8.5.7 v8.5.6 v8.5.5 v8.5.4 v8.5.3 v8.5.2 v8.5.1 v8.5.0 v8.4.1 v8.4.0 v8.3.10 v8.3.9 v8.3.8 v8.3.7 v8.3.6 v8.3.5 v8.3.4 v8.3.3 v8.3.2 v8.3.1 v8.3.0 v8.2.7 v8.2.6 v8.2.5 v8.2.4 v8.2.3 v8.2.2 v8.2.1 v8.2.0 v8.1.4 v8.1.3 v8.1.2 v8.1.1 v8.1.0 v8.0.71 v8.0.70 v8.0.69 v8.0.68 v8.0.67 v8.0.66 v8.0.65 v8.0.64 v8.0.63 v8.0.62 v8.0.61 v8.0.60 v8.0.59 v8.0.58 v8.0.57 v8.0.56 v8.0.55 v8.0.54 v8.0.53 v8.0.52 v8.0.51 v8.0.50 v8.0.49 v8.0.48 v8.0.47 v8.0.46 v8.0.45 v8.0.44 v8.0.43 v8.0.42 v8.0.41 v8.0.40 v8.0.39 v8.0.38 v8.0.37 v8.0.36 v8.0.35 v8.0.34 v8.0.33 v8.0.32 v8.0.31 v8.0.30 v8.0.29 v8.0.28 v8.0.27 v8.0.26 v8.0.25 v8.0.24 v8.0.23 v8.0.22 v8.0.21 v8.0.20 v8.0.19 v8.0.18 v8.0.17 v8.0.16 v8.0.15 v8.0.14 v8.0.13 v8.0.12 v8.0.11 v8.0.10 v8.0.9 v8.0.8 v8.0.7 v8.0.6 v8.0.5 v8.0.4 v8.0.3 v8.0.2 v8.0.1 v8.0.0 v7.2.31 v7.2.30 v7.2.29 v7.2.28 v7.2.27 v7.2.26 v7.2.25 v7.2.24 v7.2.23 v7.2.22 v7.2.21 v7.2.20 v7.2.19 v7.2.18 v7.2.17 v7.2.16 v7.2.15 v7.2.14 v7.2.13 v7.2.12 v7.2.11 v7.2.10 v7.2.9 v7.2.8 v7.2.7 v7.2.6 v7.2.5 v7.2.4 v7.2.3 v7.2.2 v7.2.1 v7.2.0 v7.1.29 v7.1.28 v7.1.27 v7.1.26 v7.1.25 v7.1.24 v7.1.23 v7.1.22 v7.1.21 v7.1.20 v7.1.19 v7.1.18 v7.1.17 v7.1.16 v7.1.15 v7.1.14 v7.1.13 v7.1.12 v7.1.11 v7.1.10 v7.1.9 v7.1.8 v7.1.7 v7.1.6 v7.1.5 v7.1.4 v7.1.3 v7.1.2 v7.1.1 v7.1.0 v7.0.47 v7.0.46 v7.0.45 v7.0.44 v7.0.43 v7.0.42 v7.0.41 v7.0.40 v7.0.39 v7.0.38 v7.0.37 v7.0.36 v7.0.35 v7.0.34 v7.0.33 v7.0.32 v7.0.31 v7.0.30 v7.0.29 v7.0.28 v7.0.27 v7.0.26 v7.0.25 v7.0.24 v7.0.23 v7.0.22 v7.0.21 v7.0.20 v7.0.19 v7.0.18 v7.0.17 v7.0.16 v7.0.15 v7.0.14 v7.0.13 v7.0.12 v7.0.11 v7.0.10 v7.0.9 v7.0.8 v7.0.7 v7.0.6 v7.0.5 v7.0.4 v7.0.3 v7.0.2 v7.0.1 v7.0.0 v6.27.24 v6.27.23 v6.27.22 v6.27.21 v6.27.20 v6.27.19 v6.27.18 v6.27.17 v6.27.16 v6.27.15 v6.27.14 v6.27.13 v6.27.12 v6.27.11 v6.27.10 v6.27.9 v6.27.8 v6.27.7 v6.27.6 v6.27.5 v6.27.4 v6.27.3 v6.27.2 v6.27.1 v6.27.0 v6.26.6 v6.26.5 v6.26.4 v6.26.3 v6.26.2 v6.26.1 v6.26.0 v6.25.6 v6.25.5 v6.25.4 v6.25.3 v6.25.2 v6.25.1 v6.25.0 v6.24.10 v6.24.9 v6.24.8 v6.24.7 v6.24.6 v6.24.5 v6.24.4 v6.24.3 v6.24.2 v6.24.1 v6.24.0 v6.23.3 v6.23.2 v6.23.1 v6.23.0 v6.22.7 v6.22.6 v6.22.5 v6.22.4 v6.22.3 v6.22.2 v6.22.1 v6.22.0 v6.21.0 v6.20.2 v6.20.1 v6.20.0 v6.19.3 v6.19.2 v6.19.1 v6.19.0 v6.18.1 v6.18.0 v6.17.6 v6.17.5 v6.17.4 v6.17.3 v6.17.2 v6.17.1 v6.17.0 v6.16.4 v6.16.3 v6.16.2 v6.16.1 v6.16.0 v6.15.4 v6.15.3 v6.15.2 v6.15.1 v6.15.0 v6.14.1 v6.14.0 v6.13.5 v6.13.4 v6.13.3 v6.13.2 v6.13.1 v6.13.0 v6.12.4 v6.12.3 v6.12.2 v6.12.1 v6.12.0 v6.11.0 v6.10.4 v6.10.3 v6.10.2 v6.10.1 v6.10.0 v6.9.3 v6.9.2 v6.9.1 v6.9.0 v6.8.2 v6.8.1 v6.8.0 v6.7.11 v6.7.10 v6.7.9 v6.7.8 v6.7.7 v6.7.6 v6.7.5 v6.7.4 v6.7.3 v6.7.2 v6.7.1 v6.7.0 v6.6.5 v6.6.4 v6.6.3 v6.6.2 v6.6.1 v6.6.0 v6.5.4 v6.5.3 v6.5.2 v6.5.1 v6.5.0 v6.4.9 v6.4.8 v6.4.7 v6.4.6 v6.4.5 v6.4.4 v6.4.3 v6.4.2 v6.4.1 v6.4.0 v6.3.0
No related merge requests found
Showing with 646 additions and 323 deletions
+646 -323
......@@ -309,7 +309,7 @@ def sendmail(recipients=(), sender="", subject="No Subject", message="No Message
as_markdown=False, bulk=False, reference_doctype=None, reference_name=None,
unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None,
attachments=None, content=None, doctype=None, name=None, reply_to=None,
cc=(), message_id=None, as_bulk=False, send_after=None):
cc=(), message_id=None, as_bulk=False, send_after=None, expose_recipients=False):
"""Send email using user's default **Email Account** or global default **Email Account**.
......@@ -327,6 +327,7 @@ def sendmail(recipients=(), sender="", subject="No Subject", message="No Message
:param reply_to: Reply-To email id.
:param message_id: Used for threading. If a reply is received to this email, Message-Id is sent back as In-Reply-To in received email.
:param send_after: Send after the given datetime.
:param expose_recipients: Display all recipients in the footer message - "This email was sent to"
"""
if bulk or as_bulk:
......@@ -335,7 +336,8 @@ def sendmail(recipients=(), sender="", subject="No Subject", message="No Message
subject=subject, message=content or message,
reference_doctype = doctype or reference_doctype, reference_name = name or reference_name,
unsubscribe_method=unsubscribe_method, unsubscribe_params=unsubscribe_params, unsubscribe_message=unsubscribe_message,
attachments=attachments, reply_to=reply_to, cc=cc, message_id=message_id, send_after=send_after)
attachments=attachments, reply_to=reply_to, cc=cc, message_id=message_id, send_after=send_after,
expose_recipients=expose_recipients)
else:
import frappe.email
if as_markdown:
......
from __future__ import unicode_literals
__version__ = "6.2.0"
__version__ = "6.3.0"
......@@ -17,57 +17,38 @@ END_LINE = '<!-- frappe: end-file -->'
TASK_LOG_MAX_AGE = 86400 # 1 day in seconds
redis_server = None
def handler(f):
cmd = f.__module__ + '.' + f.__name__
def _run(args, set_in_response=True):
def run(args, set_in_response=True):
from frappe.tasks import run_async_task
from frappe.handler import execute_cmd
if frappe.conf.disable_async:
return execute_cmd(cmd, from_async=True)
args = frappe._dict(args)
task = run_async_task.delay(frappe.local.site,
(frappe.session and frappe.session.user) or 'Administrator', cmd, args)
task = run_async_task.delay(site=frappe.local.site,
user=(frappe.session and frappe.session.user) or 'Administrator', cmd=cmd,
form_dict=args)
if set_in_response:
frappe.local.response['task_id'] = task.id
return task.id
@wraps(f)
def queue(*args, **kwargs):
from frappe.tasks import run_async_task
from frappe.handler import execute_cmd
if frappe.conf.disable_async:
return execute_cmd(cmd, from_async=True)
task = run_async_task.delay(frappe.local.site,
(frappe.session and frappe.session.user) or 'Administrator', cmd,
frappe.local.form_dict)
frappe.local.response['task_id'] = task.id
task_id = run(frappe.local.form_dict, set_in_response=True)
return {
"status": "queued",
"task_id": task.id
"task_id": task_id
}
queue.async = True
queue.queue = f
queue.run = _run
queue.run = run
frappe.whitelisted.append(f)
frappe.whitelisted.append(queue)
return queue
def run_async_task(method, args, reference_doctype=None, reference_name=None, set_in_response=True):
if frappe.local.request and frappe.local.request.method == "GET":
frappe.throw("Cannot run task in a GET request")
task_id = method.run(args, set_in_response=set_in_response)
task = frappe.new_doc("Async Task")
task.celery_task_id = task_id
task.status = "Queued"
task.reference_doctype = reference_doctype
task.reference_name = reference_name
task.save()
return task_id
@frappe.whitelist()
def get_pending_tasks_for_doc(doctype, docname):
return frappe.db.sql_list("select name from `tabAsync Task` where status in ('Queued', 'Running') and reference_doctype='%s' and reference_name='%s'" % (doctype, docname))
......@@ -76,10 +57,9 @@ def get_pending_tasks_for_doc(doctype, docname):
@handler
def ping():
from time import sleep
sleep(6)
sleep(1)
return "pong"
@frappe.whitelist()
def get_task_status(task_id):
from frappe.celery_app import get_celery
......@@ -91,9 +71,7 @@ def get_task_status(task_id):
"progress": 0
}
def set_task_status(task_id, status, response=None):
frappe.db.set_value("Async Task", task_id, "status", status)
if not response:
response = {}
response.update({
......@@ -167,6 +145,7 @@ def emit_via_redis(event, message, room):
try:
r.publish('events', frappe.as_json({'event': event, 'message': message, 'room': room}))
except redis.exceptions.ConnectionError:
# print frappe.get_traceback()
pass
def put_log(line_no, line, task_id=None):
......
......@@ -174,6 +174,10 @@ def files_dirty():
return False
def compile_less():
from distutils.spawn import find_executable
if not find_executable("lessc"):
return
for path in app_paths:
less_path = os.path.join(path, "public", "less")
if os.path.exists(less_path):
......@@ -189,4 +193,4 @@ def compile_less():
print "compiling {0}".format(fpath)
css_path = os.path.join(path, "public", "css", fname.rsplit(".", 1)[0] + ".css")
os.system("which lessc && lessc {0} > {1}".format(fpath, css_path))
os.system("lessc {0} > {1}".format(fpath, css_path))
......@@ -10,8 +10,9 @@ task_logger = get_task_logger(__name__)
from datetime import timedelta
import frappe
import json
import os
import threading
import time
SITES_PATH = os.environ.get('SITES_PATH', '.')
......@@ -26,35 +27,43 @@ _app = None
def get_celery():
global _app
if not _app:
conf = frappe.get_site_config(sites_path=SITES_PATH)
_app = Celery('frappe',
broker=conf.celery_broker or DEFAULT_CELERY_BROKER,
backend=conf.async_redis_server or DEFAULT_CELERY_BACKEND)
setup_celery(_app, conf)
_app = get_celery_app()
return _app
def setup_celery(app, conf):
def get_celery_app():
conf = get_site_config()
app = Celery('frappe',
broker=conf.celery_broker or DEFAULT_CELERY_BROKER,
backend=conf.async_redis_server or DEFAULT_CELERY_BACKEND)
app.autodiscover_tasks(frappe.get_all_apps(with_frappe=True, with_internal_apps=False,
sites_path=SITES_PATH))
app.conf.CELERY_TASK_SERIALIZER = 'json'
app.conf.CELERY_ACCEPT_CONTENT = ['json']
app.conf.CELERY_TIMEZONE = 'UTC'
app.conf.CELERY_RESULT_SERIALIZER = 'json'
app.CELERY_TASK_RESULT_EXPIRES = timedelta(0, 3600)
app.conf.CELERY_TASK_RESULT_EXPIRES = timedelta(0, 3600)
if conf.monitory_celery:
app.conf.CELERY_SEND_EVENTS = True
app.conf.CELERY_SEND_TASK_SENT_EVENT = True
if conf.celery_queue_per_site:
app.conf.CELERY_ROUTES = (SiteRouter(), AsyncTaskRouter())
app.conf.CELERYBEAT_SCHEDULE = get_beat_schedule(conf)
if conf.celery_error_emails:
app.conf.CELERY_SEND_TASK_ERROR_EMAILS = True
for k, v in conf.celery_error_emails.iteritems():
setattr(app.conf, k, v)
return app
def get_site_config():
return frappe.get_site_config(sites_path=SITES_PATH)
class SiteRouter(object):
def route_for_task(self, task, args=None, kwargs=None):
if hasattr(frappe.local, 'site'):
......@@ -62,17 +71,17 @@ class SiteRouter(object):
return get_queue(frappe.local.site, LONGJOBS_PREFIX)
else:
return get_queue(frappe.local.site)
return None
class AsyncTaskRouter(object):
def route_for_task(self, task, args=None, kwargs=None):
if task == "frappe.tasks.run_async_task" and hasattr(frappe.local, 'site'):
return get_queue(frappe.local.site, ASYNC_TASKS_PREFIX)
def get_queue(site, prefix=None):
return {'queue': "{}{}".format(prefix or "", site)}
def get_beat_schedule(conf):
schedule = {
'scheduler': {
......@@ -80,17 +89,136 @@ def get_beat_schedule(conf):
'schedule': timedelta(seconds=conf.scheduler_interval or DEFAULT_SCHEDULER_INTERVAL)
},
}
if conf.celery_queue_per_site:
schedule['sync_queues'] = {
'task': 'frappe.tasks.sync_queues',
'schedule': timedelta(seconds=conf.scheduler_interval or DEFAULT_SCHEDULER_INTERVAL)
}
return schedule
def celery_task(*args, **kwargs):
return get_celery().task(*args, **kwargs)
def make_async_task(args):
task = frappe.new_doc("Async Task")
task.update(args)
task.status = "Queued"
task.set_docstatus_user_and_timestamp()
task.db_insert()
task.notify_update()
def run_test():
for i in xrange(30):
test.delay(site=frappe.local.site)
@celery_task()
def test(site=None):
time.sleep(1)
print "task"
class MonitorThread(object):
"""Thread manager for monitoring celery events"""
def __init__(self, celery_app, interval=1):
self.celery_app = celery_app
self.interval = interval
self.state = self.celery_app.events.State()
self.thread = threading.Thread(target=self.run, args=())
self.thread.daemon = True
self.thread.start()
def catchall(self, event):
if event['type'] != 'worker-heartbeat':
self.state.event(event)
if not 'uuid' in event:
return
task = self.state.tasks.get(event['uuid'])
info = task.info()
if 'name' in event and 'enqueue_events_for_site' in event['name']:
return
try:
kwargs = eval(info.get('kwargs'))
if 'site' in kwargs:
frappe.connect(kwargs['site'])
if event['type']=='task-sent':
make_async_task({
'name': event['uuid'],
'task_name': kwargs.get("cmd") or event['name']
})
elif event['type']=='task-received':
try:
task = frappe.get_doc("Async Task", event['uuid'])
task.status = 'Started'
task.set_docstatus_user_and_timestamp()
task.db_update()
task.notify_update()
except frappe.DoesNotExistError:
pass
elif event['type']=='task-succeeded':
try:
task = frappe.get_doc("Async Task", event['uuid'])
task.status = 'Succeeded'
task.result = info.get('result')
task.runtime = info.get('runtime')
task.set_docstatus_user_and_timestamp()
task.db_update()
task.notify_update()
except frappe.DoesNotExistError:
pass
elif event['type']=='task-failed':
try:
task = frappe.get_doc("Async Task", event['uuid'])
task.status = 'Failed'
task.traceback = event.get('traceback') or event.get('exception')
task.traceback = frappe.as_json(info) + "\n\n" + task.traceback
task.runtime = info.get('runtime')
task.set_docstatus_user_and_timestamp()
task.db_update()
task.notify_update()
except frappe.DoesNotExistError:
pass
frappe.db.commit()
except Exception:
print frappe.get_traceback()
finally:
frappe.destroy()
def run(self):
while True:
try:
with self.celery_app.connection() as connection:
recv = self.celery_app.events.Receiver(connection, handlers={
'*': self.catchall
})
recv.capture(limit=None, timeout=None, wakeup=True)
except (KeyboardInterrupt, SystemExit):
raise
except Exception:
# unable to capture
print "unable to capture:"
print frappe.get_traceback()
time.sleep(self.interval)
if __name__ == '__main__':
get_celery().start()
app = get_celery()
if get_site_config().get("monitor_celery"):
MonitorThread(app)
app.start()
- You can now add **CC** in Email
- Show checkboxes in Print
......@@ -2,7 +2,7 @@
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "field:celery_task_id",
"autoname": "",
"creation": "2015-07-03 11:28:03.496346",
"custom": 0,
"docstatus": 0,
......@@ -13,18 +13,63 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "celery_task_id",
"fieldname": "status",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Status",
"no_copy": 0,
"options": "\nQueued\nRunning\nSucceeded\nFailed\n",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "task_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Task Name",
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "runtime",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Celery Task ID",
"label": "Runtime",
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
......@@ -35,19 +80,18 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "status",
"fieldtype": "Select",
"fieldname": "result",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Status",
"label": "Result",
"no_copy": 0,
"options": "\nQueued\nRunning\nFinished\nFailed\n",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
......@@ -58,13 +102,13 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "stdout",
"fieldtype": "Long Text",
"fieldname": "traceback",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "stdout",
"label": "Traceback",
"no_copy": 0,
"permlevel": 0,
"precision": "",
......@@ -80,18 +124,17 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "stderr",
"fieldtype": "Long Text",
"fieldname": "section_break_6",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "stderr",
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 1,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
......@@ -114,7 +157,7 @@
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
......@@ -137,7 +180,7 @@
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
......@@ -152,7 +195,7 @@
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"modified": "2015-07-28 16:18:11.344349",
"modified": "2015-09-07 08:08:22.193911",
"modified_by": "Administrator",
"module": "Core",
"name": "Async Task",
......@@ -183,5 +226,6 @@
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"title_field": "task_name"
}
\ No newline at end of file
frappe.listview_settings['Async Task'] = {
add_fields: ["status"],
get_indicator: function(doc) {
if(doc.status==="Succeeded") {
return [__("Succeeded"), "green", "status,=,Succeeded"];
} else if(doc.status==="Failed") {
return [__("Failed"), "red", "status,=,Failed"];
}
}
};
......@@ -42,7 +42,8 @@ class Comment(Document):
message['broadcast'] = True
frappe.publish_realtime('new_message', message)
else:
frappe.publish_realtime('new_message', self.as_dict(), user=frappe.session.user)
# comment_docname contains the user who is addressed in the messages' page comment
frappe.publish_realtime('new_message', self.as_dict(), user=self.comment_docname)
else:
frappe.publish_realtime('new_comment', self.as_dict(), doctype= self.comment_doctype,
docname = self.comment_docname)
......
......@@ -37,20 +37,21 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "sent_or_received",
"depends_on": "",
"fieldname": "communication_medium",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Sent or Received",
"label": "Communication Medium",
"no_copy": 0,
"options": "Sent\nReceived",
"options": "\nChat\nPhone\nEmail\nSMS\nVisit\nOther",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
......@@ -59,17 +60,15 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "status",
"fieldtype": "Select",
"fieldname": "recipients",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Status",
"in_list_view": 0,
"label": "Recipients",
"no_copy": 0,
"options": "Open\nReplied\nClosed\nLinked",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
......@@ -82,16 +81,15 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"description": "Integrations can use this field to set email delivery status",
"fieldname": "delivery_status",
"fieldtype": "Select",
"hidden": 1,
"depends_on": "eval:doc.communication_medium===\"Email\"",
"fieldname": "cc",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Delivery Status",
"label": "CC",
"no_copy": 0,
"options": "\nSent\nBounced\nOpened\nMarked As Spam\nRejected\nDelayed\nSoft-Bounced\nClicked\nRecipient Unsubscribed",
"permlevel": 0,
"precision": "",
"print_hide": 0,
......@@ -106,19 +104,20 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "subject",
"depends_on": "eval:doc.communication_medium!==\"Email\"",
"fieldname": "phone_no",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Subject",
"label": "Phone No.",
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
......@@ -148,15 +147,15 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "reference_doctype",
"fieldtype": "Link",
"fieldname": "status",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Reference DocType",
"in_list_view": 1,
"label": "Status",
"no_copy": 0,
"options": "DocType",
"options": "Open\nReplied\nClosed\nLinked",
"permlevel": 0,
"precision": "",
"print_hide": 0,
......@@ -171,21 +170,20 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"fieldname": "sent_or_received",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Reference Name",
"in_list_view": 1,
"label": "Sent or Received",
"no_copy": 0,
"options": "reference_doctype",
"options": "Sent\nReceived",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
......@@ -194,13 +192,16 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "section_break_8",
"fieldtype": "Section Break",
"hidden": 0,
"description": "Integrations can use this field to set email delivery status",
"fieldname": "delivery_status",
"fieldtype": "Select",
"hidden": 1,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Delivery Status",
"no_copy": 0,
"options": "\nSent\nBounced\nOpened\nMarked As Spam\nRejected\nDelayed\nSoft-Bounced\nClicked\nRecipient Unsubscribed",
"permlevel": 0,
"precision": "",
"print_hide": 0,
......@@ -215,37 +216,15 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "content",
"fieldtype": "Text Editor",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Content",
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "400"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "additional_info",
"fieldname": "section_break_10",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Additional Info",
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
......@@ -258,19 +237,19 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "recipients",
"fieldname": "subject",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Recipients",
"label": "Subject",
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
......@@ -279,15 +258,15 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "phone_no",
"fieldtype": "Data",
"fieldname": "section_break_8",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Phone No.",
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
......@@ -300,15 +279,14 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "communication_medium",
"fieldtype": "Select",
"fieldname": "content",
"fieldtype": "Text Editor",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Communication Medium",
"in_list_view": 0,
"label": "Content",
"no_copy": 0,
"options": "\nChat\nPhone\nEmail\nSMS\nVisit\nOther",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
......@@ -316,21 +294,22 @@
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"unique": 0,
"width": "400"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break_14",
"fieldtype": "Column Break",
"collapsible": 1,
"fieldname": "additional_info",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "More Information",
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
......@@ -386,14 +365,15 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "section_break2",
"fieldtype": "Section Break",
"default": "Today",
"fieldname": "communication_date",
"fieldtype": "Datetime",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Date",
"no_copy": 0,
"options": "simple",
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
......@@ -407,15 +387,15 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break4",
"fieldname": "column_break_14",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "By",
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
......@@ -428,15 +408,15 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "email_account",
"fieldname": "reference_doctype",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Email Account",
"label": "Reference DocType",
"no_copy": 0,
"options": "Email Account",
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
......@@ -451,19 +431,19 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"default": "__user",
"fieldname": "user",
"fieldtype": "Link",
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 1,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "User",
"label": "Reference Name",
"no_copy": 0,
"options": "User",
"options": "reference_doctype",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 1,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
......@@ -474,17 +454,19 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break5",
"fieldtype": "Column Break",
"fieldname": "in_reply_to",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "On",
"label": "In Reply To",
"no_copy": 0,
"options": "Communication",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
......@@ -495,16 +477,17 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"default": "Today",
"fieldname": "communication_date",
"fieldtype": "Datetime",
"fieldname": "email_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Date",
"label": "Email Account",
"no_copy": 0,
"options": "Email Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
......@@ -517,17 +500,19 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "_user_tags",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"default": "__user",
"fieldname": "user",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 1,
"in_filter": 0,
"in_list_view": 0,
"label": "User Tags",
"no_copy": 1,
"label": "User",
"no_copy": 0,
"options": "User",
"permlevel": 0,
"print_hide": 1,
"read_only": 0,
"print_hide": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
......@@ -556,6 +541,27 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "_user_tags",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "User Tags",
"no_copy": 1,
"permlevel": 0,
"print_hide": 1,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
......@@ -567,7 +573,7 @@
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"modified": "2015-08-14 17:46:20.902296",
"modified": "2015-09-15 05:51:16.112080",
"modified_by": "Administrator",
"module": "Core",
"name": "Communication",
......
......@@ -5,7 +5,7 @@ from __future__ import unicode_literals, absolute_import
import frappe
import json
from email.utils import formataddr, parseaddr
from frappe.utils import get_url, get_formatted_email, cstr, cint
from frappe.utils import get_url, get_formatted_email, cstr, cint, validate_email_add, split_emails
from frappe.utils.file_manager import get_file
import frappe.email.smtp
from frappe import _
......@@ -30,10 +30,27 @@ class Communication(Document):
if self.get("__islocal"):
if self.reference_doctype and self.reference_name:
self.status = "Linked"
else:
self.status = "Open"
# validate recipients
for email in split_emails(self.recipients):
validate_email_add(email, throw=True)
# validate CC
for email in split_emails(self.cc):
validate_email_add(email, throw=True)
def after_insert(self):
# send new comment to listening clients
comment = self.as_dict()
comment["comment"] = comment["content"]
comment["comment_by"] = comment["sender"]
comment["comment_type"] = comment["communication_medium"]
frappe.publish_realtime('new_comment', comment, doctype = self.reference_doctype,
docname = self.reference_name)
def on_update(self):
"""Update parent status as `Open` or `Replied`."""
self.update_parent()
......@@ -50,9 +67,9 @@ class Communication(Document):
to_status = "Open" if self.sent_or_received=="Received" else "Replied"
if to_status in status_field.options.splitlines():
frappe.db.set_value(parent.doctype, parent.name, "status", to_status)
parent.db_set("status", to_status)
parent.notify_modified()
parent.notify_update()
def send(self, print_html=None, print_format=None, attachments=None,
send_me_a_copy=False, recipients=None):
......@@ -64,51 +81,41 @@ class Communication(Document):
self.send_me_a_copy = send_me_a_copy
self.notify(print_html, print_format, attachments, recipients)
def set_incoming_outgoing_accounts(self):
self.incoming_email_account = self.outgoing_email_account = None
if self.reference_doctype:
self.incoming_email_account = frappe.db.get_value("Email Account",
{"append_to": self.reference_doctype, "enable_incoming": 1}, "email_id")
self.outgoing_email_account = frappe.db.get_value("Email Account",
{"append_to": self.reference_doctype, "enable_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender"], as_dict=True)
if not self.incoming_email_account:
self.incoming_email_account = frappe.db.get_value("Email Account", {"default_incoming": 1}, "email_id")
if not self.outgoing_email_account:
self.outgoing_email_account = frappe.db.get_value("Email Account", {"default_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender"], as_dict=True) or frappe._dict()
def notify(self, print_html=None, print_format=None, attachments=None, recipients=None, except_recipient=False):
def notify(self, print_html=None, print_format=None, attachments=None,
recipients=None, cc=None, fetched_from_email_account=False):
"""Calls a delayed celery task 'sendmail' that enqueus email in Bulk Email queue
:param print_html: Send given value as HTML attachment
:param print_format: Attach print format of parent document
:param attachments: A list of filenames that should be attached when sending this email
:param recipients: Email recipients
:param except_recipient: True when pulling email, the notification shouldn't go to the main recipient
:param cc: Send email as CC to
:param fetched_from_email_account: True when pulling email, the notification shouldn't go to the main recipient
"""
recipients, cc = self.get_recipients_and_cc(recipients, cc,
fetched_from_email_account=fetched_from_email_account)
self.emails_not_sent_to = set(self.all_email_addresses) - set(recipients) - set(cc)
if frappe.flags.in_test:
# for test cases, run synchronously
self._notify(print_html=print_html, print_format=print_format, attachments=attachments,
recipients=recipients, except_recipient=except_recipient)
recipients=recipients, cc=cc)
else:
from frappe.tasks import sendmail
sendmail.delay(frappe.local.site, self.name,
print_html=print_html, print_format=print_format, attachments=attachments,
recipients=recipients, except_recipient=except_recipient)
recipients=recipients, cc=cc)
def _notify(self, print_html=None, print_format=None, attachments=None,
recipients=None, cc=None):
def _notify(self, print_html=None, print_format=None, attachments=None, recipients=None, except_recipient=False):
self.prepare_to_notify(print_html, print_format, attachments)
if not recipients:
recipients = self.get_recipients(except_recipient=except_recipient)
frappe.sendmail(
recipients=recipients,
recipients=(recipients or []) + (cc or []),
expose_recipients=True,
sender=self.sender,
reply_to=self.incoming_email_account,
subject=self.subject,
......@@ -121,6 +128,27 @@ class Communication(Document):
bulk=True
)
def get_recipients_and_cc(self, recipients, cc, fetched_from_email_account=False):
self.all_email_addresses = []
if not recipients:
recipients = self.get_recipients()
if not cc:
cc = self.get_cc(recipients, fetched_from_email_account=fetched_from_email_account)
if fetched_from_email_account:
# email was already sent to the original recipient by the sender's email service
original_recipients, recipients = recipients, []
# cc that was received in the email
original_cc = split_emails(self.cc)
# don't cc to people who already received the mail from sender's email service
cc = list(set(cc) - set(original_cc) - set(original_recipients))
return recipients, cc
def prepare_to_notify(self, print_html=None, print_format=None, attachments=None):
"""Prepare to make multipart MIME Email
......@@ -156,78 +184,129 @@ class Communication(Document):
else:
self.attachments.append(a)
def get_recipients(self, except_recipient=False):
"""Build a list of users to which this email should go to"""
def set_incoming_outgoing_accounts(self):
self.incoming_email_account = self.outgoing_email_account = None
if self.reference_doctype:
self.incoming_email_account = frappe.db.get_value("Email Account",
{"append_to": self.reference_doctype, "enable_incoming": 1}, "email_id")
self.outgoing_email_account = frappe.db.get_value("Email Account",
{"append_to": self.reference_doctype, "enable_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender"], as_dict=True)
if not self.incoming_email_account:
self.incoming_email_account = frappe.db.get_value("Email Account", {"default_incoming": 1}, "email_id")
if not self.outgoing_email_account:
self.outgoing_email_account = frappe.db.get_value("Email Account", {"default_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender"], as_dict=True) or frappe._dict()
def get_recipients(self):
"""Build a list of email addresses for To"""
# [EDGE CASE] self.recipients can be None when an email is sent as BCC
original_recipients = [s.strip() for s in cstr(self.recipients).split(",")]
recipients = original_recipients[:]
recipients = split_emails(self.recipients)
if recipients:
# this will be used to eventually find email addresses that aren't sent to
self.all_email_addresses.extend(recipients)
# exclude email accounts
exclude = [d[0] for d in
frappe.db.get_all("Email Account", ["email_id"], {"enable_incoming": 1}, as_list=True)]
exclude += [d[0] for d in
frappe.db.get_all("Email Account", ["login_id"], {"enable_incoming": 1}, as_list=True)
if d[0]]
recipients = self.filter_email_list(recipients, exclude)
return recipients
def get_cc(self, recipients=None, fetched_from_email_account=False):
"""Build a list of email addresses for CC"""
# get a copy of CC list
cc = split_emails(self.cc)
if self.reference_doctype and self.reference_name:
recipients += self.get_earlier_participants()
recipients += self.get_commentors()
recipients += self.get_assignees()
recipients += self.get_starrers()
if not cc or fetched_from_email_account:
# if CC is not mentioned from the UI or is a fetched email, add follows to CC
cc.append(self.get_owner_email())
cc += self.get_assignees()
cc += self.get_starrers()
if fetched_from_email_account and self.in_reply_to:
# add sender of previous reply
cc.append(frappe.db.get_value("Communication", self.in_reply_to, "sender"))
if cc:
# this will be used to eventually find email addresses that aren't sent to
self.all_email_addresses.extend(cc)
# exclude email accounts, unfollows, recipients and unsubscribes
exclude = [d[0] for d in
frappe.db.get_all("Email Account", ["email_id"], {"enable_incoming": 1}, as_list=True)]
exclude += [d[0] for d in
frappe.db.get_all("Email Account", ["login_id"], {"enable_incoming": 1}, as_list=True)
if d[0]]
exclude += [d[0] for d in frappe.db.get_all("User", ["name"], {"thread_notify": 0}, as_list=True)]
exclude += [parseaddr(email)[1] for email in recipients]
if fetched_from_email_account:
# exclude sender when pulling email
exclude += [parseaddr(self.sender)[1]]
# remove unsubscribed recipients
unsubscribed = [d[0] for d in frappe.db.get_all("User", ["name"], {"thread_notify": 0}, as_list=True)]
email_accounts = [d[0] for d in frappe.db.get_all("Email Account", ["email_id"], {"enable_incoming": 1}, as_list=True)]
sender = parseaddr(self.sender)[1]
if self.reference_doctype and self.reference_name:
exclude += [d[0] for d in frappe.db.get_all("Email Unsubscribe", ["email"],
{"reference_doctype": self.reference_doctype, "reference_name": self.reference_name}, as_list=True)]
filtered = []
email_addresses = []
for e in list(set(recipients)):
if (e=="Administrator") or ((e==self.sender) and (e not in original_recipients)) or \
(e in unsubscribed) or (e in email_accounts):
continue
cc = self.filter_email_list(cc, exclude)
if getattr(self, "send_me_a_copy", False) and self.sender not in cc:
self.all_email_addresses.append(self.sender)
cc.append(self.sender)
email_id = parseaddr(e)[1]
return cc
def filter_email_list(self, email_list, exclude):
# temp variables
filtered = []
email_address_list = []
if not email_id:
for email in list(set(email_list)):
if email in exclude:
continue
if email_id==sender or email_id in unsubscribed or email_id in email_accounts:
email_address = (parseaddr(email)[1] or "").lower()
if not email_address:
continue
if except_recipient and (e==self.recipients or email_id==self.recipients):
# while pulling email, don't send email to current recipient
if email_address in exclude:
continue
# make sure of case-insensitive uniqueness of email address
if email_id.lower() not in email_addresses:
if email_address not in email_address_list:
# append the full email i.e. "Human <human@example.com>"
filtered.append(e)
email_addresses.append(email_id.lower())
if getattr(self, "send_me_a_copy", False):
filtered.append(self.sender)
filtered.append(email)
email_address_list.append(email_address)
return filtered
def get_starrers(self):
"""Return list of users who have starred this document."""
if self.reference_doctype and self.reference_name:
return self.get_parent_doc().get_starred_by()
else:
return []
def get_earlier_participants(self):
return frappe.db.sql_list("""
select distinct sender
from tabCommunication where
reference_doctype=%s and reference_name=%s""",
(self.reference_doctype, self.reference_name))
def get_commentors(self):
return frappe.db.sql_list("""
select distinct comment_by
from tabComment where
comment_doctype=%s and comment_docname=%s and
ifnull(unsubscribed, 0)=0 and comment_by!='Administrator'""",
(self.reference_doctype, self.reference_name))
return [( get_formatted_email(user) or user ) for user in self.get_parent_doc().get_starred_by()]
def get_owner_email(self):
owner = self.get_parent_doc().owner
return get_formatted_email(owner) or owner
def get_assignees(self):
return [d.owner for d in frappe.db.get_all("ToDo", filters={"reference_type": self.reference_doctype,
"reference_name": self.reference_name, "status": "Open"}, fields=["owner"])]
return [( get_formatted_email(d.owner) or d.owner ) for d in
frappe.db.get_all("ToDo", filters={
"reference_type": self.reference_doctype,
"reference_name": self.reference_name,
"status": "Open"
}, fields=["owner"])
]
def get_attach_link(self, print_format):
"""Returns public link for the attachment via `templates/emails/print_link.html`."""
......@@ -247,7 +326,7 @@ def on_doctype_update():
def make(doctype=None, name=None, content=None, subject=None, sent_or_received = "Sent",
sender=None, recipients=None, communication_medium="Email", send_email=False,
print_html=None, print_format=None, attachments='[]', ignore_doctype_permissions=False,
send_me_a_copy=False):
send_me_a_copy=False, cc=None):
"""Make a new communication.
:param doctype: Reference DocType.
......@@ -280,6 +359,7 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
"content": content,
"sender": sender,
"recipients": recipients,
"cc": cc or None,
"communication_medium": "Email",
"sent_or_received": sent_or_received,
"reference_doctype": doctype,
......@@ -291,15 +371,13 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received =
# if not committed, delayed task doesn't find the communication
frappe.db.commit()
recipients = None
if send_email:
comm.send_me_a_copy = send_me_a_copy
recipients = comm.get_recipients()
comm.send(print_html, print_format, attachments, send_me_a_copy=send_me_a_copy, recipients=recipients)
comm.send(print_html, print_format, attachments, send_me_a_copy=send_me_a_copy)
return {
"name": comm.name,
"recipients": ", ".join(recipients) if recipients else None
"emails_not_sent_to": ", ".join(comm.emails_not_sent_to) if hasattr(comm, "emails_not_sent_to") else None
}
@frappe.whitelist()
......
......@@ -64,7 +64,7 @@
"in_filter": 0,
"in_list_view": 1,
"label": "Title",
"no_copy": 0,
"no_copy": 1,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
......@@ -217,7 +217,7 @@
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"modified": "2015-07-13 04:45:55.942795",
"modified": "2015-09-11 12:19:55.121822",
"modified_by": "Administrator",
"module": "Core",
"name": "Page",
......
......@@ -75,20 +75,35 @@ frappe.DataImportTool = Class.extend({
onerror: function(r) {
me.onerror(r);
},
start: function() {
queued: function() {
// async, show queued
msg_dialog.clear();
msgprint(__("Import Request Queued. This may take a few moments, please be patient."));
},
running: function() {
// update async status as running
msg_dialog.clear();
msgprint(__("Importing..."));
me.write_messages([__("Importing")]);
me.has_progress = false;
},
progress: function(data) {
// show callback if async
if(data.progress) {
frappe.hide_msgprint(true);
frappe.show_progress(__("Importing"), data.progress[0], data.progress[1]);
me.has_progress = true;
frappe.show_progress(__("Importing"), data.progress[0],
data.progress[1]);
}
},
callback: function(attachment, r) {
if(r.message.error) {
me.onerror(r);
} else {
frappe.show_progress(__("Importing"), 1, 1);
if(me.has_progress) {
frappe.show_progress(__("Importing"), 1, 1);
setTimeout(frappe.hide_progress, 1000);
}
r.messages = ["<h5 style='color:green'>" + __("Import Successful!") + "</h5>"].
concat(r.message.messages)
......@@ -98,6 +113,15 @@ frappe.DataImportTool = Class.extend({
}
});
frappe.realtime.on("data_import_progress", function(data) {
if(data.progress) {
frappe.hide_msgprint(true);
me.has_progress = true;
frappe.show_progress(__("Importing"), data.progress[0],
data.progress[1]);
}
})
},
write_messages: function(data) {
this.page.main.find(".import-log").removeClass("hide");
......
......@@ -16,7 +16,7 @@ from frappe.utils import cint, cstr, flt
from frappe.core.page.data_import_tool.data_import_tool import get_data_keys
#@frappe.async.handler
@frappe.whitelist()
frappe.whitelist()
def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, overwrite=None,
ignore_links=False, pre_process=None):
"""upload data"""
......@@ -203,11 +203,11 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False,
row_idx = i + start_row
doc = None
frappe.publish_realtime(message = {"progress": [i, total]})
# publish task_update
frappe.publish_realtime("data_import_progress", {"progress": [i, total]},
user=frappe.session.user, now=True)
try:
frappe.local.message_log = []
doc = get_doc(row_idx)
if pre_process:
pre_process(doc)
......@@ -243,6 +243,8 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False,
ret.append('Error for row (#%d) %s : %s' % (row_idx + 1,
len(row)>1 and row[1] or "", err_msg))
frappe.errprint(frappe.get_traceback())
finally:
frappe.local.message_log = []
if error:
frappe.db.rollback()
......
......@@ -503,13 +503,6 @@ frappe.PermissionEngine = Class.extend({
get_perm: function(name) {
return $.map(this.perm_list, function(d) { if(d.name==name) return d; })[0];
},
get_user_fields: function(doctype) {
var user_fields = frappe.get_children("DocType", doctype, "fields", {fieldtype:"Link", options:"User"})
user_fields = user_fields.concat(frappe.get_children("DocType", doctype, "fields",
{fieldtype:"Select", link_doctype:"User"}))
return user_fields
},
get_link_fields: function(doctype) {
return frappe.get_children("DocType", doctype, "fields",
{fieldtype:"Link", options:["not in", ["User", '[Select]']]});
......
......@@ -20,7 +20,7 @@
"in_filter": 0,
"in_list_view": 0,
"label": "Title",
"no_copy": 0,
"no_copy": 1,
"permlevel": 0,
"print_hide": 1,
"read_only": 0,
......@@ -84,7 +84,7 @@
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"modified": "2015-09-07 15:51:26",
"modified": "2015-09-11 12:20:04.912891",
"modified_by": "Administrator",
"module": "Desk",
"name": "Note",
......
......@@ -33,6 +33,7 @@ class FormMeta(Meta):
def load_assets(self):
self.add_search_fields()
self.add_linked_document_type()
if not self.istable:
self.add_linked_with()
......@@ -49,7 +50,7 @@ class FormMeta(Meta):
d[k] = self.get(k)
for i, df in enumerate(d.get("fields")):
for k in ("link_doctype", "search_fields", "is_custom_field"):
for k in ("search_fields", "is_custom_field", "linked_document_type"):
df[k] = self.get("fields")[i].get(k)
return d
......@@ -120,6 +121,15 @@ class FormMeta(Meta):
if search_fields:
df.search_fields = map(lambda sf: sf.strip(), search_fields.split(","))
def add_linked_document_type(self):
for df in self.get("fields", {"fieldtype": "Link"}):
if df.options:
try:
df.linked_document_type = frappe.get_meta(df.options).document_type
except frappe.DoesNotExistError:
# edge case where options="[Select]"
pass
def add_linked_with(self):
"""add list of doctypes this doctype is 'linked' with.
......
......@@ -41,6 +41,7 @@ frappe.desk.pages.Messages = Class.extend({
},
setup_realtime: function() {
var me = this;
frappe.realtime.on('new_message', function(comment) {
if(comment.modified_by !== user) {
frappe.utils.notify(__("Message from {0}", [comment.comment_by_fullname]), comment.comment);
......@@ -48,16 +49,20 @@ frappe.desk.pages.Messages = Class.extend({
if (frappe.get_route()[0] === 'messages') {
var current_contact = $(cur_page.page).find('[data-contact]').data('contact');
var on_broadcast_page = current_contact === user;
if (current_contact == comment.owner || (on_broadcast_page && comment.broadcast)) {
var $row = $('<div class="list-row"/>');
frappe.desk.pages.messages.list.data.unshift(comment);
frappe.desk.pages.messages.list.render_row($row, comment);
frappe.desk.pages.messages.list.parent.prepend($row);
if ((current_contact == comment.owner) || (on_broadcast_page && comment.broadcast)) {
me.prepend_comment(comment);
}
}
});
},
prepend_comment: function(comment) {
var $row = $('<div class="list-row"/>');
frappe.desk.pages.messages.list.data.unshift(comment);
frappe.desk.pages.messages.list.render_row($row, comment);
frappe.desk.pages.messages.list.$w.prepend($row);
},
make_sidebar: function() {
var me = this;
return frappe.call({
......@@ -124,7 +129,9 @@ frappe.desk.pages.Messages = Class.extend({
},
callback:function(r,rt) {
textarea.val('');
me.list.run();
if (!r.exc) {
me.prepend_comment(r.message);
}
},
btn: this
});
......
......@@ -89,6 +89,8 @@ def post(txt, contact, parenttype=None, notify=False, subject=None):
else:
_notify(contact, txt, subject)
return d
@frappe.whitelist()
def delete(arg=None):
frappe.get_doc("Comment", frappe.form_dict['name']).delete()
......
......@@ -10,13 +10,14 @@ from frappe.email.smtp import SMTPServer, get_outgoing_email_account
from frappe.email.email_body import get_email, get_formatted_html
from frappe.utils.verified_command import get_signed_params, verify_request
from html2text import html2text
from frappe.utils import get_url, nowdate, encode, now_datetime, add_days
from frappe.utils import get_url, nowdate, encode, now_datetime, add_days, split_emails
class BulkLimitCrossedError(frappe.ValidationError): pass
def send(recipients=None, sender=None, subject=None, message=None, reference_doctype=None,
reference_name=None, unsubscribe_method=None, unsubscribe_params=None, unsubscribe_message=None,
attachments=None, reply_to=None, cc=(), message_id=None, send_after=None):
attachments=None, reply_to=None, cc=(), message_id=None, send_after=None,
expose_recipients=False):
"""Add email to sending queue (Bulk Email)
:param recipients: List of recipients.
......@@ -39,7 +40,7 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc
return
if isinstance(recipients, basestring):
recipients = recipients.split(",")
recipients = split_emails(recipients)
if isinstance(send_after, int):
send_after = add_days(nowdate(), send_after)
......@@ -66,23 +67,30 @@ def send(recipients=None, sender=None, subject=None, message=None, reference_doc
else:
unsubscribed = []
for email in filter(None, list(set(recipients))):
if email not in unsubscribed:
email_content = formatted
email_text_context = text_content
recipients = [r for r in list(set(recipients)) if r and r not in unsubscribed]
if reference_doctype:
unsubscribe_url = get_unsubcribed_url(reference_doctype, reference_name, email,
unsubscribe_method, unsubscribe_params)
for email in recipients:
email_content = formatted
email_text_context = text_content
# add to queue
email_content = add_unsubscribe_link(email_content, email, reference_doctype,
reference_name, unsubscribe_url, unsubscribe_message)
if reference_doctype:
unsubscribe_link = get_unsubscribe_link(
reference_doctype=reference_doctype,
reference_name=reference_name,
email=email,
recipients=recipients,
expose_recipients=expose_recipients,
unsubscribe_method=unsubscribe_method,
unsubscribe_params=unsubscribe_params,
unsubscribe_message=unsubscribe_message
)
email_text_context += "\n" + _("This email was sent to {0}. To unsubscribe click on this link: {1}").format(email, unsubscribe_url)
email_content = email_content.replace("<!--unsubscribe link here-->", unsubscribe_link.html)
email_text_context += unsubscribe_link.text
add(email, sender, subject, email_content, email_text_context, reference_doctype,
reference_name, attachments, reply_to, cc, message_id, send_after)
# add to queue
add(email, sender, subject, email_content, email_text_context, reference_doctype,
reference_name, attachments, reply_to, cc, message_id, send_after)
def add(email, sender, subject, formatted, text_content=None,
reference_doctype=None, reference_name=None, attachments=None, reply_to=None,
......@@ -129,18 +137,41 @@ def check_bulk_limit(recipients):
throw(_("Email limit {0} crossed").format(monthly_bulk_mail_limit),
BulkLimitCrossedError)
def add_unsubscribe_link(message, email, reference_doctype, reference_name, unsubscribe_url, unsubscribe_message):
unsubscribe_link = """<div style="padding: 7px; text-align: center; color: #8D99A6;">
{email}. <a href="{unsubscribe_url}" style="color: #8D99A6; text-decoration: underline;
target="_blank">{unsubscribe_message}.
</a>
</div>""".format(unsubscribe_url = unsubscribe_url,
email= _("This email was sent to {0}").format(email),
unsubscribe_message = unsubscribe_message or _("Unsubscribe from this list"))
message = message.replace("<!--unsubscribe link here-->", unsubscribe_link)
return message
def get_unsubscribe_link(reference_doctype, reference_name,
email, recipients, expose_recipients, unsubscribe_method, unsubscribe_params, unsubscribe_message):
unsubscribe_email = recipients if expose_recipients else [email]
unsubscribe_email = _("This email was sent to {0}").format(", ".join(unsubscribe_email))
if not unsubscribe_message:
unsubscribe_message = _("Unsubscribe from this list")
unsubscribe_url = get_unsubcribed_url(reference_doctype, reference_name, email,
unsubscribe_method, unsubscribe_params)
html = """<div style="margin: 15px auto; padding: 0px 7px; text-align: center; color: #8d99a6;">
{email}
<p style="margin: 15px auto;">
<a href="{unsubscribe_url}" style="color: #8d99a6; text-decoration: underline;
target="_blank">{unsubscribe_message}
</a>
</p>
</div>""".format(
unsubscribe_url = unsubscribe_url,
email=unsubscribe_email,
unsubscribe_message=unsubscribe_message
)
text = "\n{email}\n\n{unsubscribe_message}: {unsubscribe_url}".format(
email=unsubscribe_email,
unsubscribe_message=unsubscribe_message,
unsubscribe_url=unsubscribe_url
)
return frappe._dict({
"html": html,
"text": text
})
def get_unsubcribed_url(reference_doctype, reference_name, email, unsubscribe_method, unsubscribe_params):
params = {"email": email.encode("utf-8"),
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment