Merge branch 'develop' into paytm-integration

This commit is contained in:
Suraj Shetty 2020-04-25 09:21:32 +05:30 committed by GitHub
commit c09da72a7d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 755 additions and 157 deletions

View file

@ -78,6 +78,7 @@
"has_common": true,
"has_words": true,
"validate_email": true,
"validate_name": true,
"validate_phone": true,
"get_number_format": true,
"format_number": true,

View file

@ -3,7 +3,7 @@
{
"hidden": 0,
"label": "Tools",
"links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]"
"links": "[\n {\n \"description\": \"Documents assigned to you and by you.\",\n \"label\": \"To Do\",\n \"name\": \"ToDo\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Event and other calendars.\",\n \"label\": \"Calendar\",\n \"link\": \"List/Event/Calendar\",\n \"name\": \"Event\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Private and public Notes.\",\n \"label\": \"Note\",\n \"name\": \"Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Files\",\n \"name\": \"File\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Video\",\n \"name\": \"Video\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Activity log of all users.\",\n \"label\": \"Activity\",\n \"name\": \"activity\",\n \"type\": \"page\"\n }\n]"
},
{
"hidden": 0,
@ -32,7 +32,7 @@
"idx": 0,
"is_standard": 1,
"label": "Tools",
"modified": "2020-04-01 11:24:40.804346",
"modified": "2020-04-20 18:21:14.152537",
"modified_by": "Administrator",
"module": "Automation",
"name": "Tools",

View file

@ -17,6 +17,7 @@ from frappe.utils.change_log import get_versions
from frappe.translate import get_lang_dict
from frappe.email.inbox import get_email_accounts
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled
from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_points
from frappe.social.doctype.post.post import frequently_visited_links
@ -79,6 +80,7 @@ def get_bootinfo():
bootinfo.success_action = get_success_action()
bootinfo.update(get_email_accounts(user=frappe.session.user))
bootinfo.energy_points_enabled = is_energy_point_enabled()
bootinfo.website_tracking_enabled = is_tracking_enabled()
bootinfo.points = get_energy_points(frappe.session.user)
bootinfo.frequently_visited_links = frequently_visited_links()
bootinfo.link_preview_doctypes = get_link_preview_doctypes()

View file

@ -477,7 +477,8 @@ class DocType(Document):
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict['fields']))
if field_dict:
new_field_dicts.append(field_dict[0])
remaining_field_names.remove(fieldname)
if fieldname in remaining_field_names:
remaining_field_names.remove(fieldname)
for fieldname in remaining_field_names:
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict['fields']))
@ -498,7 +499,8 @@ class DocType(Document):
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict.get('fields', [])))
if field_dict:
new_field_dicts.append(field_dict[0])
remaining_field_names.remove(fieldname)
if fieldname in remaining_field_names:
remaining_field_names.remove(fieldname)
for fieldname in remaining_field_names:
field_dict = list(filter(lambda d: d['fieldname'] == fieldname, docdict.get('fields', [])))
@ -893,7 +895,7 @@ def validate_fields(meta):
field.fetch_from = field.fetch_from.strip('\n').strip()
def validate_data_field_type(docfield):
if docfield.fieldtype == "Data":
if docfield.fieldtype == "Data" and not (docfield.oldfieldtype and docfield.oldfieldtype != "Data"):
if docfield.options and (docfield.options not in data_field_options):
df_str = frappe.bold(_(docfield.label))
text_str = _("{0} is an invalid Data field.").format(df_str) + "<br>" * 2 + _("Only Options allowed for Data field are:") + "<br>"

View file

