Merge branch 'develop' into offline-erpnext

This commit is contained in:
Suraj Shetty 2020-01-24 09:23:15 +05:30 committed by GitHub
commit a25f5d207a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 596 additions and 198 deletions

View file

@ -107,5 +107,6 @@ install:
- bench build --app frappe
after_script:
- pip install coverage==4.5.4
- pip install python-coveralls
- coveralls -b apps/frappe -d ../../sites/.coverage

View file

@ -19,11 +19,13 @@ context('Report View', () => {
cy.server();
cy.route('POST', 'api/method/frappe.client.set_value').as('value-update');
cy.visit(`/desk#List/${doctype_name}/Report`);
let cell = cy.get('.dt-row-0 > .dt-cell--col-3');
// check status column added from docstatus
cy.get('.dt-row-0 > .dt-cell--col-3').should('contain', 'Submitted');
let cell = cy.get('.dt-row-0 > .dt-cell--col-4');
// select the cell
cell.dblclick();
cell.find('input[data-fieldname="enabled"]').check({force: true});
cy.get('.dt-row-0 > .dt-cell--col-4').click();
cy.get('.dt-row-0 > .dt-cell--col-5').click();
cy.wait('@value-update');
cy.get('@doc').then(doc => {
cy.call('frappe.client.get_value', {

View file

@ -1537,7 +1537,7 @@ def logger(module=None, with_more_info=True):
from frappe.utils.logger import get_logger
return get_logger(module or 'default', with_more_info=with_more_info)
def log_error(message=None, title=None):
def log_error(message=None, title=_("Error")):
'''Log error to Error Log'''
# AI ALERT:
@ -1546,9 +1546,8 @@ def log_error(message=None, title=None):
# this hack tries to be smart about whats a title (single line ;-)) and fixes it
if message:
if '\n' not in message:
title = message
error = get_traceback()
if '\n' in title:
error, title = title, message
else:
error = message
else:

View file

@ -116,7 +116,7 @@ def clear_doctype_map(doctype, name):
cache_key = frappe.scrub(doctype) + '_map'
frappe.cache().hdel(cache_key, name)
def build_table_count_cache():
def build_table_count_cache(*args, **kwargs):
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import:
return
_cache = frappe.cache()
@ -137,7 +137,7 @@ def build_table_count_cache():
return counts
def build_domain_restriced_doctype_cache():
def build_domain_restriced_doctype_cache(*args, **kwargs):
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import:
return
_cache = frappe.cache()
@ -148,7 +148,7 @@ def build_domain_restriced_doctype_cache():
return doctypes
def build_domain_restriced_page_cache():
def build_domain_restriced_page_cache(*args, **kwargs):
if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_import:
return
_cache = frappe.cache()

View file

@ -26,7 +26,7 @@ class TestDocType(unittest.TestCase):
}],
"permissions": [{
"role": "System Manager",
"read": 1
"read": 1,
}],
"name": name
})
@ -295,3 +295,58 @@ class TestDocType(unittest.TestCase):
field_1.search_index = 1
self.assertRaises(CannotIndexedError, doc.insert)
def test_cancel_link_doctype(self):
import json
from frappe.desk.form.linked_with import get_submitted_linked_docs, cancel_all_linked_docs
#create doctype
link_doc = self.new_doctype('Test Linked Doctype')
link_doc.is_submittable = 1
for data in link_doc.get('permissions'):
data.submit = 1
data.cancel = 1
link_doc.insert()
doc = self.new_doctype('Test Doctype')
doc.is_submittable = 1
field_2 = doc.append('fields', {})
field_2.label = 'Test Linked Doctype'
field_2.fieldname = 'test_linked_doctype'
field_2.fieldtype = 'Link'
field_2.options = 'Test Linked Doctype'
for data in link_doc.get('permissions'):
data.submit = 1
data.cancel = 1
doc.insert()
# create doctype data
data_link_doc = frappe.new_doc('Test Linked Doctype')
data_link_doc.some_fieldname = 'Data1'
data_link_doc.insert()
data_link_doc.save()
data_link_doc.submit()
data_doc = frappe.new_doc('Test Doctype')
data_doc.some_fieldname = 'Data1'
data_doc.test_linked_doctype = data_link_doc.name
data_doc.insert()
data_doc.save()
data_doc.submit()
docs = get_submitted_linked_docs(link_doc.name, data_link_doc.name)
dump_docs = json.dumps(docs.get('docs'))
cancel_all_linked_docs(dump_docs)
data_link_doc.cancel()
data_doc.load_from_db()
self.assertEqual(data_link_doc.docstatus, 2)
self.assertEqual(data_doc.docstatus, 2)
# delete doctype record
data_doc.delete()
data_link_doc.delete()
# delete doctype
link_doc.delete()
doc.delete()
frappe.db.commit()

View file

@ -126,13 +126,15 @@ class Report(Document):
safe_exec(self.report_script, None, loc)
return loc['data']
def get_data(self, filters=None, limit=None, user=None, as_dict=False):
def get_data(self, filters=None, limit=None, user=None, as_dict=False, ignore_prepared_report=False):
columns = []
out = []
if self.report_type in ('Query Report', 'Script Report', 'Custom Report'):
# query and script reports
data = frappe.desk.query_report.run(self.name, filters=filters, user=user)
data = frappe.desk.query_report.run(self.name,
filters=filters, user=user, ignore_prepared_report=ignore_prepared_report)
for d in data.get('columns'):
if isinstance(d, dict):
col = frappe._dict(d)

View file

@ -168,7 +168,7 @@ frappe.ui.form.on('User', {
email: frm.doc.email
},
callback: function(r) {
if (r.message == undefined) {
if (!Array.isArray(r.message)) {
frappe.route_options = {
"email_id": frm.doc.email,
"awaiting_password": 1,

View file

@ -11,7 +11,7 @@
<tbody>
{% for j in jobs %}
<tr>
<td><span class="indicator {{ j.color }}" title="{{ j.get_status() }}">{{ j.queue.split(".").slice(-1)[0] }}</span></td>
<td><span class="indicator {{ j.color }}" title="{{ j.status }}">{{ j.queue.split(".").slice(-1)[0] }}</span></td>
<td style="overflow: auto;">
<div>
{{ frappe.utils.encode_tags(j.job_name) }}

View file

@ -17,6 +17,14 @@ frappe.ui.form.on("Customize Form", {
};
});
frm.set_query("default_print_format", function() {
return {
filters: {
'print_format_type': ['!=', 'JS']
}
}
});
$(frm.wrapper).on("grid-row-render", function(e, grid_row) {
if(grid_row.doc && grid_row.doc.fieldtype=="Section Break") {
$(grid_row.row).css({"font-weight": "bold"});

View file

@ -114,9 +114,8 @@ frappe.ui.form.on('Dashboard Chart', {
} else {
// standard filters
if (frm.doc.document_type) {
// allow all link and select fields as filters
frm.chart_filters = [];
frappe.model.with_doctype(frm.doc.document_type, () => {
frm.chart_filters = [];
frappe.get_meta(frm.doc.document_type).fields.map(df => {
if (['Link', 'Select'].includes(df.fieldtype)) {
let _df = copy_dict(df);
@ -131,8 +130,8 @@ frappe.ui.form.on('Dashboard Chart', {
frm.chart_filters.push(_df);
}
frm.trigger('render_filters_table');
});
frm.trigger('render_filters_table');
});
}
}
@ -158,7 +157,7 @@ frappe.ui.form.on('Dashboard Chart', {
let filters = JSON.parse(frm.doc.filters_json || '{}');
var filters_set = false;
fields.map( f => {
fields.map(f => {
if (filters[f.fieldname]) {
const filter_row = $(`<tr><td>${f.label}</td><td>${filters[f.fieldname] || ""}</td></tr>`);
table.find('tbody').append(filter_row);

View file

@ -73,6 +73,8 @@ class Event(Document):
communication.subject = self.subject
communication.content = self.description if self.description else self.subject
communication.communication_date = self.starts_on
communication.sender = self.owner
communication.sender_full_name = frappe.utils.get_fullname(self.owner)
communication.reference_doctype = self.doctype
communication.reference_name = self.name
communication.communication_medium = communication_mapping.get(self.event_category) if self.event_category else ""

View file

@ -1,14 +1,119 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe, json
import json
from collections import defaultdict
from six import string_types
import frappe
import frappe.desk.form.load
import frappe.desk.form.meta
from frappe import _
from frappe.model.meta import is_single
from frappe.modules import load_doctype_module
import frappe.desk.form.meta
import frappe.desk.form.load
from six import string_types
from collections import defaultdict
@frappe.whitelist()
def get_submitted_linked_docs(doctype, name, docs=None):
"""
Get all nested submitted linked doctype linkinfo
Arguments:
doctype (str) - The doctype for which get all linked doctypes
name (str) - The docname for which get all linked doctypes
Keyword Arguments:
docs (list of dict) - (Optional) Get list of dictionary for linked doctype.
Returns:
dict - Return list of documents and link count
"""
if not docs:
docs = []
linkinfo = get_linked_doctypes(doctype)
linked_docs = get_linked_docs(doctype, name, linkinfo)
link_count = 0
for link_doctype, link_names in linked_docs.items():
for link in link_names:
docinfo = link.update({"doctype": link_doctype})
validated_doc = validate_linked_doc(docinfo)
if not validated_doc:
continue
link_count += 1
if link.name in [doc.get("name") for doc in docs]:
continue
links = get_submitted_linked_docs(link_doctype, link.name, docs)
docs.append({
"doctype": link_doctype,
"name": link.name,
"docstatus": link.docstatus,
"link_count": links.get("count")
})
# sort linked documents by ascending number of links
docs.sort(key=lambda doc: doc.get("link_count"))
return {
"docs": docs,
"count": link_count
}
@frappe.whitelist()
def cancel_all_linked_docs(docs):
"""
Cancel all linked doctype
Arguments:
docs (str) - It contains all list of dictionaries of a linked documents.
"""
docs = json.loads(docs)
for i, doc in enumerate(docs, 1):
if validate_linked_doc(doc) is True:
frappe.publish_progress(percent=i * 100 / len(docs), title=_("Cancelling documents"))
linked_doc = frappe.get_doc(doc.get("doctype"), doc.get("name"))
linked_doc.cancel()
def validate_linked_doc(docinfo):
"""
Validate a document to be submitted and non-exempted from auto-cancel.
Args:
docs (dict): The document to check for submitted and non-exempt from auto-cancel
Returns:
bool: True if linked document passes all validations, else False
"""
# skip non-submittable doctypes since they don't need to be cancelled
if not frappe.get_meta(docinfo.get('doctype')).is_submittable:
return False
# skip draft or cancelled documents
if docinfo.get('docstatus') != 1:
return False
# skip other doctypes since they don't need to be cancelled
auto_cancel_exempt_doctypes = get_exempted_doctypes()
if docinfo.get('doctype') in auto_cancel_exempt_doctypes:
return False
return True
def get_exempted_doctypes():
""" Get list of doctypes exempted from being auto-cancelled """
auto_cancel_exempt_doctypes = []
for doctypes in frappe.get_hooks('auto_cancel_exempted_doctypes'):
auto_cancel_exempt_doctypes.append(doctypes)
return auto_cancel_exempt_doctypes
@frappe.whitelist()
@ -184,8 +289,8 @@ def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=F
if is_single(df.doctype): continue
# optimized to get both link exists and parenttype
possible_link = frappe.db.sql("""select distinct `{doctype_fieldname}`, parenttype
from `tab{doctype}` where `{doctype_fieldname}`=%s""".format(**df), doctype, as_dict=True)
possible_link = frappe.get_all(df.doctype, filters={df.doctype_fieldname: doctype},
fields=['parenttype'], distinct=True)
if not possible_link: continue
@ -203,4 +308,4 @@ def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=F
"doctype_fieldname": df.doctype_fieldname
}
return ret
return ret

View file

@ -11,6 +11,7 @@ from frappe.model.utils.user_settings import get_user_settings
from frappe.permissions import get_doc_permissions
from frappe.desk.form.document_follow import is_document_followed
from frappe import _
from six.moves.urllib.parse import quote
@frappe.whitelist()
def getdoc(doctype, name, user=None):
@ -101,7 +102,8 @@ def get_docinfo(doc=None, doctype=None, name=None):
"energy_point_logs": get_point_logs(doc.doctype, doc.name),
"milestones": get_milestones(doc.doctype, doc.name),
"is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user),
"tags": get_tags(doc.doctype, doc.name)
"tags": get_tags(doc.doctype, doc.name),
"document_email": get_document_email(doc.doctype, doc.name)
}
def get_milestones(doctype, name):
@ -263,4 +265,15 @@ def get_tags(doctype, name):
"document_name": name
}, fields=["tag"])]
return ",".join([tag for tag in tags])
return ",".join(tags)
def get_document_email(doctype, name):
email = get_automatic_email_link()
if not email:
return None
email = email.split("@")
return "{0}+{1}+{2}@{3}".format(email[0], quote(doctype), quote(name), email[1])
def get_automatic_email_link():
return frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id")

View file

@ -13,7 +13,7 @@ class Leaderboard {
constructor(parent) {
frappe.ui.make_app_page({
parent: parent,
title: "Leaderboard",
title: __("Leaderboard"),
single_column: false
});
this.parent = parent;
@ -187,7 +187,7 @@ class Leaderboard {
render_search_box() {
this.$search_box =
$(`<div class="leaderboard-search col-md-3">
$(`<div class="leaderboard-search form-group col-md-3">
<input type="text" placeholder="Search" class="form-control leaderboard-search-input input-sm">
</div>`);
@ -363,7 +363,7 @@ class Leaderboard {
const link = `#Form/${this.options.selected_doctype}/${item.name}`;
const name_html = item.formatted_name ?
`<span class="text-muted ellipsis">${item.formatted_name}</span>`
`<span class="text-muted ellipsis list-id">${item.formatted_name}</span>`
: `<a class="grey list-id ellipsis" href="${link}"> ${item.name} </a>`;
const html =
`<div class="list-item">

View file

@ -158,7 +158,7 @@ def get_script(report_name):
@frappe.whitelist()
@frappe.read_only()
def run(report_name, filters=None, user=None):
def run(report_name, filters=None, user=None, ignore_prepared_report=False):
report = get_report_doc(report_name)
if not user:
@ -169,7 +169,7 @@ def run(report_name, filters=None, user=None):
result = None
if report.prepared_report and not report.disable_prepared_report:
if report.prepared_report and not report.disable_prepared_report and not ignore_prepared_report:
if filters:
if isinstance(filters, string_types):
filters = json.loads(filters)
@ -229,7 +229,7 @@ def get_prepared_report_result(report, filters, dn="", user=None):
"status": "Completed",
"filters": json.dumps(filters),
"owner": user,
"report_name": report.custom_report or report.report_name
"report_name": report.get('custom_report') or report.get('report_name')
},
order_by = 'creation desc'
)

View file

@ -66,7 +66,7 @@ class AutoEmailReport(Document):
self.prepare_dynamic_filters()
columns, data = report.get_data(limit=self.no_of_rows or 100, user = self.user,
filters = self.filters, as_dict=True)
filters = self.filters, as_dict=True, ignore_prepared_report=True)
# add serial numbers
columns.insert(0, frappe._dict(fieldname='idx', label='', width='30px'))

View file

@ -765,7 +765,3 @@ def get_max_email_uid(email_account):
else:
max_uid = cint(result[0].get("uid", 0)) + 1
return max_uid
@frappe.whitelist()
def get_automatic_email_link():
return frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id")

View file

@ -139,6 +139,7 @@ doc_events = {
"on_change": [
"frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points"
],
"after_insert": "frappe.cache_manager.build_table_count_cache",
},
"Event": {
"after_insert": "frappe.integrations.doctype.google_calendar.google_calendar.insert_event_in_google_calendar",

View file

@ -76,7 +76,7 @@ class StripeSettings(Document):
def create_charge_on_stripe(self):
import stripe
try:
charge = stripe.Charge.create(amount=cint(flt(self.data.amount)*100), currency=self.data.currency, source=self.data.stripe_token_id, description=self.data.description)
charge = stripe.Charge.create(amount=cint(flt(self.data.amount)*100), currency=self.data.currency, source=self.data.stripe_token_id, description=self.data.description, receipt_email=self.data.payer_email)
if charge.captured == True:
self.integration_request.db_set('status', 'Completed', update_modified=False)

View file

@ -66,6 +66,10 @@ frappe.ui.form.on('Webhook', {
webhook_doctype: (frm) => {
frappe.webhook.set_fieldname_select(frm);
},
enable_security: (frm) => {
frm.toggle_reqd('webhook_secret', frm.doc.enable_security);
}
});

View file

@ -19,6 +19,9 @@
"request_url",
"cb_webhook",
"request_structure",
"sb_security",
"enable_security",
"webhook_secret",
"sb_webhook_headers",
"webhook_headers",
"sb_webhook_data",
@ -127,10 +130,27 @@
"fieldtype": "Select",
"label": "Naming Series",
"options": "\nHOOK-.####"
},
{
"fieldname": "sb_security",
"fieldtype": "Section Break",
"label": "Webhook Security"
},
{
"default": "0",
"fieldname": "enable_security",
"fieldtype": "Check",
"label": "Enable Security"
},
{
"depends_on": "eval:doc.enable_security == 1",
"fieldname": "webhook_secret",
"fieldtype": "Password",
"label": "Webhook Secret"
}
],
"links": [],
"modified": "2020-01-06 02:51:07.997566",
"modified": "2020-01-13 01:53:04.459968",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Webhook",

View file

@ -4,7 +4,10 @@
from __future__ import unicode_literals
import base64
import datetime
import hashlib
import hmac
import json
from time import sleep
@ -16,6 +19,8 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils.jinja import validate_template
WEBHOOK_SECRET_HEADER = "X-Frappe-Webhook-Signature"
class Webhook(Document):
def validate(self):
@ -94,10 +99,23 @@ def enqueue_webhook(doc, webhook):
def get_webhook_headers(doc, webhook):
headers = {}
if webhook.enable_security:
data = get_webhook_data(doc, webhook)
signature = base64.b64encode(
hmac.new(
webhook.get_password("webhook_secret").encode("utf8"),
json.dumps(data).encode("utf8"),
hashlib.sha256
).digest()
)
headers[WEBHOOK_SECRET_HEADER] = signature
if webhook.webhook_headers:
for h in webhook.webhook_headers:
if h.get("key") and h.get("value"):
headers[h.get("key")] = h.get("value")
return headers

View file

@ -31,6 +31,10 @@ def login_via_office365(code, state):
def login_via_salesforce(code, state):
login_via_oauth2("salesforce", code, state, decoder=decoder_compat)
@frappe.whitelist(allow_guest=True)
def login_via_fairlogin(code, state):
login_via_oauth2("fairlogin", code, state, decoder=decoder_compat)
@frappe.whitelist(allow_guest=True)
def custom(code, state):
"""

View file

@ -584,14 +584,17 @@ class Document(BaseDocument):
def get_permlevel_access(self, permission_type='write'):
if not hasattr(self, "_has_access_to"):
self._has_access_to = {}
if not self._has_access_to.get(permission_type):
self._has_access_to[permission_type] = []
roles = frappe.get_roles()
self._has_access_to = []
for perm in self.get_permissions():
if perm.role in roles and perm.permlevel > 0 and perm.get(permission_type):
if perm.permlevel not in self._has_access_to:
self._has_access_to.append(perm.permlevel)
if perm.permlevel not in self._has_access_to[permission_type]:
self._has_access_to[permission_type].append(perm.permlevel)
return self._has_access_to
return self._has_access_to[permission_type]
def has_permlevel_access_to(self, fieldname, df=None, permission_type='read'):
if not df:

View file

@ -3,7 +3,7 @@ import frappe
def execute():
if frappe.db.table_exists('Prepared Report'):
frappe.reload_doc("core", "doctype", "prepared_doctype")
frappe.reload_doc("core", "doctype", "prepared_report")
prepared_reports = frappe.get_all("Prepared Report")
for report in prepared_reports:
frappe.delete_doc("Prepared Report", report.name)

View file

@ -481,7 +481,7 @@ frappe.Application = Class.extend({
// Iterate over changelog
var change_log_dialog = frappe.msgprint({
message: frappe.render_template("change_log", {"change_log": change_log}),
title: __("Updated To New Version 🎉"),
title: __("Updated To A New Version 🎉"),
wide: true,
scroll: true
});

View file

@ -27,7 +27,11 @@ frappe.ui.form.ControlBarcode = frappe.ui.form.ControlData.extend({
set_formatted_input(value) {
// Set values to display
let svg = value;
const barcode_value = $(svg).attr('data-barcode-value');
let barcode_value = '';
if (value && value.startsWith('<svg')) {
barcode_value = $(svg).attr('data-barcode-value');
}
if (!barcode_value && this.doc) {
svg = this.get_barcode_html(value);

View file

@ -1,4 +1,6 @@
frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlCode.extend({
frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({
horizontal: false,
make_wrapper() {
// Create the elements for map area
this._super();

View file

@ -15,7 +15,7 @@ frappe.ui.form.Timeline = class Timeline {
var me = this;
this.wrapper = $(frappe.render_template("timeline",{doctype: me.frm.doctype,allow_events_in_timeline: me.frm.meta.allow_events_in_timeline})).appendTo(me.parent);
this.set_automatic_link_email();
this.display_automatic_link_email();
this.list = this.wrapper.find(".timeline-items");
this.email_link = this.wrapper.find(".timeline-email-import");
@ -117,31 +117,12 @@ frappe.ui.form.Timeline = class Timeline {
});
}
set_automatic_link_email() {
if (!frappe.email.automatic_link_email){
frappe.call("frappe.email.doctype.email_account.email_account.get_automatic_email_link").then((r) => {
if (r && r.message) {
frappe.email.automatic_link_email = r.message;
} else {
frappe.email.automatic_link_email = null;
}
this.display_automatic_link_email();
});
} else {
this.display_automatic_link_email();
}
}
display_automatic_link_email() {
var me = this;
if (frappe.email.automatic_link_email){
let email_id = frappe.email.automatic_link_email;
email_id = email_id.split("@")[0] +"+"+ encodeURIComponent(me.frm.doctype) +"+"+ encodeURIComponent(me.frm.docname)
+"@"+ email_id.split("@")[1];
let docinfo = this.frm.get_docinfo();
$(".timeline-email-import-link").text(email_id);
} else {
$('.timeline-email-import').addClass("hide");
if (docinfo.document_email){
let link = __("Send an email to {0} to link it here", [`<b><a class="timeline-email-import-link copy-to-clipboard">${docinfo.document_email}</a></b>`]);
$('.timeline-email-import').html(link);
}
}
@ -211,7 +192,7 @@ frappe.ui.form.Timeline = class Timeline {
me.render_timeline_item(d);
});
me.display_automatic_link_email();
// more btn
if (this.more===undefined && timeline.length===20) {
@ -371,6 +352,10 @@ frappe.ui.form.Timeline = class Timeline {
c.sender = c.sender.split("<")[1].split(">")[0];
}
if (!c.doctype && ['Comment', 'Communication'].includes(c.communication_type)) {
c.doctype = c.communication_type;
}
c.user_info = frappe.user_info(c.sender);
c["delete"] = "";

View file

@ -648,18 +648,96 @@ frappe.ui.form.Form = class FrappeForm {
}
savecancel(btn, callback, on_error) {
var me = this;
const me = this;
this.validate_form_action('Cancel');
frappe.confirm(__("Permanently Cancel {0}?", [this.docname]), function() {
frappe.call({
method: "frappe.desk.form.linked_with.get_submitted_linked_docs",
args: {
doctype: me.doc.doctype,
name: me.doc.name
},
freeze: true,
callback: (r) => {
if (!r.exc && r.message.count > 0) {
me._cancel_all(r, btn, callback, on_error);
} else {
me._cancel(btn, callback, on_error, false);
}
}
});
}
_cancel_all(r, btn, callback, on_error) {
const me = this;
// add confirmation message for cancelling all linked docs
let links_text = "";
let links = r.message.docs;
const doctypes = Array.from(new Set(links.map(link => link.doctype)));
for (let doctype of doctypes) {
let docnames = links
.filter((link) => link.doctype == doctype)
.map((link) => frappe.utils.get_form_link(link.doctype, link.name, true))
.join(", ");
links_text += `<li><strong>${doctype}</strong>: ${docnames}</li>`;
}
links_text = `<ul>${links_text}</ul>`;
let confirm_message = __('{0} {1} is linked with the following submitted documents: {2}',
[(me.doc.doctype).bold(), me.doc.name, links_text]);
let can_cancel = links.every((link) => frappe.model.can_cancel(link.doctype));
if (can_cancel) {
confirm_message += __('Do you want to cancel all linked documents?');
} else {
confirm_message += __('You do not have permissions to cancel all linked documents.');
}
// generate dialog box to cancel all linked docs
let d = new frappe.ui.Dialog({
title: __("Cancel All Documents"),
fields: [{
fieldtype: "HTML",
options: `<p class="frappe-confirm-message">${confirm_message}</p>`
}]
}, () => me.handle_save_fail(btn, on_error));
// if user can cancel all linked docs, add action to the dialog
if (can_cancel) {
d.set_primary_action("Cancel All", () => {
d.hide();
frappe.call({
method: "frappe.desk.form.linked_with.cancel_all_linked_docs",
args: {
docs: links
},
freeze: true,
callback: (resp) => {
if (!resp.exc) {
me.reload_doc();
me._cancel(btn, callback, on_error, true);
}
}
});
});
}
d.show();
};
_cancel(btn, callback, on_error, skip_confirm) {
const me = this;
const cancel_doc = () => {
frappe.validated = true;
me.script_manager.trigger("before_cancel").then(function() {
if(!frappe.validated) {
me.script_manager.trigger("before_cancel").then(() => {
if (!frappe.validated) {
return me.handle_save_fail(btn, on_error);
}
var after_cancel = function(r) {
if(r.exc) {
if (r.exc) {
me.handle_save_fail(btn, on_error);
} else {
frappe.utils.play_sound("cancel");
@ -670,8 +748,14 @@ frappe.ui.form.Form = class FrappeForm {
};
frappe.ui.form.save(me, "cancel", after_cancel, btn);
});
}, () => me.handle_save_fail(btn, on_error));
}
}
if (skip_confirm) {
cancel_doc();
} else {
frappe.confirm(__("Permanently Cancel {0}?", [this.docname]), cancel_doc, me.handle_save_fail(btn, on_error));
}
};
savetrash() {
this.validate_form_action("Delete");

View file

@ -65,26 +65,22 @@ export default class Grid {
<div class="small form-clickable-section grid-footer">
<div class="row">
<div class="col-sm-5 grid-buttons">
<button type="reset"
class="btn btn-xs btn-danger grid-remove-rows hidden"
<button class="btn btn-xs btn-danger grid-remove-rows hidden"
style="margin-right: 4px;"
data-action="delete_rows">
${__("Delete")}
</button>
<button type="reset"
class="btn btn-xs btn-danger grid-remove-all-rows hidden"
<button class="btn btn-xs btn-danger grid-remove-all-rows hidden"
style="margin-right: 4px;"
data-action="delete_all_rows">
${__("Delete All")}
</button>
<button type="reset"
class="grid-add-multiple-rows btn btn-xs btn-default hidden"
<button class="grid-add-multiple-rows btn btn-xs btn-default hidden"
style="margin-right: 4px;">
${__("Add Multiple")}</a>
</button>
<!-- hack to allow firefox include this in tabs -->
<button type="reset"
class="btn btn-xs btn-default grid-add-row">
<button class="btn btn-xs btn-default grid-add-row">
${__("Add Row")}
</button>
</div>

View file

@ -18,9 +18,7 @@
{% } %}
</div>
<div class="timeline-new-email timeline-email-import text-muted small">
<p>
{%= __("Send an email to {0} to link it here.", [`<b><a class="timeline-email-import-link copy-to-clipboard"></a></b>`]) %}
</p>
</div>
<div class="timeline-items">

View file

@ -267,19 +267,24 @@ frappe.ui.form.Toolbar = Class.extend({
});
}
if(frappe.user_roles.includes("System Manager") && me.frm.meta.issingle === 0) {
this.page.add_menu_item(__("Customize"), function() {
if (frappe.user_roles.includes("System Manager") && me.frm.meta.issingle === 0) {
let is_doctype_form = me.frm.doctype === 'DocType';
let doctype = is_doctype_form ? me.frm.docname : me.frm.doctype;
let is_doctype_custom = is_doctype_form ? me.frm.doc.custom : false;
if (me.frm.meta && me.frm.meta.custom) {
frappe.set_route('Form', 'DocType', me.frm.doctype);
} else {
frappe.set_route('Form', 'Customize Form', {
doc_type: me.frm.doctype
});
}
}, true);
if (doctype != 'DocType' && !is_doctype_custom) {
this.page.add_menu_item(__("Customize"), function() {
if (me.frm.meta && me.frm.meta.custom) {
frappe.set_route('Form', 'DocType', doctype);
} else {
frappe.set_route('Form', 'Customize Form', {
doc_type: doctype
});
}
}, true);
}
if (frappe.boot.developer_mode===1) {
if (frappe.boot.developer_mode===1 && !is_doctype_form) {
// edit doctype
this.page.add_menu_item(__("Edit DocType"), function() {
frappe.set_route('Form', 'DocType', me.frm.doctype);

View file

@ -195,7 +195,7 @@ frappe.ui.Filter = class {
// called when condition is changed,
// don't change if all is well
if(this.field && cur.fieldname == fieldname && df.fieldtype == cur.fieldtype &&
df.parent == cur.parent) {
df.parent == cur.parent && df.options == cur.options) {
return;
}

View file

@ -81,7 +81,11 @@ frappe.ui.GroupBy = class {
}
apply_settings(settings) {
this.groupby_select.val(settings.group_by);
// Extract fieldname from `tabdoctype`.`fieldname`
let group_by_fieldname = settings.group_by.split('.')[1].replace(/`/g, '');
this.groupby_select.val(group_by_fieldname);
this.aggregate_function_select.val(settings.aggregate_function);
this.show_hide_aggregate_on();
this.aggregate_on_select.val(settings.aggregate_on);

View file

@ -538,7 +538,7 @@ frappe.provide("frappe.views");
if(!card) return;
make_dom();
render_card_meta();
bind_edit_card();
add_task_link();
// edit_card_title();
}
@ -576,11 +576,9 @@ frappe.provide("frappe.views");
self.$card.find(".kanban-card-meta").empty().append(html);
}
function bind_edit_card() {
self.$card.find('.kanban-card.content').on('click', function() {
frappe.set_route('Form', card.doctype, card.name);
// setup_edit_card();
});
function add_task_link() {
let taskLink = frappe.utils.get_form_link(card.doctype, card.name);
self.$card.find('.kanban-card-redirect').attr('href', taskLink);
}
function refresh_dialog() {

View file

@ -1,9 +1,11 @@
<div class="kanban-card-wrapper {{ disable_click }}" data-name="{{name}}">
<div class="kanban-card content">
<div class="kanban-card-title">
{{ title }}
<a class="kanban-card-redirect" href="#">
<div class="kanban-card content">
<div class="kanban-card-title">
{{ title }}
</div>
<div class="kanban-card-meta">
</div>
</div>
<div class="kanban-card-meta">
</div>
</div>
</a>
</div>

View file

@ -29,13 +29,13 @@
<!-- body -->
<tbody>
{% for row in data %}
<tr>
<tr style="height: 30px">
{% for col in columns %}
{% if col.name && col._id !== "_check" %}
{% var value = col.fieldname ? row[col.fieldname] : row[col.id]; %}
<td>
<td {% if row.bold == 1 %} style="font-weight: bold" {% endif %}>
<span {% if col._index == 0 %} style="padding-left: {%= cint(row.indent) * 2 %}em" {% endif %}>
{{
col.formatter

View file

@ -684,7 +684,6 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
}
build_fields() {
this.fields.push(['docstatus', this.doctype]);
super.build_fields();
}
@ -742,6 +741,16 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
}
}
add_status_dependency_column(col, doctype) {
// Adds dependent column from which status is derived if required
if (!this.fields.find(f => f[0] === col)) {
const field = [col, doctype];
this.fields.push(field);
this.refresh();
frappe.show_alert(__('Also adding the status dependency field {0}', [field[0].bold()]));
}
}
remove_column_from_datatable(column) {
const index = this.fields.findIndex(f => column.field === f[0]);
if (index === -1) return;
@ -778,12 +787,24 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
let doctype_fields = frappe.meta.get_docfields(this.doctype).filter(standard_fields_filter);
// filter out docstatus field from picker
let std_fields = frappe.model.std_fields.filter( df => df.fieldname !== 'docstatus');
// add status field derived from docstatus, if status is not a standard field
if (!frappe.meta.has_field(this.doctype, 'status')) {
doctype_fields = [{
label: __('Status'),
fieldname: 'docstatus',
fieldtype: 'Data'
}].concat(doctype_fields);
}
doctype_fields = [{
label: __('ID'),
fieldname: 'name',
fieldtype: 'Data',
reqd: 1
}].concat(doctype_fields, frappe.model.std_fields);
}].concat(doctype_fields, std_fields);
out[this.doctype] = doctype_fields;
@ -858,16 +879,23 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
this.columns_map = {};
for (let f of this.fields) {
let column;
if (f[0]!=='docstatus') {
let column = this.build_column(f);
if (column) {
if (column_widths) {
column.width = column_widths[column.id] || column.width || 120;
}
this.columns.push(column);
this.columns_map[column.id] = column;
column = this.build_column(f);
} else {
// if status is not in fields append status column derived from docstatus
if (!this.fields.includes(['status', this.doctype]) && !frappe.meta.has_field(this.doctype, 'status')) {
column = this.build_column(['docstatus', this.doctype]);
}
}
if (column) {
if (column_widths) {
column.width = column_widths[column.id] || column.width || 120;
}
this.columns.push(column);
this.columns_map[column.id] = column;
}
}
}
@ -892,9 +920,14 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
}
}
docfield.parent = this.doctype;
if (fieldname == "name") {
if (fieldname == 'name') {
docfield.options = this.doctype;
}
if (fieldname == 'docstatus' && !frappe.meta.has_field(this.doctype, 'status')) {
docfield.label = 'Status';
docfield.fieldtype = 'Data';
docfield.name = 'status';
}
}
}
if (!docfield) return;
@ -982,7 +1015,6 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
totals_row[0].content = __('Totals').bold();
out.push(totals_row);
}
return out;
}
@ -1003,8 +1035,22 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
}
};
}
if (col.field in d) {
if (col.field === 'docstatus' && !frappe.meta.has_field(this.doctype, 'status')) {
// get status from docstatus
let status = frappe.get_indicator(d, this.doctype);
if (!status[0]) {
// get_indicator returns the dependent field's condition as the 3rd parameter
let dependent_col = status[2].split(',')[0];
// add status dependency column
this.add_status_dependency_column(dependent_col, this.doctype);
}
return {
name: d.name,
doctype: col.docfield.parent,
content: status[0],
editable: false
};
} else if (col.field in d) {
const value = d[col.field];
return {
name: d.name,

View file

@ -86,7 +86,11 @@ export default class WebForm extends frappe.ui.FieldGroup {
}
setup_delete_button() {
this.add_button_to_header("Delete", "danger", () => this.delete());
this.add_button_to_header(
'<i class="fa fa-trash" aria-hidden="true"></i>',
"light",
() => this.delete()
);
}
setup_print_button() {

View file

@ -713,6 +713,7 @@ li.user-progress {
height: 60px;
width: 60px;
background-color: #fafbfc;
overflow: hidden;
.flex-text {
display: flex;

View file

@ -87,6 +87,15 @@
.kanban-card-wrapper {
position: relative;
.kanban-card-redirect {
display: block;
&:hover,
&:focus {
text-decoration: none;
}
}
}
.kanban-card:hover, .new-card-area, .edit-card-area {

View file

@ -3,7 +3,7 @@
{{ render_table(df, doc) }}
{%- elif df.fieldtype=="HTML" and df.options -%}
<div>{{ frappe.render_template(df.options, {"doc": doc}) or "" }}</div>
{%- elif df.fieldtype in ("Text", "Text Editor", "Code") -%}
{%- elif df.fieldtype in ("Text", "Text Editor", "Code", "Long Text") -%}
{{ render_text_field(df, doc) }}
{%- elif df.fieldtype in ("Image", "Attach Image", "Attach")
and (guess_mimetype(doc[df.fieldname])[0] or "").startswith("image/") -%}
@ -95,7 +95,7 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}"
{%- macro render_text_field(df, doc) -%}
{%- if doc.get(df.fieldname) != None -%}
<div style="padding: 10px 0px" {{ fieldmeta(df) }}>
{%- if df.fieldtype in ("Text", "Code") %}<label>{{ _(df.label) }}</label>{%- endif %}
{%- if df.fieldtype in ("Text", "Code", "Long Text") %}<label>{{ _(df.label) }}</label>{%- endif %}
{%- if df.fieldtype=="Code" %}
<pre class="value">{{ doc.get(df.fieldname) }}</pre>
{% else -%}

View file

@ -41,7 +41,7 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(),
xmloutput_fh = None
if junit_xml_output:
xmloutput_fh = open(junit_xml_output, 'w')
xmloutput_fh = open(junit_xml_output, 'wb')
unittest_runner = xmlrunner_wrapper(xmloutput_fh)
else:
unittest_runner = unittest.TextTestRunner
@ -68,11 +68,11 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(),
frappe.get_attr(fn)()
if doctype:
ret = run_tests_for_doctype(doctype, verbose, tests, force, profile)
ret = run_tests_for_doctype(doctype, verbose, tests, force, profile, junit_xml_output=junit_xml_output)
elif module:
ret = run_tests_for_module(module, verbose, tests, profile)
ret = run_tests_for_module(module, verbose, tests, profile, junit_xml_output=junit_xml_output)
else:
ret = run_all_tests(app, verbose, profile, ui_tests, failfast=failfast)
ret = run_all_tests(app, verbose, profile, ui_tests, failfast=failfast, junit_xml_output=junit_xml_output)
if frappe.db: frappe.db.commit()
@ -109,7 +109,7 @@ class TimeLoggingTestResult(unittest.TextTestResult):
super(TimeLoggingTestResult, self).addSuccess(test)
def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfast=False):
def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfast=False, junit_xml_output=False):
import os
apps = [app] if app else frappe.get_installed_apps()
@ -130,11 +130,16 @@ def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfa
_add_test(app, path, filename, verbose,
test_suite, ui_tests)
if junit_xml_output:
runner = unittest_runner(verbosity=1+(verbose and 1 or 0), failfast=failfast)
else:
runner = unittest_runner(resultclass=TimeLoggingTestResult, verbosity=1+(verbose and 1 or 0), failfast=failfast)
if profile:
pr = cProfile.Profile()
pr.enable()
out = unittest_runner(resultclass=TimeLoggingTestResult, verbosity=1+(verbose and 1 or 0), failfast=failfast).run(test_suite)
out = runner.run(test_suite)
if profile:
pr.disable()
@ -145,7 +150,7 @@ def run_all_tests(app=None, verbose=False, profile=False, ui_tests=False, failfa
return out
def run_tests_for_doctype(doctypes, verbose=False, tests=(), force=False, profile=False):
def run_tests_for_doctype(doctypes, verbose=False, tests=(), force=False, profile=False, junit_xml_output=False):
modules = []
if not isinstance(doctypes, (list, tuple)):
doctypes = [doctypes]
@ -163,17 +168,17 @@ def run_tests_for_doctype(doctypes, verbose=False, tests=(), force=False, profil
make_test_records(doctype, verbose=verbose, force=force)
modules.append(importlib.import_module(test_module))
return _run_unittest(modules, verbose=verbose, tests=tests, profile=profile)
return _run_unittest(modules, verbose=verbose, tests=tests, profile=profile, junit_xml_output=junit_xml_output)
def run_tests_for_module(module, verbose=False, tests=(), profile=False):
def run_tests_for_module(module, verbose=False, tests=(), profile=False, junit_xml_output=False):
module = importlib.import_module(module)
if hasattr(module, "test_dependencies"):
for doctype in module.test_dependencies:
make_test_records(doctype, verbose=verbose)
return _run_unittest(module, verbose=verbose, tests=tests, profile=profile)
return _run_unittest(module, verbose=verbose, tests=tests, profile=profile, junit_xml_output=junit_xml_output)
def _run_unittest(modules, verbose=False, tests=(), profile=False):
def _run_unittest(modules, verbose=False, tests=(), profile=False, junit_xml_output=False):
test_suite = unittest.TestSuite()
if not isinstance(modules, (list, tuple)):
@ -189,13 +194,18 @@ def _run_unittest(modules, verbose=False, tests=(), profile=False):
else:
test_suite.addTest(module_test_cases)
if junit_xml_output:
runner = unittest_runner(verbosity=1+(verbose and 1 or 0))
else:
runner = unittest_runner(resultclass=TimeLoggingTestResult, verbosity=1+(verbose and 1 or 0))
if profile:
pr = cProfile.Profile()
pr.enable()
frappe.flags.tests_verbose = verbose
out = unittest_runner(verbosity=1+(verbose and 1 or 0)).run(test_suite)
out = runner.run(test_suite)
if profile:

View file

@ -4,8 +4,10 @@ from __future__ import unicode_literals
import frappe, unittest
from frappe.desk.form.load import getdoctype, getdoc
from frappe.core.page.permission_manager.permission_manager import update, reset
from frappe.core.page.permission_manager.permission_manager import update, reset, add
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
test_dependencies = ['Blog Category', 'Blogger']
class TestFormLoad(unittest.TestCase):
def test_load(self):
@ -20,56 +22,65 @@ class TestFormLoad(unittest.TestCase):
self.assertTrue(meta.get("__calendar_js"))
def test_fieldlevel_permissions_in_load(self):
blog = frappe.get_doc({
"doctype": "Blog Post",
"blog_category": "_Test Blog Category 1",
"blog_intro": "Test Blog Intro",
"blogger": "_Test Blogger 1",
"content": "Test Blog Content",
"title": "_Test Blog Post {}".format(frappe.utils.now()),
"published": 0
})
blog.insert()
user = frappe.get_doc('User', 'test@example.com')
user.remove_roles('Website Manager')
user_roles = frappe.get_roles()
user.remove_roles(*user_roles)
user.add_roles('Blogger')
make_property_setter('Blog Post', 'published', 'permlevel', 1, 'Int')
reset('Blog Post')
frappe.db.set_value('DocField', {
'fieldname': 'published',
'parent': 'Blog Post'
}, 'permlevel', 1)
update('Blog Post', 'Website Manager', 0, 'permlevel', 1)
add('Blog Post', 'Website Manager', 1)
update('Blog Post', 'Website Manager', 1, 'write', 1)
frappe.set_user(user.name)
# print frappe.as_json(get_valid_perms('Blog Post'))
blog_doc = get_blog(blog.name)
frappe.clear_cache(doctype='Blog Post')
self.assertEqual(blog_doc.name, blog.name)
# since published field has higher permlevel
self.assertEqual(blog_doc.published, None)
blog = frappe.db.get_value('Blog Post', {'title': '_Test Blog Post'})
getdoc('Blog Post', blog)
checked = False
for doc in frappe.response.docs:
if doc.name == blog:
self.assertEqual(doc.published, None)
checked = True
self.assertTrue(checked, True)
frappe.db.set_value('DocField', {
'fieldname': 'published',
'parent': 'Blog Post'
}, 'permlevel', 0)
reset('Blog Post')
frappe.clear_cache(doctype='Blog Post')
frappe.response.docs = []
getdoc('Blog Post', blog)
checked = False
for doc in frappe.response.docs:
if doc.name == blog:
self.assertEqual(doc.published, 1)
checked = True
self.assertTrue(checked, True)
# this will be ignored because user does not
# have write access on `published` field (or on permlevel 1 fields)
blog_doc.published = 1
blog_doc.save()
# since published field has higher permlevel
self.assertEqual(blog_doc.published, 0)
frappe.set_user('Administrator')
user.add_roles('Website Manager')
frappe.set_user(user.name)
doc = frappe.get_doc('Blog Post', blog.name)
doc.published = 1
doc.save()
blog_doc = get_blog(blog.name)
# now user should be allowed to read field with higher permlevel
# (after adding Website Manager role)
self.assertEqual(blog_doc.published, 1)
frappe.set_user('Administrator')
# reset user roles
user.remove_roles('Blogger', 'Website Manager')
user.add_roles(*user_roles)
def get_blog(blog_name):
frappe.response.docs = []
getdoc('Blog Post', blog_name)
doc = frappe.response.docs[0]
return doc

View file

@ -4046,3 +4046,5 @@ apps/frappe/frappe/public/js/frappe/web_form/web_form.js,Saved Successfully,保
apps/frappe/frappe/core/doctype/user/user.py,Please ask your administrator to verify your sign-up,请向管理员询问,以确认您的注册
DocType: Domain Settings,Active Domains,活动域
apps/frappe/frappe/public/js/integrations/razorpay.js,Show Log,显示日志
apps/frappe/frappe/desk/page/leaderboard/leaderboard.js,Leaderboard,排行榜
apps/frappe/frappe/config/desktop.py,Leaderboard,排行榜

1 apps/frappe/frappe/utils/change_log.py New {} releases for the following apps are available 可以使用以下应用程序的新{}版本
4046
4047
4048
4049
4050

View file

@ -256,6 +256,10 @@ app_license = "{app_license}"
# "Task": "{app_name}.task.get_dashboard_data"
# }}
# exempt linked doctypes from being automatically cancelled
#
# auto_cancel_exempted_doctypes = ["Auto Repeat"]
"""
desktop_template = """# -*- coding: utf-8 -*-

View file

@ -375,7 +375,8 @@ $.extend(frappe, {
window.valid_email = function(id) {
// eslint-disable-next-line
return /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/.test(id.toLowerCase());
// copied regex from frappe/utils.js validate_type
return /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/.test(id.toLowerCase());
}
window.validate_email = valid_email;

View file

@ -20,11 +20,11 @@ google-auth-httplib2==0.0.3
google-auth-oauthlib==0.4.1
google-auth==1.7.1
googlemaps==3.1.1
gunicorn==19.9.0
gunicorn==19.10.0
html2text==2016.9.19
ipython==5.8.0
Jinja2==2.10.3
markdown2==2.3.6
markdown2==2.3.7
maxminddb-geolite2==2018.703
ndg-httpsclient==0.5.1
num2words==0.5.5
@ -32,7 +32,7 @@ oauthlib==3.1.0
openpyxl==2.6.4
passlib==1.7.1
pdfkit==0.6.1
Pillow==6.2.1
Pillow==6.2.2
premailer==3.6.1
psycopg2-binary==2.8.4
pyasn1==0.4.7
@ -46,7 +46,7 @@ pypng==0.0.20
PyQRCode==1.2.1
python-dateutil==2.8.1
pytz==2019.3
PyYAML==3.13
PyYAML==5.1
rauth==0.7.3
redis>=3.0
requests-oauthlib==1.3.0