@@ -1,2 +1,2 @@ | |||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
__version__ = "5.2.2" | |||||
__version__ = "5.3.0" |
@@ -0,0 +1,40 @@ | |||||
- Added Language Support for Following languages | |||||
<table class="table table-bordered"> | |||||
<tr> | |||||
<td style="width: 30%">bo*</td> | |||||
<td>ལྷ་སའི་སྐད་</td> | |||||
</tr> | |||||
<tr> | |||||
<td>fi</td> | |||||
<td>suomalainen</td> | |||||
</tr> | |||||
<tr> | |||||
<td>km</td> | |||||
<td>ភាសាខ្មែរ</td> | |||||
</tr> | |||||
<tr> | |||||
<td>mk</td> | |||||
<td>македонски</td> | |||||
</tr> | |||||
<tr> | |||||
<td>my</td> | |||||
<td>Melayu</td> | |||||
</tr> | |||||
<tr> | |||||
<td>no</td> | |||||
<td>norsk</td> | |||||
</tr> | |||||
<tr> | |||||
<td>sv</td> | |||||
<td>Svenska</td> | |||||
</tr> | |||||
<tr> | |||||
<td>sq</td> | |||||
<td>shqiptar</td> | |||||
</tr> | |||||
</table> | |||||
* Unable to find translations for Tibetian via Google | |||||
- To contribute to translations, please login to [https://translate.erpnext.com](https://translate.erpnext.com) |
@@ -103,7 +103,7 @@ def _is_scheduler_enabled(): | |||||
enable_scheduler = False | enable_scheduler = False | ||||
try: | try: | ||||
frappe.connect() | frappe.connect() | ||||
enable_scheduler = cint(frappe.db.get_default("enable_scheduler")) | |||||
enable_scheduler = cint(frappe.db.get_single_value("System Settings", "enable_scheduler")) and True or False | |||||
except: | except: | ||||
pass | pass | ||||
finally: | finally: | ||||
@@ -72,6 +72,26 @@ class Communication(Document): | |||||
["email_id", "always_use_account_email_id_as_sender"], as_dict=True) or frappe._dict() | ["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, except_recipient=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 | |||||
""" | |||||
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) | |||||
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) | |||||
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) | self.prepare_to_notify(print_html, print_format, attachments) | ||||
if not recipients: | if not recipients: | ||||
recipients = self.get_recipients(except_recipient=except_recipient) | recipients = self.get_recipients(except_recipient=except_recipient) | ||||
@@ -143,6 +163,7 @@ class Communication(Document): | |||||
sender = parseaddr(self.sender)[1] | sender = parseaddr(self.sender)[1] | ||||
filtered = [] | filtered = [] | ||||
email_addresses = [] | |||||
for e in list(set(recipients)): | for e in list(set(recipients)): | ||||
if (e=="Administrator") or ((e==self.sender) and (e not in original_recipients)) or \ | if (e=="Administrator") or ((e==self.sender) and (e not in original_recipients)) or \ | ||||
(e in unsubscribed) or (e in email_accounts): | (e in unsubscribed) or (e in email_accounts): | ||||
@@ -160,8 +181,11 @@ class Communication(Document): | |||||
# while pulling email, don't send email to current recipient | # while pulling email, don't send email to current recipient | ||||
continue | continue | ||||
if e not in filtered and email_id not in filtered: | |||||
# make sure of case-insensitive uniqueness of email address | |||||
if email_id.lower() not in email_addresses: | |||||
# append the full email i.e. "Human <human@example.com>" | |||||
filtered.append(e) | filtered.append(e) | ||||
email_addresses.append(email_id.lower()) | |||||
if getattr(self, "send_me_a_copy", False): | if getattr(self, "send_me_a_copy", False): | ||||
filtered.append(self.sender) | filtered.append(self.sender) | ||||
@@ -252,6 +276,10 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = | |||||
}) | }) | ||||
comm.insert(ignore_permissions=True) | comm.insert(ignore_permissions=True) | ||||
# needed for communication.notify which uses celery delay | |||||
# if not committed, delayed task doesn't find the communication | |||||
frappe.db.commit() | |||||
recipients = None | recipients = None | ||||
if send_email: | if send_email: | ||||
comm.send_me_a_copy = send_me_a_copy | comm.send_me_a_copy = send_me_a_copy | ||||
@@ -277,7 +277,7 @@ frappe.PermissionEngine = Class.extend({ | |||||
}, | }, | ||||
rights: ["read", "write", "create", "delete", "submit", "cancel", "amend", | rights: ["read", "write", "create", "delete", "submit", "cancel", "amend", | ||||
"print", "email", "report", "import", "export", "set_user_permissions"], | |||||
"print", "email", "report", "import", "export", "set_user_permissions", "share"], | |||||
set_show_users: function(cell, role) { | set_show_users: function(cell, role) { | ||||
cell.html("<a class='grey' href='#'>"+__(role)+"</a>") | cell.html("<a class='grey' href='#'>"+__(role)+"</a>") | ||||
@@ -1,5 +1,6 @@ | |||||
ar العربية | ar العربية | ||||
bg bǎlgarski | bg bǎlgarski | ||||
bo ལྷ་སའི་སྐད་ | |||||
bs bosanski | bs bosanski | ||||
ca català | ca català | ||||
cs česky | cs česky | ||||
@@ -9,6 +10,7 @@ el ελληνικά | |||||
es español | es español | ||||
en english | en english | ||||
fa پارسی | fa پارسی | ||||
fi suomalainen | |||||
fr français | fr français | ||||
he עברית | he עברית | ||||
hi हिंदी | hi हिंदी | ||||
@@ -17,21 +19,27 @@ hu magyar | |||||
id Indonesia | id Indonesia | ||||
it italiano | it italiano | ||||
ja 日本語 | ja 日本語 | ||||
km ភាសាខ្មែរ | |||||
kn ಕನ್ನಡ | kn ಕನ್ನಡ | ||||
ko 한국의 | ko 한국의 | ||||
lv latviešu valoda | lv latviešu valoda | ||||
mr मराठी | mr मराठी | ||||
mk македонски | |||||
my Melayu | |||||
nl nederlands | nl nederlands | ||||
no norsk | |||||
pl polski | pl polski | ||||
pt português | pt português | ||||
pt-BR português brasileiro | pt-BR português brasileiro | ||||
ro român | ro român | ||||
ru русский | ru русский | ||||
sv svenska | |||||
sk slovenčina | sk slovenčina | ||||
sq shqiptar | |||||
sr српски | sr српски | ||||
ta தமிழ் | ta தமிழ் | ||||
th ไทย | th ไทย | ||||
tr Türk | tr Türk | ||||
vi việt | vi việt | ||||
zh-cn 中国(简体) | |||||
zh-tw 中國(繁體) | |||||
zh-cn 簡體中文 | |||||
zh-tw 正體中文 |
@@ -258,11 +258,13 @@ def get_columns_dict(columns): | |||||
else: | else: | ||||
col_dict["fieldtype"] = col[1] | col_dict["fieldtype"] = col[1] | ||||
col_dict["fieldname"] = col[0].lower() | |||||
col_dict["fieldname"] = frappe.scrub(col[0]) | |||||
# dict | # dict | ||||
else: | else: | ||||
col_dict.update(col) | col_dict.update(col) | ||||
if "fieldname" not in col_dict: | |||||
col_dict["fieldname"] = frappe.scrub(col_dict["label"]) | |||||
columns_dict[idx] = col_dict | columns_dict[idx] = col_dict | ||||
columns_dict[col_dict["fieldname"]] = col_dict | columns_dict[col_dict["fieldname"]] = col_dict | ||||
@@ -4,6 +4,7 @@ | |||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
import frappe | import frappe | ||||
import HTMLParser | import HTMLParser | ||||
import smtplib | |||||
from frappe import msgprint, throw, _ | from frappe import msgprint, throw, _ | ||||
from frappe.email.smtp import SMTPServer, get_outgoing_email_account | from frappe.email.smtp import SMTPServer, get_outgoing_email_account | ||||
from frappe.email.email_body import get_email, get_formatted_html | from frappe.email.email_body import get_email, get_formatted_html | ||||
@@ -107,14 +108,18 @@ def add(email, sender, subject, formatted, text_content=None, | |||||
e.insert(ignore_permissions=True) | e.insert(ignore_permissions=True) | ||||
def check_bulk_limit(recipients): | def check_bulk_limit(recipients): | ||||
# get count of mails sent this month | |||||
this_month = frappe.db.sql("""select count(*) from `tabBulk Email` where | this_month = frappe.db.sql("""select count(*) from `tabBulk Email` where | ||||
MONTH(creation)=MONTH(CURDATE())""")[0][0] | |||||
status='Sent' and MONTH(creation)=MONTH(CURDATE())""")[0][0] | |||||
# if using settings from site_config.json, check bulk limit | |||||
# No limit for own email settings | # No limit for own email settings | ||||
smtp_server = SMTPServer() | smtp_server = SMTPServer() | ||||
if smtp_server.email_account and getattr(smtp_server.email_account, | |||||
"from_site_config", False) or frappe.flags.in_test: | |||||
if (smtp_server.email_account | |||||
and getattr(smtp_server.email_account, "from_site_config", False) | |||||
or frappe.flags.in_test): | |||||
monthly_bulk_mail_limit = frappe.conf.get('monthly_bulk_mail_limit') or 500 | monthly_bulk_mail_limit = frappe.conf.get('monthly_bulk_mail_limit') or 500 | ||||
if (this_month + len(recipients)) > monthly_bulk_mail_limit: | if (this_month + len(recipients)) > monthly_bulk_mail_limit: | ||||
@@ -174,6 +179,9 @@ def flush(from_test=False): | |||||
auto_commit = not from_test | auto_commit = not from_test | ||||
# additional check | |||||
check_bulk_limit([]) | |||||
if frappe.flags.mute_emails or frappe.conf.get("mute_emails") or False: | if frappe.flags.mute_emails or frappe.conf.get("mute_emails") or False: | ||||
msgprint(_("Emails are muted")) | msgprint(_("Emails are muted")) | ||||
from_test = True | from_test = True | ||||
@@ -200,11 +208,16 @@ def flush(from_test=False): | |||||
frappe.db.sql("""update `tabBulk Email` set status='Sent' where name=%s""", | frappe.db.sql("""update `tabBulk Email` set status='Sent' where name=%s""", | ||||
(email["name"],), auto_commit=auto_commit) | (email["name"],), auto_commit=auto_commit) | ||||
except smtplib.SMTPException: | |||||
# bad connection, retry later | |||||
frappe.db.sql("""update `tabBulk Email` set status='Not Sent' where name=%s""", | |||||
(email["name"],), auto_commit=auto_commit) | |||||
except Exception, e: | except Exception, e: | ||||
frappe.db.sql("""update `tabBulk Email` set status='Error', error=%s | frappe.db.sql("""update `tabBulk Email` set status='Error', error=%s | ||||
where name=%s""", (unicode(e), email["name"]), auto_commit=auto_commit) | where name=%s""", (unicode(e), email["name"]), auto_commit=auto_commit) | ||||
def clear_outbox(): | def clear_outbox(): | ||||
"""remove mails older than 30 days in Outbox""" | |||||
"""Remove mails older than 31 days in Outbox. Called daily via scheduler.""" | |||||
frappe.db.sql("""delete from `tabBulk Email` where | frappe.db.sql("""delete from `tabBulk Email` where | ||||
datediff(now(), creation) > 30""") | |||||
datediff(now(), creation) > 31""") |
@@ -124,7 +124,7 @@ class EmailAccount(Document): | |||||
exceptions = [] | exceptions = [] | ||||
for raw in incoming_mails: | for raw in incoming_mails: | ||||
try: | try: | ||||
self.insert_communication(raw) | |||||
communication = self.insert_communication(raw) | |||||
except Exception: | except Exception: | ||||
frappe.db.rollback() | frappe.db.rollback() | ||||
@@ -132,6 +132,7 @@ class EmailAccount(Document): | |||||
else: | else: | ||||
frappe.db.commit() | frappe.db.commit() | ||||
communication.notify(attachments=communication._attachments, except_recipient=True) | |||||
if exceptions: | if exceptions: | ||||
raise Exception, frappe.as_json(exceptions) | raise Exception, frappe.as_json(exceptions) | ||||
@@ -156,7 +157,7 @@ class EmailAccount(Document): | |||||
communication.insert(ignore_permissions = 1) | communication.insert(ignore_permissions = 1) | ||||
# save attachments | # save attachments | ||||
email.save_attachments_in_doc(communication) | |||||
communication._attachments = email.save_attachments_in_doc(communication) | |||||
if self.enable_auto_reply and getattr(communication, "is_first", False): | if self.enable_auto_reply and getattr(communication, "is_first", False): | ||||
self.send_auto_reply(communication, email) | self.send_auto_reply(communication, email) | ||||
@@ -164,7 +165,8 @@ class EmailAccount(Document): | |||||
# notify all participants of this thread | # notify all participants of this thread | ||||
# convert content to HTML - by default text parts of replies are used. | # convert content to HTML - by default text parts of replies are used. | ||||
communication.content = markdown2.markdown(communication.content) | communication.content = markdown2.markdown(communication.content) | ||||
communication.notify(attachments=email.attachments, except_recipient = True) | |||||
return communication | |||||
def set_thread(self, communication, email): | def set_thread(self, communication, email): | ||||
"""Appends communication to parent based on thread ID. Will extract | """Appends communication to parent based on thread ID. Will extract | ||||
@@ -153,7 +153,7 @@ class POP3Server: | |||||
"Connection timed out", | "Connection timed out", | ||||
) | ) | ||||
for message in messages: | for message in messages: | ||||
if message in strip(cstr(e.message)): | |||||
if message in strip(cstr(e.message)) or message in strip(cstr(getattr(e, 'strerror', ''))): | |||||
return True | return True | ||||
return False | return False | ||||
@@ -296,10 +296,13 @@ class Email: | |||||
def save_attachments_in_doc(self, doc): | def save_attachments_in_doc(self, doc): | ||||
"""Save email attachments in given document.""" | """Save email attachments in given document.""" | ||||
from frappe.utils.file_manager import save_file, MaxFileSizeReachedError | from frappe.utils.file_manager import save_file, MaxFileSizeReachedError | ||||
saved_attachments = [] | |||||
for attachment in self.attachments: | for attachment in self.attachments: | ||||
try: | try: | ||||
save_file(attachment['fname'], attachment['fcontent'], | |||||
file_data = save_file(attachment['fname'], attachment['fcontent'], | |||||
doc.doctype, doc.name) | doc.doctype, doc.name) | ||||
saved_attachments.append(file_data.file_name) | |||||
except MaxFileSizeReachedError: | except MaxFileSizeReachedError: | ||||
# WARNING: bypass max file size exception | # WARNING: bypass max file size exception | ||||
pass | pass | ||||
@@ -307,6 +310,8 @@ class Email: | |||||
# same file attached twice?? | # same file attached twice?? | ||||
pass | pass | ||||
return saved_attachments | |||||
def get_thread_id(self): | def get_thread_id(self): | ||||
"""Extract thread ID from `[]`""" | """Extract thread ID from `[]`""" | ||||
import re | import re | ||||
@@ -53,3 +53,4 @@ class EmptyTableError(ValidationError): pass | |||||
class LinkExistsError(ValidationError): pass | class LinkExistsError(ValidationError): pass | ||||
class InvalidEmailAddressError(ValidationError): pass | class InvalidEmailAddressError(ValidationError): pass | ||||
class TemplateNotFoundError(ValidationError): pass | class TemplateNotFoundError(ValidationError): pass | ||||
class UniqueValidationError(ValidationError): pass |
@@ -26,7 +26,7 @@ to ERPNext. | |||||
""" | """ | ||||
app_icon = "octicon octicon-circuit-board" | app_icon = "octicon octicon-circuit-board" | ||||
app_version = "5.2.2" | |||||
app_version = "5.3.0" | |||||
app_color = "orange" | app_color = "orange" | ||||
github_link = "https://github.com/frappe/frappe" | github_link = "https://github.com/frappe/frappe" | ||||
@@ -160,7 +160,9 @@ class BaseDocument(object): | |||||
value.parent = self.name | value.parent = self.name | ||||
value.parenttype = self.doctype | value.parenttype = self.doctype | ||||
value.parentfield = key | value.parentfield = key | ||||
value.docstatus = 0 | |||||
if value.docstatus is None: | |||||
value.docstatus = 0 | |||||
if not getattr(value, "idx", None): | if not getattr(value, "idx", None): | ||||
value.idx = len(self.get(key) or []) + 1 | value.idx = len(self.get(key) or []) + 1 | ||||
@@ -182,10 +184,9 @@ class BaseDocument(object): | |||||
elif df.fieldtype in ("Datetime", "Date") and d[fieldname]=="": | elif df.fieldtype in ("Datetime", "Date") and d[fieldname]=="": | ||||
d[fieldname] = None | d[fieldname] = None | ||||
if d[fieldname]=="": | |||||
df = self.meta.get_field(fieldname) | |||||
if df and df.fieldtype in ("Datetime", "Date"): | |||||
elif df.get("unique") and cstr(d[fieldname]).strip()=="": | |||||
# unique empty field should be set to None | |||||
d[fieldname] = None | d[fieldname] = None | ||||
return d | return d | ||||
@@ -262,15 +263,23 @@ class BaseDocument(object): | |||||
values = ", ".join(["%s"] * len(columns)) | values = ", ".join(["%s"] * len(columns)) | ||||
), d.values()) | ), d.values()) | ||||
except Exception, e: | except Exception, e: | ||||
if e.args[0]==1062 and "PRIMARY" in cstr(e.args[1]): | |||||
if self.meta.autoname=="hash": | |||||
# hash collision? try again | |||||
self.name = None | |||||
self.db_insert() | |||||
return | |||||
type, value, traceback = sys.exc_info() | |||||
frappe.msgprint(_("Duplicate name {0} {1}").format(self.doctype, self.name)) | |||||
raise frappe.DuplicateEntryError, (self.doctype, self.name, e), traceback | |||||
if e.args[0]==1062: | |||||
if "PRIMARY" in cstr(e.args[1]): | |||||
if self.meta.autoname=="hash": | |||||
# hash collision? try again | |||||
self.name = None | |||||
self.db_insert() | |||||
return | |||||
type, value, traceback = sys.exc_info() | |||||
frappe.msgprint(_("Duplicate name {0} {1}").format(self.doctype, self.name)) | |||||
raise frappe.DuplicateEntryError, (self.doctype, self.name, e), traceback | |||||
elif "Duplicate" in cstr(e.args[1]): | |||||
# unique constraint | |||||
self.show_unique_validation_message(e) | |||||
else: | |||||
raise | |||||
else: | else: | ||||
raise | raise | ||||
@@ -290,13 +299,17 @@ class BaseDocument(object): | |||||
values = ", ".join(["`"+c+"`=%s" for c in columns]) | values = ", ".join(["`"+c+"`=%s" for c in columns]) | ||||
), d.values() + [d.get("name")]) | ), d.values() + [d.get("name")]) | ||||
except Exception, e: | except Exception, e: | ||||
if e.args[0]==1062: | |||||
type, value, traceback = sys.exc_info() | |||||
fieldname = str(e).split("'")[-2] | |||||
frappe.msgprint(_("{0} must be unique".format(self.meta.get_label(fieldname)))) | |||||
raise frappe.ValidationError, (self.doctype, self.name, e), traceback | |||||
if e.args[0]==1062 and "Duplicate" in cstr(e.args[1]): | |||||
self.show_unique_validation_message(e) | |||||
else: | else: | ||||
raise | raise | ||||
def show_unique_validation_message(self, e): | |||||
type, value, traceback = sys.exc_info() | |||||
fieldname = str(e).split("'")[-2] | |||||
label = fieldname if fieldname.startswith("unique_") else self.meta.get_label(fieldname) | |||||
frappe.msgprint(_("{0} must be unique".format(label))) | |||||
raise frappe.UniqueValidationError, (self.doctype, self.name, e), traceback | |||||
def db_set(self, fieldname, value, update_modified=True): | def db_set(self, fieldname, value, update_modified=True): | ||||
self.set(fieldname, value) | self.set(fieldname, value) | ||||
@@ -44,17 +44,19 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None): | |||||
def false_if_not_shared(): | def false_if_not_shared(): | ||||
if ptype in ("read", "write", "share", "email", "print"): | if ptype in ("read", "write", "share", "email", "print"): | ||||
shared = frappe.share.get_shared(doctype, user, | |||||
["read" if ptype in ("email", "print") else ptype]) | |||||
if doc: | if doc: | ||||
doc_name = doc if isinstance(doc, basestring) else doc.name | doc_name = doc if isinstance(doc, basestring) else doc.name | ||||
shared = frappe.share.get_shared(doctype, user, | |||||
["read" if ptype in ("email", "print") else ptype]) | |||||
if doc_name in shared: | if doc_name in shared: | ||||
if verbose: print "Shared" | if verbose: print "Shared" | ||||
if ptype in ("read", "write", "share") or meta.permissions[0].get(ptype): | if ptype in ("read", "write", "share") or meta.permissions[0].get(ptype): | ||||
return True | return True | ||||
else: | |||||
elif shared: | |||||
# if atleast one shared doc of that type, then return True | |||||
# this is used in db_query to check if permission on DocType | |||||
if verbose: print "Has a shared document" | if verbose: print "Has a shared document" | ||||
return True | return True | ||||
@@ -35,7 +35,7 @@ | |||||
{%= __("About") %}</a></li> | {%= __("About") %}</a></li> | ||||
<li><a href="https://frappe.io" target="_blank" data-link="docs"> | <li><a href="https://frappe.io" target="_blank" data-link="docs"> | ||||
{%= __("Documentation") %}</a></li> | {%= __("Documentation") %}</a></li> | ||||
<li><a href="https://discuss.frappe.io" target="_blank"> | |||||
<li><a href="https://discuss.erpnext.com" target="_blank"> | |||||
{%= __("Forums") %}</a></li> | {%= __("Forums") %}</a></li> | ||||
<li><a href="https://github.com/frappe/erpnext/issues" target="_blank"> | <li><a href="https://github.com/frappe/erpnext/issues" target="_blank"> | ||||
{%= __("Report an Issue") %}</a></li> | {%= __("Report an Issue") %}</a></li> | ||||
@@ -25,7 +25,7 @@ | |||||
{%= __("About") %}</a></li> | {%= __("About") %}</a></li> | ||||
<li><a href="https://frappe.io" target="_blank" data-link="docs"> | <li><a href="https://frappe.io" target="_blank" data-link="docs"> | ||||
{%= __("Documentation") %}</a></li> | {%= __("Documentation") %}</a></li> | ||||
<li><a href="https://discuss.frappe.io" target="_blank"> | |||||
<li><a href="https://discuss.erpnext.com" target="_blank"> | |||||
{%= __("Forums") %}</a></li> | {%= __("Forums") %}</a></li> | ||||
<li><a href="https://github.com/frappe/erpnext/issues" target="_blank"> | <li><a href="https://github.com/frappe/erpnext/issues" target="_blank"> | ||||
{%= __("Report an Issue") %}</a></li> | {%= __("Report an Issue") %}</a></li> | ||||
@@ -1,9 +1,9 @@ | |||||
/*! | |||||
/*! | |||||
* jquery.event.drag - v 2.2 | * jquery.event.drag - v 2.2 | ||||
* Copyright (c) 2010 Three Dub Media - http://threedubmedia.com | * Copyright (c) 2010 Three Dub Media - http://threedubmedia.com | ||||
* Open Source MIT License - http://threedubmedia.com/code/license | * Open Source MIT License - http://threedubmedia.com/code/license | ||||
*/ | */ | ||||
// Created: 2008-06-04 | |||||
// Created: 2008-06-04 | |||||
// Updated: 2012-05-21 | // Updated: 2012-05-21 | ||||
// REQUIRES: jquery 1.7.x | // REQUIRES: jquery 1.7.x | ||||
@@ -16,7 +16,7 @@ $.fn.drag = function( str, arg, opts ){ | |||||
// figure out the event handler... | // figure out the event handler... | ||||
fn = $.isFunction( str ) ? str : $.isFunction( arg ) ? arg : null; | fn = $.isFunction( str ) ? str : $.isFunction( arg ) ? arg : null; | ||||
// fix the event type | // fix the event type | ||||
if ( type.indexOf("drag") !== 0 ) | |||||
if ( type.indexOf("drag") !== 0 ) | |||||
type = "drag"+ type; | type = "drag"+ type; | ||||
// were options passed | // were options passed | ||||
opts = ( str == fn ? arg : opts ) || {}; | opts = ( str == fn ? arg : opts ) || {}; | ||||
@@ -25,11 +25,11 @@ $.fn.drag = function( str, arg, opts ){ | |||||
}; | }; | ||||
// local refs (increase compression) | // local refs (increase compression) | ||||
var $event = $.event, | |||||
var $event = $.event, | |||||
$special = $event.special, | $special = $event.special, | ||||
// configure the drag special event | |||||
// configure the drag special event | |||||
drag = $special.drag = { | drag = $special.drag = { | ||||
// these are the default settings | // these are the default settings | ||||
defaults: { | defaults: { | ||||
which: 1, // mouse button pressed to start drag sequence | which: 1, // mouse button pressed to start drag sequence | ||||
@@ -40,82 +40,88 @@ drag = $special.drag = { | |||||
drop: true, // false to suppress drop events, true or selector to allow | drop: true, // false to suppress drop events, true or selector to allow | ||||
click: false // false to suppress click events after dragend (no proxy) | click: false // false to suppress click events after dragend (no proxy) | ||||
}, | }, | ||||
// the key name for stored drag data | // the key name for stored drag data | ||||
datakey: "dragdata", | datakey: "dragdata", | ||||
// prevent bubbling for better performance | // prevent bubbling for better performance | ||||
noBubble: true, | noBubble: true, | ||||
// count bound related events | // count bound related events | ||||
add: function( obj ){ | |||||
add: function( obj ){ | |||||
// read the interaction data | // read the interaction data | ||||
var data = $.data( this, drag.datakey ), | var data = $.data( this, drag.datakey ), | ||||
// read any passed options | |||||
// read any passed options | |||||
opts = obj.data || {}; | opts = obj.data || {}; | ||||
// count another realted event | // count another realted event | ||||
data.related += 1; | data.related += 1; | ||||
// extend data options bound with this event | // extend data options bound with this event | ||||
// don't iterate "opts" in case it is a node | |||||
// don't iterate "opts" in case it is a node | |||||
$.each( drag.defaults, function( key, def ){ | $.each( drag.defaults, function( key, def ){ | ||||
if ( opts[ key ] !== undefined ) | if ( opts[ key ] !== undefined ) | ||||
data[ key ] = opts[ key ]; | data[ key ] = opts[ key ]; | ||||
}); | }); | ||||
}, | }, | ||||
// forget unbound related events | // forget unbound related events | ||||
remove: function(){ | remove: function(){ | ||||
$.data( this, drag.datakey ).related -= 1; | $.data( this, drag.datakey ).related -= 1; | ||||
}, | }, | ||||
// configure interaction, capture settings | // configure interaction, capture settings | ||||
setup: function(){ | setup: function(){ | ||||
// check for related events | // check for related events | ||||
if ( $.data( this, drag.datakey ) ) | |||||
if ( $.data( this, drag.datakey ) ) | |||||
return; | return; | ||||
// initialize the drag data with copied defaults | // initialize the drag data with copied defaults | ||||
var data = $.extend({ related:0 }, drag.defaults ); | var data = $.extend({ related:0 }, drag.defaults ); | ||||
// store the interaction data | // store the interaction data | ||||
$.data( this, drag.datakey, data ); | $.data( this, drag.datakey, data ); | ||||
// bind the mousedown event, which starts drag interactions | // bind the mousedown event, which starts drag interactions | ||||
// don't attached drag event via special for fullcalendar | |||||
// return false to attach the normal way | |||||
if(this===document) return false; | |||||
$event.add( this, "touchstart mousedown", drag.init, data ); | $event.add( this, "touchstart mousedown", drag.init, data ); | ||||
// prevent image dragging in IE... | // prevent image dragging in IE... | ||||
if ( this.attachEvent ) | |||||
this.attachEvent("ondragstart", drag.dontstart ); | |||||
if ( this.attachEvent ) | |||||
this.attachEvent("ondragstart", drag.dontstart ); | |||||
}, | }, | ||||
// destroy configured interaction | // destroy configured interaction | ||||
teardown: function(){ | teardown: function(){ | ||||
var data = $.data( this, drag.datakey ) || {}; | var data = $.data( this, drag.datakey ) || {}; | ||||
// check for related events | // check for related events | ||||
if ( data.related ) | |||||
if ( data.related ) | |||||
return; | return; | ||||
// remove the stored data | // remove the stored data | ||||
$.removeData( this, drag.datakey ); | $.removeData( this, drag.datakey ); | ||||
// remove the mousedown event | // remove the mousedown event | ||||
$event.remove( this, "touchstart mousedown", drag.init ); | $event.remove( this, "touchstart mousedown", drag.init ); | ||||
// enable text selection | // enable text selection | ||||
drag.textselect( true ); | |||||
drag.textselect( true ); | |||||
// un-prevent image dragging in IE... | // un-prevent image dragging in IE... | ||||
if ( this.detachEvent ) | |||||
this.detachEvent("ondragstart", drag.dontstart ); | |||||
if ( this.detachEvent ) | |||||
this.detachEvent("ondragstart", drag.dontstart ); | |||||
}, | }, | ||||
// initialize the interaction | // initialize the interaction | ||||
init: function( event ){ | |||||
init: function( event ){ | |||||
// sorry, only one touch at a time | // sorry, only one touch at a time | ||||
if ( drag.touched ) | |||||
if ( drag.touched ) | |||||
return; | return; | ||||
// the drag/drop interaction data | // the drag/drop interaction data | ||||
var dd = event.data, results; | var dd = event.data, results; | ||||
// check the which directive | // check the which directive | ||||
if ( event.which != 0 && dd.which > 0 && event.which != dd.which ) | |||||
return; | |||||
if ( event.which != 0 && dd.which > 0 && event.which != dd.which ) | |||||
return; | |||||
// check for suppressed selector | // check for suppressed selector | ||||
if ( $( event.target ).is( dd.not ) ) | |||||
if ( $( event.target ).is( dd.not ) ) | |||||
return; | return; | ||||
// check for handle selector | // check for handle selector | ||||
if ( dd.handle && !$( event.target ).closest( dd.handle, event.currentTarget ).length ) | |||||
if ( dd.handle && !$( event.target ).closest( dd.handle, event.currentTarget ).length ) | |||||
return; | return; | ||||
drag.touched = event.type == 'touchstart' ? this : null; | drag.touched = event.type == 'touchstart' ? this : null; | ||||
@@ -126,7 +132,7 @@ drag = $special.drag = { | |||||
dd.pageX = event.pageX; | dd.pageX = event.pageX; | ||||
dd.pageY = event.pageY; | dd.pageY = event.pageY; | ||||
dd.dragging = null; | dd.dragging = null; | ||||
// handle draginit event... | |||||
// handle draginit event... | |||||
results = drag.hijack( event, "draginit", dd ); | results = drag.hijack( event, "draginit", dd ); | ||||
// early cancel | // early cancel | ||||
if ( !dd.propagates ) | if ( !dd.propagates ) | ||||
@@ -143,43 +149,43 @@ drag = $special.drag = { | |||||
// remember how many interactions are propagating | // remember how many interactions are propagating | ||||
dd.propagates = dd.interactions.length; | dd.propagates = dd.interactions.length; | ||||
// locate and init the drop targets | // locate and init the drop targets | ||||
if ( dd.drop !== false && $special.drop ) | |||||
if ( dd.drop !== false && $special.drop ) | |||||
$special.drop.handler( event, dd ); | $special.drop.handler( event, dd ); | ||||
// disable text selection | // disable text selection | ||||
drag.textselect( false ); | |||||
drag.textselect( false ); | |||||
// bind additional events... | // bind additional events... | ||||
if ( drag.touched ) | if ( drag.touched ) | ||||
$event.add( drag.touched, "touchmove touchend", drag.handler, dd ); | $event.add( drag.touched, "touchmove touchend", drag.handler, dd ); | ||||
else | |||||
else | |||||
$event.add( document, "mousemove mouseup", drag.handler, dd ); | $event.add( document, "mousemove mouseup", drag.handler, dd ); | ||||
// helps prevent text selection or scrolling | // helps prevent text selection or scrolling | ||||
if ( !drag.touched || dd.live ) | if ( !drag.touched || dd.live ) | ||||
return false; | return false; | ||||
}, | |||||
}, | |||||
// returns an interaction object | // returns an interaction object | ||||
interaction: function( elem, dd ){ | interaction: function( elem, dd ){ | ||||
var offset = $( elem )[ dd.relative ? "position" : "offset" ]() || { top:0, left:0 }; | var offset = $( elem )[ dd.relative ? "position" : "offset" ]() || { top:0, left:0 }; | ||||
return { | return { | ||||
drag: elem, | |||||
callback: new drag.callback(), | |||||
drag: elem, | |||||
callback: new drag.callback(), | |||||
droppable: [], | droppable: [], | ||||
offset: offset | offset: offset | ||||
}; | }; | ||||
}, | }, | ||||
// handle drag-releatd DOM events | // handle drag-releatd DOM events | ||||
handler: function( event ){ | |||||
handler: function( event ){ | |||||
// read the data before hijacking anything | // read the data before hijacking anything | ||||
var dd = event.data; | |||||
var dd = event.data; | |||||
// handle various events | // handle various events | ||||
switch ( event.type ){ | switch ( event.type ){ | ||||
// mousemove, check distance, start dragging | // mousemove, check distance, start dragging | ||||
case !dd.dragging && 'touchmove': | |||||
case !dd.dragging && 'touchmove': | |||||
event.preventDefault(); | event.preventDefault(); | ||||
case !dd.dragging && 'mousemove': | case !dd.dragging && 'mousemove': | ||||
// drag tolerance, x≤ + y≤ = distance≤ | |||||
if ( Math.pow( event.pageX-dd.pageX, 2 ) + Math.pow( event.pageY-dd.pageY, 2 ) < Math.pow( dd.distance, 2 ) ) | |||||
// drag tolerance, x² + y² = distance² | |||||
if ( Math.pow( event.pageX-dd.pageX, 2 ) + Math.pow( event.pageY-dd.pageY, 2 ) < Math.pow( dd.distance, 2 ) ) | |||||
break; // distance tolerance not reached | break; // distance tolerance not reached | ||||
event.target = dd.target; // force target from "mousedown" event (fix distance issue) | event.target = dd.target; // force target from "mousedown" event (fix distance issue) | ||||
drag.hijack( event, "dragstart", dd ); // trigger "dragstart" | drag.hijack( event, "dragstart", dd ); // trigger "dragstart" | ||||
@@ -190,42 +196,42 @@ drag = $special.drag = { | |||||
event.preventDefault(); | event.preventDefault(); | ||||
case 'mousemove': | case 'mousemove': | ||||
if ( dd.dragging ){ | if ( dd.dragging ){ | ||||
// trigger "drag" | |||||
// trigger "drag" | |||||
drag.hijack( event, "drag", dd ); | drag.hijack( event, "drag", dd ); | ||||
if ( dd.propagates ){ | if ( dd.propagates ){ | ||||
// manage drop events | // manage drop events | ||||
if ( dd.drop !== false && $special.drop ) | if ( dd.drop !== false && $special.drop ) | ||||
$special.drop.handler( event, dd ); // "dropstart", "dropend" | |||||
break; // "drag" not rejected, stop | |||||
$special.drop.handler( event, dd ); // "dropstart", "dropend" | |||||
break; // "drag" not rejected, stop | |||||
} | } | ||||
event.type = "mouseup"; // helps "drop" handler behave | event.type = "mouseup"; // helps "drop" handler behave | ||||
} | } | ||||
// mouseup, stop dragging | // mouseup, stop dragging | ||||
case 'touchend': | |||||
case 'mouseup': | |||||
case 'touchend': | |||||
case 'mouseup': | |||||
default: | default: | ||||
if ( drag.touched ) | if ( drag.touched ) | ||||
$event.remove( drag.touched, "touchmove touchend", drag.handler ); // remove touch events | $event.remove( drag.touched, "touchmove touchend", drag.handler ); // remove touch events | ||||
else | |||||
$event.remove( document, "mousemove mouseup", drag.handler ); // remove page events | |||||
else | |||||
$event.remove( document, "mousemove mouseup", drag.handler ); // remove page events | |||||
if ( dd.dragging ){ | if ( dd.dragging ){ | ||||
if ( dd.drop !== false && $special.drop ) | if ( dd.drop !== false && $special.drop ) | ||||
$special.drop.handler( event, dd ); // "drop" | $special.drop.handler( event, dd ); // "drop" | ||||
drag.hijack( event, "dragend", dd ); // trigger "dragend" | |||||
drag.hijack( event, "dragend", dd ); // trigger "dragend" | |||||
} | } | ||||
drag.textselect( true ); // enable text selection | drag.textselect( true ); // enable text selection | ||||
// if suppressing click events... | // if suppressing click events... | ||||
if ( dd.click === false && dd.dragging ) | if ( dd.click === false && dd.dragging ) | ||||
$.data( dd.mousedown, "suppress.click", new Date().getTime() + 5 ); | $.data( dd.mousedown, "suppress.click", new Date().getTime() + 5 ); | ||||
dd.dragging = drag.touched = false; // deactivate element | |||||
dd.dragging = drag.touched = false; // deactivate element | |||||
break; | break; | ||||
} | } | ||||
}, | }, | ||||
// re-use event object for custom events | // re-use event object for custom events | ||||
hijack: function( event, type, dd, x, elem ){ | hijack: function( event, type, dd, x, elem ){ | ||||
// not configured | // not configured | ||||
if ( !dd ) | |||||
if ( !dd ) | |||||
return; | return; | ||||
// remember the original event and type | // remember the original event and type | ||||
var orig = { event:event.originalEvent, type:event.type }, | var orig = { event:event.originalEvent, type:event.type }, | ||||
@@ -255,7 +261,7 @@ drag = $special.drag = { | |||||
callback.target = subject; | callback.target = subject; | ||||
// force propagtion of the custom event | // force propagtion of the custom event | ||||
event.isPropagationStopped = function(){ return false; }; | event.isPropagationStopped = function(){ return false; }; | ||||
// handle the event | |||||
// handle the event | |||||
result = subject ? $event.dispatch.call( subject, event, callback ) : null; | result = subject ? $event.dispatch.call( subject, event, callback ) : null; | ||||
// stop the drag interaction for this element | // stop the drag interaction for this element | ||||
if ( result === false ){ | if ( result === false ){ | ||||
@@ -270,25 +276,25 @@ drag = $special.drag = { | |||||
// assign any dropinit elements | // assign any dropinit elements | ||||
else if ( type == "dropinit" ) | else if ( type == "dropinit" ) | ||||
ia.droppable.push( drag.element( result ) || subject ); | ia.droppable.push( drag.element( result ) || subject ); | ||||
// accept a returned proxy element | |||||
// accept a returned proxy element | |||||
if ( type == "dragstart" ) | if ( type == "dragstart" ) | ||||
ia.proxy = $( drag.element( result ) || ia.drag )[0]; | ia.proxy = $( drag.element( result ) || ia.drag )[0]; | ||||
// remember this result | |||||
// remember this result | |||||
ia.results.push( result ); | ia.results.push( result ); | ||||
// forget the event result, for recycling | // forget the event result, for recycling | ||||
delete event.result; | delete event.result; | ||||
// break on cancelled handler | // break on cancelled handler | ||||
if ( type !== "dropinit" ) | if ( type !== "dropinit" ) | ||||
return result; | return result; | ||||
}); | |||||
// flatten the results | |||||
dd.results[ i ] = drag.flatten( ia.results ); | |||||
}); | |||||
// flatten the results | |||||
dd.results[ i ] = drag.flatten( ia.results ); | |||||
// accept a set of valid drop targets | // accept a set of valid drop targets | ||||
if ( type == "dropinit" ) | if ( type == "dropinit" ) | ||||
ia.droppable = drag.flatten( ia.droppable ); | ia.droppable = drag.flatten( ia.droppable ); | ||||
// locate drop targets | // locate drop targets | ||||
if ( type == "dragstart" && !ia.cancelled ) | if ( type == "dragstart" && !ia.cancelled ) | ||||
callback.update(); | |||||
callback.update(); | |||||
} | } | ||||
while ( ++i < len ) | while ( ++i < len ) | ||||
// restore the original event & type | // restore the original event & type | ||||
@@ -297,9 +303,9 @@ drag = $special.drag = { | |||||
// return all handler results | // return all handler results | ||||
return drag.flatten( dd.results ); | return drag.flatten( dd.results ); | ||||
}, | }, | ||||
// extend the callback object with drag/drop properties... | // extend the callback object with drag/drop properties... | ||||
properties: function( event, dd, ia ){ | |||||
properties: function( event, dd, ia ){ | |||||
var obj = ia.callback; | var obj = ia.callback; | ||||
// elements | // elements | ||||
obj.drag = ia.drag; | obj.drag = ia.drag; | ||||
@@ -314,44 +320,44 @@ drag = $special.drag = { | |||||
obj.originalX = ia.offset.left; | obj.originalX = ia.offset.left; | ||||
obj.originalY = ia.offset.top; | obj.originalY = ia.offset.top; | ||||
// adjusted element position | // adjusted element position | ||||
obj.offsetX = obj.originalX + obj.deltaX; | |||||
obj.offsetX = obj.originalX + obj.deltaX; | |||||
obj.offsetY = obj.originalY + obj.deltaY; | obj.offsetY = obj.originalY + obj.deltaY; | ||||
// assign the drop targets information | // assign the drop targets information | ||||
obj.drop = drag.flatten( ( ia.drop || [] ).slice() ); | obj.drop = drag.flatten( ( ia.drop || [] ).slice() ); | ||||
obj.available = drag.flatten( ( ia.droppable || [] ).slice() ); | obj.available = drag.flatten( ( ia.droppable || [] ).slice() ); | ||||
return obj; | |||||
return obj; | |||||
}, | }, | ||||
// determine is the argument is an element or jquery instance | // determine is the argument is an element or jquery instance | ||||
element: function( arg ){ | element: function( arg ){ | ||||
if ( arg && ( arg.jquery || arg.nodeType == 1 ) ) | if ( arg && ( arg.jquery || arg.nodeType == 1 ) ) | ||||
return arg; | return arg; | ||||
}, | }, | ||||
// flatten nested jquery objects and arrays into a single dimension array | // flatten nested jquery objects and arrays into a single dimension array | ||||
flatten: function( arr ){ | flatten: function( arr ){ | ||||
return $.map( arr, function( member ){ | return $.map( arr, function( member ){ | ||||
return member && member.jquery ? $.makeArray( member ) : | |||||
return member && member.jquery ? $.makeArray( member ) : | |||||
member && member.length ? drag.flatten( member ) : member; | member && member.length ? drag.flatten( member ) : member; | ||||
}); | }); | ||||
}, | }, | ||||
// toggles text selection attributes ON (true) or OFF (false) | // toggles text selection attributes ON (true) or OFF (false) | ||||
textselect: function( bool ){ | |||||
textselect: function( bool ){ | |||||
$( document )[ bool ? "unbind" : "bind" ]("selectstart", drag.dontstart ) | $( document )[ bool ? "unbind" : "bind" ]("selectstart", drag.dontstart ) | ||||
.css("MozUserSelect", bool ? "" : "none" ); | .css("MozUserSelect", bool ? "" : "none" ); | ||||
// .attr("unselectable", bool ? "off" : "on" ) | // .attr("unselectable", bool ? "off" : "on" ) | ||||
document.unselectable = bool ? "off" : "on"; | |||||
document.unselectable = bool ? "off" : "on"; | |||||
}, | }, | ||||
// suppress "selectstart" and "ondragstart" events | // suppress "selectstart" and "ondragstart" events | ||||
dontstart: function(){ | |||||
return false; | |||||
dontstart: function(){ | |||||
return false; | |||||
}, | }, | ||||
// a callback instance contructor | // a callback instance contructor | ||||
callback: function(){} | callback: function(){} | ||||
}; | }; | ||||
// callback methods | // callback methods | ||||
@@ -375,9 +381,9 @@ $event.dispatch = function( event ){ | |||||
}; | }; | ||||
// event fix hooks for touch events... | // event fix hooks for touch events... | ||||
var touchHooks = | |||||
$event.fixHooks.touchstart = | |||||
$event.fixHooks.touchmove = | |||||
var touchHooks = | |||||
$event.fixHooks.touchstart = | |||||
$event.fixHooks.touchmove = | |||||
$event.fixHooks.touchend = | $event.fixHooks.touchend = | ||||
$event.fixHooks.touchcancel = { | $event.fixHooks.touchcancel = { | ||||
props: "clientX clientY pageX pageY screenX screenY".split( " " ), | props: "clientX clientY pageX pageY screenX screenY".split( " " ), | ||||
@@ -385,9 +391,9 @@ $event.fixHooks.touchcancel = { | |||||
if ( orig ){ | if ( orig ){ | ||||
var touched = ( orig.touches && orig.touches[0] ) | var touched = ( orig.touches && orig.touches[0] ) | ||||
|| ( orig.changedTouches && orig.changedTouches[0] ) | || ( orig.changedTouches && orig.changedTouches[0] ) | ||||
|| null; | |||||
|| null; | |||||
// iOS webkit: touchstart, touchmove, touchend | // iOS webkit: touchstart, touchmove, touchend | ||||
if ( touched ) | |||||
if ( touched ) | |||||
$.each( touchHooks.props, function( i, prop ){ | $.each( touchHooks.props, function( i, prop ){ | ||||
event[ prop ] = touched[ prop ]; | event[ prop ] = touched[ prop ]; | ||||
}); | }); | ||||
@@ -399,4 +405,4 @@ $event.fixHooks.touchcancel = { | |||||
// share the same special event configuration with related events... | // share the same special event configuration with related events... | ||||
$special.draginit = $special.dragstart = $special.dragend = drag; | $special.draginit = $special.dragstart = $special.dragend = drag; | ||||
})( jQuery ); | |||||
})( jQuery ); |
@@ -3,6 +3,7 @@ | |||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
import frappe | import frappe | ||||
from frappe import _ | |||||
from frappe.utils import cint | from frappe.utils import cint | ||||
@frappe.whitelist() | @frappe.whitelist() | ||||
@@ -11,6 +12,8 @@ def add(doctype, name, user=None, read=1, write=0, share=0, everyone=0, flags=No | |||||
if not user: | if not user: | ||||
user = frappe.session.user | user = frappe.session.user | ||||
check_share_permission(doctype, name) | |||||
share_name = get_share_name(doctype, name, user, everyone) | share_name = get_share_name(doctype, name, user, everyone) | ||||
if share_name: | if share_name: | ||||
@@ -48,6 +51,8 @@ def remove(doctype, name, user, flags=None): | |||||
@frappe.whitelist() | @frappe.whitelist() | ||||
def set_permission(doctype, name, user, permission_to, value=1, everyone=0): | def set_permission(doctype, name, user, permission_to, value=1, everyone=0): | ||||
"""Set share permission.""" | """Set share permission.""" | ||||
check_share_permission(doctype, name) | |||||
share_name = get_share_name(doctype, name, user, everyone) | share_name = get_share_name(doctype, name, user, everyone) | ||||
value = int(value) | value = int(value) | ||||
@@ -124,3 +129,7 @@ def get_share_name(doctype, name, user, everyone): | |||||
return share_name | return share_name | ||||
def check_share_permission(doctype, name): | |||||
"""Check if the user can share with other users""" | |||||
if not frappe.has_permission(doctype, ptype="share", doc=name): | |||||
frappe.throw(_("No permission to {0} {1} {2}".format("share", doctype, name)), frappe.PermissionError) |
@@ -7,6 +7,8 @@ from frappe.utils.scheduler import enqueue_events | |||||
from frappe.celery_app import get_celery, celery_task, task_logger, LONGJOBS_PREFIX | from frappe.celery_app import get_celery, celery_task, task_logger, LONGJOBS_PREFIX | ||||
from frappe.utils import get_sites | from frappe.utils import get_sites | ||||
from frappe.utils.file_lock import create_lock, delete_lock | from frappe.utils.file_lock import create_lock, delete_lock | ||||
import time | |||||
import MySQLdb | |||||
@celery_task() | @celery_task() | ||||
def sync_queues(): | def sync_queues(): | ||||
@@ -122,3 +124,33 @@ def pull_from_email_account(site, email_account): | |||||
frappe.db.commit() | frappe.db.commit() | ||||
finally: | finally: | ||||
frappe.destroy() | frappe.destroy() | ||||
@celery_task() | |||||
def sendmail(site, communication_name, print_html=None, print_format=None, attachments=None, recipients=None, except_recipient=False): | |||||
try: | |||||
frappe.connect(site=site) | |||||
# upto 3 retries | |||||
for i in xrange(3): | |||||
try: | |||||
communication = frappe.get_doc("Communication", communication_name) | |||||
communication._notify(print_html=print_html, print_format=print_format, attachments=attachments, recipients=recipients, except_recipient=except_recipient) | |||||
except MySQLdb.OperationalError, e: | |||||
# deadlock, try again | |||||
if e.args[0]==1213: | |||||
frappe.db.rollback() | |||||
time.sleep(1) | |||||
continue | |||||
else: | |||||
raise | |||||
else: | |||||
break | |||||
except: | |||||
frappe.db.rollback() | |||||
else: | |||||
frappe.db.commit() | |||||
finally: | |||||
frappe.destroy() |
@@ -1,6 +1,6 @@ | |||||
from setuptools import setup, find_packages | from setuptools import setup, find_packages | ||||
version = "5.2.2" | |||||
version = "5.3.0" | |||||
with open("requirements.txt", "r") as f: | with open("requirements.txt", "r") as f: | ||||
install_requires = f.readlines() | install_requires = f.readlines() | ||||