@ -97,47 +97,49 @@ frappe.ui.form.on('User', {
});
}, __("Password"));
frappe.db.get_single_value("LDAP Settings", "enabled").then((value) => {
if (value === 1 && frm.doc.name != "Administrator") {
frm.add_custom_button(__("Reset LDAP Password"), function() {
const d = new frappe.ui.Dialog({
title: __("Reset LDAP Password"),
fields: [
{
label: __("New Password"),
fieldtype: "Password",
fieldname: "new_password",
reqd: 1
},
{
label: __("Confirm New Password"),
fieldtype: "Password",
fieldname: "confirm_password",
reqd: 1
},
{
label: __("Logout All Sessions"),
fieldtype: "Check",
fieldname: "logout_sessions"
if (frappe.user.has_role("System Manager")) {
frappe.db.get_single_value("LDAP Settings", "enabled").then((value) => {
if (value === 1 && frm.doc.name != "Administrator") {
frm.add_custom_button(__("Reset LDAP Password"), function() {
const d = new frappe.ui.Dialog({
title: __("Reset LDAP Password"),
fields: [
{
label: __("New Password"),
fieldtype: "Password",
fieldname: "new_password",
reqd: 1
},
{
label: __("Confirm New Password"),
fieldtype: "Password",
fieldname: "confirm_password",
reqd: 1
},
{
label: __("Logout All Sessions"),
fieldtype: "Check",
fieldname: "logout_sessions"
}
],
primary_action: (values) => {
d.hide();
if (values.new_password !== values.confirm_password) {
frappe.throw(__("Passwords do not match!"));
}
frappe.call(
"frappe.integrations.doctype.ldap_settings.ldap_settings.reset_password", {
user: frm.doc.email,
password: values.new_password,
logout: values.logout_sessions
});
}
],
primary_action: (values) => {
d.hide();
if (values.new_password !== values.confirm_password) {
frappe.throw(__("Passwords do not match!"));
}
frappe.call(
"frappe.integrations.doctype.ldap_settings.ldap_settings.reset_password", {
user: frm.doc.email,
password: values.new_password,
logout: values.logout_sessions
});
}
});
d.show();
}, __("Password"));
}
});
});
d.show();
}, __("Password"));
}
});
}
frm.add_custom_button(__("Reset OTP Secret"), function() {
frappe.call({

View file

@ -551,6 +551,7 @@ def update_password(new_password, logout_all_sessions=0, key=None, old_password=
res = _get_user_for_update_password(key, old_password)
if res.get('message'):
frappe.local.response.http_status_code = 410
return res['message']
else:
user = res['user']
@ -718,7 +719,7 @@ def _get_user_for_update_password(key, old_password):
user = frappe.db.get_value("User", {"reset_password_key": key})
if not user:
return {
'message': _("Cannot Update: Incorrect / Expired Link.")
'message': _("The Link specified has either been used before or Invalid")
}
elif old_password:

View file

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestVideo(unittest.TestCase):
pass

View file

@ -0,0 +1,8 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Video', {
// refresh: function(frm) {
// }
});

View file

@ -0,0 +1,106 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:title",
"creation": "2018-10-17 05:47:13.087395",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"provider",
"url",
"column_break_4",
"publish_date",
"duration",
"section_break_7",
"description"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1,
"unique": 1
},
{
"fieldname": "provider",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Provider",
"options": "YouTube\nVimeo",
"reqd": 1
},
{
"fieldname": "url",
"fieldtype": "Data",
"in_list_view": 1,
"label": "URL",
"reqd": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "publish_date",
"fieldtype": "Date",
"label": "Publish Date"
},
{
"fieldname": "duration",
"fieldtype": "Data",
"label": "Duration"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break"
},
{
"fieldname": "description",
"fieldtype": "Text Editor",
"in_list_view": 1,
"label": "Description",
"reqd": 1
}
],
"links": [],
"modified": "2020-04-22 12:09:49.057403",
"modified_by": "Administrator",
"module": "Core",
"name": "Video",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class Video(Document):
pass

View file

@ -268,8 +268,9 @@ def get_open_count(doctype, name, items=[]):
"count": out,
}
module = frappe.get_meta_module(doctype)
if hasattr(module, "get_timeline_data"):
out["timeline_data"] = module.get_timeline_data(doctype, name)
if not meta.custom:
module = frappe.get_meta_module(doctype)
if hasattr(module, "get_timeline_data"):
out["timeline_data"] = module.get_timeline_data(doctype, name)
return out

View file

@ -242,7 +242,7 @@ def get_prepared_report_result(report, filters, dn="", user=None):
columns = json.loads(doc.columns) if doc.columns else data[0]
for column in columns:
if isinstance(column, dict):
if isinstance(column, dict) and column.get("label"):
column["label"] = _(column["label"])
latest_report_data = {

View file

@ -39,7 +39,7 @@ class EmailDomain(Document):
except Exception:
frappe.throw(_("Incoming email account not correct"))
return None
finally:
try:
if self.use_imap:
@ -48,9 +48,10 @@ class EmailDomain(Document):
test.quit()
except Exception:
pass
try:
if self.use_ssl_for_outgoing:
if not self.smtp_port:
if self.get('use_ssl_for_outgoing'):
if not self.get('smtp_port'):
self.smtp_port = 465
sess = smtplib.SMTP_SSL((self.smtp_server or "").encode('utf-8'),
@ -62,28 +63,15 @@ class EmailDomain(Document):
sess.quit()
except Exception:
frappe.throw(_("Outgoing email account not correct"))
return None
return
def on_update(self):
"""update all email accounts using this domain"""
for email_account in frappe.get_all("Email Account",
filters={"domain": self.name}):
for email_account in frappe.get_all("Email Account", filters={"domain": self.name}):
try:
email_account = frappe.get_doc("Email Account",
email_account.name)
email_account.set("email_server",self.email_server)
email_account.set("use_imap",self.use_imap)
email_account.set("use_ssl",self.use_ssl)
email_account.set("use_tls",self.use_tls)
email_account.set("attachment_limit",self.attachment_limit)
email_account.set("smtp_server",self.smtp_server)
email_account.set("smtp_port",self.smtp_port)
email_account.set("use_ssl_for_outgoing", self.use_ssl_for_outgoing)
email_account.set("append_emails_to_sent_folder", self.append_emails_to_sent_folder)
email_account = frappe.get_doc("Email Account", email_account.name)
for attr in ["email_server", "use_imap", "use_ssl", "use_tls", "attachment_limit", "smtp_server", "smtp_port", "use_ssl_for_outgoing", "append_emails_to_sent_folder"]:
email_account.set(attr, self.get(attr, default=0))
email_account.save()
except Exception as e:
frappe.msgprint(email_account.name)
frappe.throw(e)
return None
frappe.msgprint(_("Error has occurred in {0}").format(email_account.name), raise_exception=e.__class__)

View file

@ -78,6 +78,7 @@ class TimestampMismatchError(ValidationError): pass
class EmptyTableError(ValidationError): pass
class LinkExistsError(ValidationError): pass
class InvalidEmailAddressError(ValidationError): pass
class InvalidNameError(ValidationError): pass
class InvalidPhoneNumberError(ValidationError): pass
class TemplateNotFoundError(ValidationError): pass
class UniqueValidationError(ValidationError): pass
@ -95,4 +96,4 @@ class DataTooLongException(ValidationError): pass
# OAuth exceptions
class InvalidAuthorizationHeader(CSRFTokenError): pass
class InvalidAuthorizationPrefix(CSRFTokenError): pass
class InvalidAuthorizationToken(CSRFTokenError): pass
class InvalidAuthorizationToken(CSRFTokenError): pass

View file

@ -48,7 +48,7 @@ table_fields = ('Table', 'Table MultiSelect')
core_doctypes_list = ('DocType', 'DocField', 'DocPerm', 'DocType Action', 'DocType Link', 'User', 'Role', 'Has Role',
'Page', 'Module Def', 'Print Format', 'Report', 'Customize Form',
'Customize Form Field', 'Property Setter', 'Custom Field', 'Custom Script')
data_field_options = ('Email', 'Phone')
data_field_options = ('Email', 'Name', 'Phone')
def copytables(srctype, src, srcfield, tartype, tar, tarfield, srcfields, tarfields=[]):
if not tarfields:

View file

@ -11,11 +11,12 @@ from frappe.model import default_fields, table_fields
from frappe.model.naming import set_new_name
from frappe.model.utils.link_count import notify_link_count
from frappe.modules import load_doctype_module
from frappe.model import display_fieldtypes, data_fieldtypes
from frappe.model import display_fieldtypes
from frappe.utils.password import get_decrypted_password, set_encrypted_password
from frappe.utils import (cint, flt, now, cstr, strip_html, getdate, get_datetime, to_timedelta,
from frappe.utils import (cint, flt, now, cstr, strip_html,
sanitize_html, sanitize_email, cast_fieldtype)
from frappe.utils.html_utils import unescape_html
from bs4 import BeautifulSoup
max_positive_value = {
'smallint': 2 ** 15,
@ -288,7 +289,7 @@ class BaseDocument(object):
if k in default_fields:
del doc[k]
for key in ("_user_tags", "__islocal", "__onload", "_liked_by", "__run_link_triggers"):
for key in ("_user_tags", "__islocal", "__onload", "_liked_by", "__run_link_triggers", "__unsaved"):
if self.get(key):
doc[key] = self.get(key)
@ -564,13 +565,20 @@ class BaseDocument(object):
for data_field in self.meta.get_data_fields():
data = self.get(data_field.fieldname)
data_field_options = data_field.get("options")
old_fieldtype = data_field.get("oldfieldtype")
if old_fieldtype and old_fieldtype != "Data":
continue
if data_field_options == "Email":
if (self.owner in STANDARD_USERS) and (data in STANDARD_USERS):
return
continue
for email_address in frappe.utils.split_emails(data):
frappe.utils.validate_email_address(email_address, throw=True)
if data_field_options == "Name":
frappe.utils.validate_name(data, throw=True)
if data_field_options == "Phone":
frappe.utils.validate_phone_number(data, throw=True)
@ -678,7 +686,7 @@ class BaseDocument(object):
# doesn't look like html so no need
continue
elif "<!-- markdown -->" in value and not ("<script" in value or "javascript:" in value):
elif "<!-- markdown -->" in value and not bool(BeautifulSoup(value, "html.parser").find()):
# should be handled separately via the markdown converter function
continue

View file

@ -74,11 +74,9 @@ def set_user_and_static_default_values(doc):
def get_user_default_value(df, defaults, doctype_user_permissions, allowed_records, default_doc):
# don't set defaults for "User" link field using User Permissions!
if df.fieldtype == "Link" and df.options != "User":
# 1 - look in user permissions only for document_type==Setup
# We don't want to include permissions of transactions to be used for defaults.
if (frappe.get_meta(df.options).document_type=="Setup"
and not df.ignore_user_permissions and default_doc):
return default_doc
# If user permission has Is Default enabled or single-user permission has found against respective doctype.
if (not df.ignore_user_permissions and default_doc):
return default_doc
# 2 - Look in user defaults
user_default = defaults.get(df.fieldname)

View file

@ -329,6 +329,10 @@ class Document(BaseDocument):
self.update_children()
self.run_post_save_methods()
# clear unsaved flag
if hasattr(self, "__unsaved"):
delattr(self, "__unsaved")
return self
def copy_attachments_from_amended_from(self):

View file

@ -1,6 +1,10 @@
import frappe
def execute():
frappe.reload_doc("contacts", "doctype", "contact_email")
frappe.reload_doc("contacts", "doctype", "contact_phone")
frappe.reload_doc("contacts", "doctype", "contact")
contact_details = frappe.db.sql("""
SELECT
`name`, `email_id`, `phone`, `mobile_no`, `modified_by`, `creation`, `modified`
@ -10,10 +14,6 @@ def execute():
and `tabContact Email`.email_id=`tabContact`.email_id)
""", as_dict=True)
frappe.reload_doc("contacts", "doctype", "contact_email")
frappe.reload_doc("contacts", "doctype", "contact_phone")
frappe.reload_doc("contacts", "doctype", "contact")
email_values = []
phone_values = []
for count, contact_detail in enumerate(contact_details):

View file

@ -441,18 +441,16 @@ frappe.PrintFormatBuilder = Class.extend({
});
},
setup_field_settings: function() {
this.page.main.find(".field-settings").on("click", () => {
var field = $(this).parent();
this.page.main.find(".field-settings").on("click", e => {
const field = $(e.currentTarget).parent();
// new dialog
var d = new frappe.ui.Dialog({
title: "Set Properties",
fields: [
{
label:__("Label"),
fieldname:"label",
fieldtype:"Data"
label: __("Label"),
fieldname: "label",
fieldtype: "Data"
},
{
label: __("Align Value"),
@ -485,7 +483,7 @@ frappe.PrintFormatBuilder = Class.extend({
});
// set current value
if(field.attr('data-align')) {
if (field.attr('data-align')) {
d.set_value('align', field.attr('data-align'));
} else {
d.set_value('align', 'left');

View file

@ -90,6 +90,7 @@
"public/css/font-awesome.css",
"public/css/octicons/octicons.css",
"public/less/desk.less",
"public/less/module.less",
"public/less/flex.less",
"public/less/indicator.less",
"public/less/avatar.less",

View file

@ -10,11 +10,11 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
.appendTo(this.input_area);
this.expanded = false;
this.$expand_button = $(`<button class="margin-top btn btn-xs btn-default">${__('Expand / Collapse')}</button>`).click(() => {
this.$expand_button = $(`<button class="btn btn-xs btn-default">${__('Expand')}</button>`).click(() => {
this.expanded = !this.expanded;
this.refresh_height();
}).insertAfter(this.ace_editor_target);
this.toggle_label();
}).appendTo(this.$input_wrapper);
// styling
this.ace_editor_target.addClass('border rounded');
this.ace_editor_target.css('height', 300);
@ -37,6 +37,11 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
this.editor.resize();
},
toggle_label() {
const button_label = this.expanded ? __('Collapse') : __('Expand');
this.$expand_button.text(button_label);
},
set_language() {
const language_map = {
'Javascript': 'ace/mode/javascript',

View file

@ -96,6 +96,9 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({
if(this.df.options == 'Phone') {
this.df.invalid = !validate_phone(v);
return v;
} else if (this.df.options == 'Name') {
this.df.invalid = !validate_name(v);
return v;
} else if(this.df.options == 'Email') {
var email_list = frappe.utils.split_emails(v);
if (!email_list) {

View file

@ -69,7 +69,7 @@ frappe.ui.form.Sidebar = Class.extend({
},
refresh: function() {
if(this.frm.doc.__islocal) {
if (this.frm.doc.__islocal) {
this.sidebar.toggle(false);
} else {
this.sidebar.toggle(true);
@ -81,12 +81,34 @@ frappe.ui.form.Sidebar = Class.extend({
}
this.frm.viewers.refresh();
this.frm.tags && this.frm.tags.refresh(this.frm.get_docinfo().tags);
this.sidebar.find(".modified-by").html(__("{0} edited this {1}",
["<strong>" + frappe.user.full_name(this.frm.doc.modified_by) + "</strong>",
"<br>" + comment_when(this.frm.doc.modified)]));
this.sidebar.find(".created-by").html(__("{0} created this {1}",
["<strong>" + frappe.user.full_name(this.frm.doc.owner) + "</strong>",
"<br>" + comment_when(this.frm.doc.creation)]));
if (this.frm.doc.route && cint(frappe.boot.website_tracking_enabled)) {
let route = this.frm.doc.route;
frappe.utils.get_page_view_count(route).then((res) => {
this.sidebar
.find(".pageview-count")
.html(
__("{0} Page Views", [String(res.message).bold()])
);
});
}
this.sidebar
.find(".modified-by")
.html(
__("{0} edited this {1}", [
frappe.user.full_name(this.frm.doc.modified_by).bold(),
"<br>" + comment_when(this.frm.doc.modified),
])
);
this.sidebar
.find(".created-by")
.html(
__("{0} created this {1}", [
frappe.user.full_name(this.frm.doc.owner).bold(),
"<br>" + comment_when(this.frm.doc.creation),
])
);
this.refresh_like();
frappe.ui.form.set_user_image(this.frm);

View file

@ -105,6 +105,7 @@
</li>
</ul>
<ul class="list-unstyled sidebar-menu text-muted">
<li class="pageview-count"></li>
<li class="modified-by"></li>
<li class="created-by"></li>
</ul>

View file

@ -137,10 +137,8 @@ $.extend(frappe.model, {
// don't set defaults for "User" link field using User Permissions!
if (df.fieldtype==="Link" && df.options!=="User") {
// 1 - look in user permissions for document_type=="Setup".
// We don't want to include permissions of transactions to be used for defaults.
if (df.linked_document_type==="Setup"
&& has_user_permissions && default_doc) {
// If user permission has Is Default enabled or single-user permission has found against respective doctype.
if (has_user_permissions && default_doc) {
return default_doc;
}
@ -161,10 +159,6 @@ $.extend(frappe.model, {
user_default = frappe.boot.user.last_selected_values[df.options];
}
if (!user_default && default_doc) {
user_default = default_doc;
}
var is_allowed_user_default = user_default &&
(!has_user_permissions || allowed_records.includes(user_default));

View file

@ -352,3 +352,9 @@ frappe.utils.new_auto_repeat_prompt = function(frm) {
__('Save')
);
}
frappe.utils.get_page_view_count = function(route) {
return frappe.call("frappe.website.doctype.web_page_view.web_page_view.get_page_view_count", {
path: route
});
};

View file

@ -48,6 +48,10 @@ window.validate_phone = function(txt) {
return frappe.utils.validate_type(txt, "phone");
};
window.validate_name = function(txt) {
return frappe.utils.validate_type(txt, "name");
};
window.nth = function(number) {
number = cint(number);
var s = 'th';
@ -73,4 +77,4 @@ window.has_common = function(list1, list2) {
if(in_list(list2, list1[i]))return true;
}
return false;
};
};

View file

@ -13,7 +13,7 @@ function prettyDate(date, mini) {
// Return short format of time difference
if (day_diff == 0) {
if (diff < 60) {
return __("Now");
return __("now");
} else if (diff < 3600) {
return __("{0} m", [Math.floor(diff / 60)]);
} else if (diff < 86400) {
@ -21,20 +21,20 @@ function prettyDate(date, mini) {
}
} else {
if (day_diff < 7) {
return __("{0} D", [day_diff]);
return __("{0} d", [day_diff]);
} else if (day_diff < 31) {
return __("{0} W", [Math.ceil(day_diff / 7)]);
return __("{0} w", [Math.ceil(day_diff / 7)]);
} else if (day_diff < 365) {
return __("{0} M", [Math.ceil(day_diff / 30)]);
} else {
return __("{0} Y", [Math.ceil(day_diff / 365)]);
return __("{0} y", [Math.ceil(day_diff / 365)]);
}
}
} else {
// Return long format of time difference
if (day_diff == 0) {
if (diff < 60) {
return __("Just now");
return __("just now");
} else if (diff < 120) {
return __("1 minute ago");
} else if (diff < 3600) {
@ -46,7 +46,7 @@ function prettyDate(date, mini) {
}
} else {
if (day_diff == 1) {
return __("Yesterday");
return __("yesterday");
} else if (day_diff < 7) {
return __("{0} days ago", [day_diff]);
} else if (day_diff < 14) {

View file

@ -237,6 +237,9 @@ Object.assign(frappe.utils, {
case "phone":
regExp = /^([0-9\ \+\_\-\,\.\*\#\(\)]){1,20}$/;
break;
case "name":
regExp = /^[\w][\w'-]*([ \w][\w'-]+)*$/;
break;
case "number":
regExp = /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/;
break;
@ -745,7 +748,36 @@ Object.assign(frappe.utils, {
});
return $el;
}
},
get_browser() {
var ua = navigator.userAgent,
tem,
M =
ua.match(
/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i
) || [];
if (/trident/i.test(M[1])) {
tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
return { name: "IE", version: tem[1] || "" };
}
if (M[1] === "Chrome") {
tem = ua.match(/\bOPR|Edge\/(\d+)/);
if (tem != null) {
return { name: "Opera", version: tem[1] };
}
}
M = M[2]
? [M[1], M[2]]
: [navigator.appName, navigator.appVersion, "-?"];
if ((tem = ua.match(/version\/(\d+)/i)) != null) {
M.splice(1, 1, tem[1]);
}
return {
name: M[0],
version: M[1],
};
},
});
// Array de duplicate

View file

@ -6,9 +6,10 @@ frappe.breadcrumbs = {
preferred: {
"File": "",
"Video": "",
"Dashboard": "Customization",
"Dashboard Chart": "Customization",
"Dashboard Chart Source": "Customization",
"Dashboard Chart Source": "Customization"
},
module_map: {

View file

@ -20,7 +20,8 @@ frappe.views.CommunicationComposer = Class.extend({
primary_action: function() {
me.delete_saved_draft();
me.send_action();
}
},
minimizable: true
});
['recipients', 'cc', 'bcc'].forEach(field => {

View file

@ -770,6 +770,7 @@ h6.uppercase, .h6.uppercase {
.help-box {
margin-top: 3px;
margin-bottom: 6px;
}
pre {

View file

@ -0,0 +1,147 @@
@import "variables.less";
.module-head {
padding: 15px 30px;
border-bottom: 1px solid @light-border-color;
}
.module-head h1 {
padding: 0px;
margin: 0px;
}
.module-body {
padding: 0px 15px;
.section-head {
margin-bottom: 15px;
margin-top: 0px;
}
}
.module-section {
border-bottom: 1px solid @light-border-color;
.module-section-link {
line-height: 1.5em;
// font-size: 14px;
}
}
.module-section-column {
padding: 30px;
}
@media(min-width: @screen-xs) {
.module-section:nth-child(even) {
background-color: @light-bg;
}
.module-section:last-child {
border-bottom: none;
}
}
@media(max-width: @screen-sm) {
.module-body {
margin-top: 15px;
border-top: 1px solid @border-color;
}
}
@media(max-width: @screen-xs) {
.module-body {
margin-top: 0;
border-top: 1px solid transparent;
}
}
@media(max-width: @screen-xs) {
.module-section {
border: none;
}
.module-section-column {
border-bottom: 1px solid @light-border-color;
}
.module-section-column:nth-child(even) {
background-color: @light-bg;
}
.module-section:last-child .module-section-column:last-child {
border-bottom: none;
}
}
.module-item {
margin: 0px;
padding: 7px;
font-weight: 400;
border-bottom: 1px solid @border-color;
cursor: pointer;
transition: 0.2s;
-webkit-transition: 0.2s;
}
.module-item h4 {
display: inline-block;
}
.module-item .module-item-description {
margin-top: -5px;
}
.module-item .badge {
margin-top: -2px;
margin-left: 3px;
}
.module-item:hover, .module-item:focus {
background-color: @panel-bg;
}
.module-item:last-child {
border: none;
}
.module-link.active .icon-chevron-right {
margin-top: 4px;
display: block !important;
}
.module-item-progress {
margin-bottom: 10px;
height: 17px;
}
.module-item-progress-total {
height: 7px;
background-color: #999999;
width: 0px;
}
.module-item-progress-open {
height: 7px;
background-color: red;
width: 0px;
}
@media(max-width: @screen-xs) {
body[data-route^="Module"] {
.page-title {
width: 100%;
}
.page-actions {
display: none !important;
}
.layout-main-section {
border-bottom: 0px;
}
}
}

View file

@ -273,7 +273,8 @@ body[data-route^="Module"] .main-menu {
}
.layout-side-section .form-sidebar {
.modified-by {
.modified-by,
.pageview-count {
margin-bottom: 15px;
}
}

View file

@ -81,13 +81,29 @@ def validate_phone_number(phone_number, throw=False):
return False
phone_number = phone_number.strip()
match = re.match("([0-9\ \+\_\-\,\.\*\#\(\)]){1,20}$", phone_number)
match = re.match(r"([0-9\ \+\_\-\,\.\*\#\(\)]){1,20}$", phone_number)
if not match and throw:
frappe.throw(frappe._("{0} is not a valid Phone Number").format(phone_number), frappe.InvalidPhoneNumberError)
return bool(match)
def validate_name(name, throw=False):
"""Returns True if the name is valid
valid names may have unicode and ascii characters, dash, quotes, numbers
anything else is considered invalid
"""
if not name:
return False
name = name.strip()
match = re.match(r"^[\w][\w\'\-]*([ \w][\w\'\-]+)*$", name)
if not match and throw:
frappe.throw(frappe._("{0} is not a valid Name").format(name), frappe.InvalidNameError)
return bool(match)
def validate_email_address(email_str, throw=False):
"""Validates the email string"""
email = email_str = (email_str or "").strip()

View file

@ -174,9 +174,12 @@ def parse_latest_non_beta_release(response):
Returns
json : json object pertaining to the latest non-beta release
"""
for release in response:
if release['prerelease'] == True: continue
return release
version_list = [release.get('tag_name').strip('v') for release in response if not release.get('prerelease')]
if version_list:
return sorted(version_list, key=Version, reverse=True)[0]
return None
def check_release_on_github(app):
# Check if repo remote is on github
@ -199,12 +202,11 @@ def check_release_on_github(app):
org_name = remote_url.split('/')[3]
r = requests.get('https://api.github.com/repos/{}/{}/releases'.format(org_name, app))
if r.status_code == 200 and r.json():
if r.ok:
lastest_non_beta_release = parse_latest_non_beta_release(r.json())
return Version(lastest_non_beta_release['tag_name'].strip('v')), org_name
else:
# In case of an improper response or if there are no releases
return None
return Version(lastest_non_beta_release), org_name
# In case of an improper response or if there are no releases
return None
def add_message_to_redis(update_json):
# "update-message" will store the update message string

View file

@ -5,7 +5,7 @@ from logging.handlers import RotatingFileHandler
from six import text_type
default_log_level = logging.DEBUG
LOG_FILENAME = '../logs/frappe.log'
LOG_FILENAME = '../logs/{}-frappe.log'.format(frappe.local.site)
def get_logger(module, with_more_info=True):
if module in frappe.loggers:
@ -57,4 +57,3 @@ def set_log_level(level):
'''Use this method to set log level to something other than the default DEBUG'''
frappe.log_level = getattr(logging, (level or '').upper(), None) or default_log_level
frappe.loggers = {}

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestWebPageView(unittest.TestCase):
pass

View file

@ -0,0 +1,8 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Web Page View', {
// refresh: function(frm) {
// }
});

View file

@ -0,0 +1,75 @@
{
"actions": [],
"creation": "2020-04-15 22:54:46.009703",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"path",
"referrer",
"browser",
"browser_version",
"date"
],
"fields": [
{
"fieldname": "path",
"fieldtype": "Data",
"label": "Path",
"set_only_once": 1
},
{
"fieldname": "referrer",
"fieldtype": "Data",
"label": "Referrer",
"search_index": 1,
"set_only_once": 1
},
{
"fieldname": "browser",
"fieldtype": "Data",
"label": "Browser",
"search_index": 1,
"set_only_once": 1
},
{
"fieldname": "browser_version",
"fieldtype": "Data",
"label": "Browser Version",
"set_only_once": 1
},
{
"fieldname": "date",
"fieldtype": "Datetime",
"label": "Date",
"set_only_once": 1
}
],
"in_create": 1,
"links": [],
"modified": "2020-04-15 23:31:27.517793",
"modified_by": "Administrator",
"module": "Website",
"name": "Web Page View",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "path",
"track_changes": 1
}

View file

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class WebPageView(Document):
pass
@frappe.whitelist(allow_guest=True)
def make_view_log(path, referrer=None, browser=None, version=None, url=None, user_tz=None):
request_dict = frappe.request.__dict__
user_agent = request_dict.get('environ', {}).get('HTTP_USER_AGENT')
is_unique = True
if referrer.startswith(url):
is_unique = False
if path.startswith('/'):
path = path[1:]
if is_tracking_enabled():
view = frappe.new_doc("Web Page View")
view.path = path
view.referrer = referrer
view.browser = browser
view.browser_version = version
view.time_zone = user_tz
view.user_agent = user_agent
view.is_unique = is_unique
view.insert(ignore_permissions=True)
return
@frappe.whitelist()
def get_page_view_count(path):
return frappe.db.count("Web Page View", filters={'path': path})
def is_tracking_enabled():
return frappe.db.get_value("Website Settings", "Website Settings", "enable_view_tracking")

View file

@ -3,7 +3,6 @@
"allow_guest_to_view": 1,
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:title",
"beta": 1,
"creation": "2020-03-16 15:28:03.828741",
"doctype": "DocType",
@ -117,7 +116,7 @@
"has_web_view": 1,
"is_published_field": "published",
"links": [],
"modified": "2020-04-19 12:25:48.014935",
"modified": "2020-04-22 00:54:23.413077",
"modified_by": "Administrator",
"module": "Website",
"name": "Web View",

View file

@ -56,6 +56,10 @@ frappe.ui.form.on('Website Settings', {
});
},
enable_view_tracking: function(frm) {
frappe.boot.website_tracking_enabled = frm.doc.enable_view_tracking;
},
set_parent_options: function(frm, doctype, name) {
var item = frappe.get_doc(doctype, name);
if(item.parentfield === "top_bar_items") {

View file

@ -33,6 +33,7 @@
"footer_items",
"hide_footer_signup",
"integrations",
"enable_view_tracking",
"enable_google_indexing",
"authorize_api_indexing_access",
"indexing_refresh_token",
@ -196,7 +197,7 @@
"collapsible": 1,
"fieldname": "integrations",
"fieldtype": "Section Break",
"label": "Google Integrations"
"label": "Integrations"
},
{
"description": "Add Google Analytics ID: eg. UA-89XXX57-1. Please search help on Google Analytics for more information.",
@ -330,6 +331,12 @@
"fieldtype": "Button",
"label": "Authorize API Indexing Access"
},
{
"default": "0",
"fieldname": "enable_view_tracking",
"fieldtype": "Check",
"label": "Enable In App Website Tracking"
},
{
"default": "Standard",
"fieldname": "footer_type",
@ -364,7 +371,7 @@
"issingle": 1,
"links": [],
"max_attachments": 10,
"modified": "2020-04-21 16:46:59.947403",
"modified": "2020-04-21 12:37:44.070662",
"modified_by": "Administrator",
"module": "Website",
"name": "Website Settings",

View file

@ -118,7 +118,7 @@ def get_website_settings():
for k in ["banner_html", "brand_html", "copyright", "twitter_share_via",
"facebook_share", "google_plus_one", "twitter_share", "linked_in_share",
"disable_signup", "hide_footer_signup", "head_html", "title_prefix",
"navbar_search"]:
"navbar_search", "enable_view_tracking"]:
if hasattr(settings, k):
context[k] = settings.get(k)

View file

@ -9,7 +9,7 @@
<span class='indicator blue password-box'>{{ _("Reset Password") if frappe.db.get_default('company') else _("Set Password")}}</span>
</div>
<form id="reset-password">
<div class="form-group">
<div class="form-group" style="display: none;">
<input id="old_password" type="password"
class="form-control" placeholder="{{ _("Old Password") }}">
</div>
@ -32,8 +32,8 @@
<script>
frappe.ready(function() {
if(frappe.utils.get_url_arg("key")) {
$("#old_password").parent().toggle(false);
if(!frappe.utils.get_url_arg("key")) {
$("#old_password").parent().toggle();
}
if(frappe.utils.get_url_arg("password_expired")) {
@ -57,12 +57,10 @@ frappe.ready(function() {
}
if(!args.old_password && !args.key) {
frappe.msgprint("{{ _("Old Password Required.") }}");
return;
frappe.msgprint(__("Old Password Required."));
}
if(!args.new_password) {
frappe.msgprint("{{ _("New Password Required.") }}");
return;
frappe.msgprint(__("New Password Required."));
}
frappe.call({
type: "POST",
@ -71,19 +69,24 @@ frappe.ready(function() {
args: args,
statusCode: {
401: function() {
$('.page-card-head .indicator').removeClass().addClass('indicator red')
.text("{{ _('Invalid Password') }}");
$(".page-card-head .indicator").removeClass().addClass("indicator red").text(__("Invalid Password"));
},
410: function({ responseJSON }) {
const title = __("Invalid Link");
const message = responseJSON.message;
$(".page-card-head .indicator").removeClass().addClass("indicator grey").text(title);
frappe.msgprint({ title: title, message: message, clear: true });
},
200: function(r) {
$("input").val("");
strength_indicator.addClass('hidden');
strength_message.addClass('hidden');
$('.page-card-head .indicator')
.removeClass().addClass('indicator green')
.html("{{ _('Password Updated') }}");
strength_indicator.addClass("hidden");
strength_message.addClass("hidden");
$(".page-card-head .indicator")
.removeClass().addClass("indicator blue")
.html(__("Status Updated"));
if(r.message) {
frappe.msgprint({
message: "{{ _("Password Updated") }}",
message: __("Password Updated"),
// password is updated successfully
// clear any server message
clear: true

View file

@ -12,3 +12,19 @@ ga('create', '{{ google_analytics_id }}', 'auto');
ga('send', 'pageview');
// End Google Analytics
{%- endif %}
{% if enable_view_tracking %}
if (navigator.doNotTrack != 1) {
frappe.ready(() => {
let browser = frappe.utils.get_browser();
frappe.call("frappe.website.doctype.web_page_view.web_page_view.make_view_log", {
path: location.pathname,
referrer: document.referrer,
browser: browser.name,
version: browser.version,
url: location.origin,
user_tz: Intl.DateTimeFormat().resolvedOptions().timeZone
})
})
}
{% endif %}

View file

@ -11,6 +11,7 @@ const buble = require('rollup-plugin-buble');
const { terser } = require('rollup-plugin-terser');
const vue = require('rollup-plugin-vue');
const frappe_html = require('./frappe-html-plugin');
const less_loader = require('./less-loader');
const production = process.env.FRAPPE_ENV === 'production';
@ -116,6 +117,7 @@ function get_rollup_options_for_css(output_file, input_files) {
// less -> css
postcss({
extract: output_path,
loaders: [less_loader],
use: [
['less', {
// import other less/css files starting from these folders
@ -130,7 +132,8 @@ function get_rollup_options_for_css(output_file, input_files) {
path.resolve(bench_path, '**/*.scss'),
path.resolve(bench_path, '**/*.css')
],
minimize: minimize_css
minimize: minimize_css,
sourceMap: output_file.startsWith('css/') && !production
})
];

54
rollup/less-loader.js Normal file
View file

@ -0,0 +1,54 @@
const pify = require('pify');
const importCwd = require('import-cwd');
const path = require('path');
const getFileName = filepath => path.basename(filepath);
function loadModule(moduleId) {
// Trying to load module normally (relative to plugin directory)
try {
return require(moduleId);
} catch (_) {
// Ignore error
}
// Then, trying to load it relative to CWD
return importCwd.silent(moduleId);
}
module.exports = {
name: 'less',
test: /\.less$/,
async process({
code
}) {
const less = loadModule('less');
if (!less) {
throw new Error('You need to install "less" packages in order to process Less files');
}
let {
css,
map,
imports
} = await pify(less.render.bind(less))(code, {
...this.options,
sourceMap: this.sourceMap && { outputSourceFiles: true },
filename: this.id
});
for (const dep of imports) {
this.dependencies.add(dep);
}
if (map) {
map = JSON.parse(map);
map.sources = map.sources.map(source => getFileName(source));
}
return {
code: css,
map
};
}
};