diff --git a/.eslintrc b/.eslintrc
index eef33ec8a0..69c731b079 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -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,
diff --git a/.travis.yml b/.travis.yml
index 0296f38527..219d16c74f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,14 +7,19 @@ addons:
- test_site_producer
mariadb: 10.3
postgresql: 9.5
+ chrome: stable
git:
depth: 1
cache:
- - pip
- - npm
- - yarn
+ pip: true
+ npm: true
+ yarn: true
+ directories:
+ # we also need to cache folder with Cypress binary
+ # https://docs.cypress.io/guides/guides/continuous-integration.html#Caching
+ - ~/.cache
matrix:
include:
diff --git a/cypress/integration/depends_on.js b/cypress/integration/depends_on.js
index 375b690fb2..93417014c5 100644
--- a/cypress/integration/depends_on.js
+++ b/cypress/integration/depends_on.js
@@ -1,8 +1,4 @@
context('Depends On', () => {
- beforeEach(() => {
- cy.login();
- return cy.new_form('Test Depends On');
- });
before(() => {
cy.login();
cy.visit('/desk#workspace/Website');
diff --git a/cypress/integration/file_uploader.js b/cypress/integration/file_uploader.js
index f4ef2a19f0..5e9a264189 100644
--- a/cypress/integration/file_uploader.js
+++ b/cypress/integration/file_uploader.js
@@ -50,7 +50,7 @@ context('FileUploader', () => {
open_upload_dialog();
cy.get_open_dialog().find('a:contains("web link")').click();
- cy.get_open_dialog().find('.file-web-link input').type('https://github.com');
+ cy.get_open_dialog().find('.file-web-link input').type('https://github.com', { delay: 100, force: true });
cy.server();
cy.route('POST', '/api/method/upload_file').as('upload_file');
cy.get_open_dialog().find('.btn-primary').click();
diff --git a/cypress/integration/form.js b/cypress/integration/form.js
index a622a66e13..23fc57fc57 100644
--- a/cypress/integration/form.js
+++ b/cypress/integration/form.js
@@ -6,14 +6,17 @@ context('Form', () => {
return frappe.call("frappe.tests.ui_test_helpers.create_contact_records");
});
});
- beforeEach(() => {
- cy.visit('/desk#workspace/Website');
- });
it('create a new form', () => {
cy.visit('/desk#Form/ToDo/New ToDo 1');
cy.fill_field('description', 'this is a test todo', 'Text Editor').blur();
cy.get('.page-title').should('contain', 'Not Saved');
+ cy.server();
+ cy.route({
+ method: 'POST',
+ url: 'api/method/frappe.desk.form.save.savedocs'
+ }).as('form_save');
cy.get('.primary-action').click();
+ cy.wait('@form_save').its('status').should('eq', 200);
cy.visit('/desk#List/ToDo');
cy.location('hash').should('eq', '#List/ToDo/List');
cy.get('h1').should('be.visible').and('contain', 'To Do');
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 2d31d9a988..7816d5526f 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -186,7 +186,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => {
if (fieldtype === 'Select') {
cy.get('@input').select(value);
} else {
- cy.get('@input').type(value, { waitForAnimations: false });
+ cy.get('@input').type(value, { waitForAnimations: false, force: true });
}
return cy.get('@input');
});
diff --git a/frappe/auth.py b/frappe/auth.py
index dba8b05a62..1353acf10f 100644
--- a/frappe/auth.py
+++ b/frappe/auth.py
@@ -219,7 +219,10 @@ class LoginManager:
user = frappe.db.get_value("User", filters={"username": user}, fieldname="name") or user
self.check_if_enabled(user)
- self.user = self.check_password(user, pwd)
+ if not frappe.form_dict.get('tmp_id'):
+ self.user = self.check_password(user, pwd)
+ else:
+ self.user = user
def force_user_to_reset_password(self):
if not self.user:
diff --git a/frappe/automation/desk_page/tools/tools.json b/frappe/automation/desk_page/tools/tools.json
index 235498724d..2164a4ce38 100644
--- a/frappe/automation/desk_page/tools/tools.json
+++ b/frappe/automation/desk_page/tools/tools.json
@@ -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",
diff --git a/frappe/boot.py b/frappe/boot.py
index e6d1199b19..9d5dbe1909 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -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()
@@ -268,4 +270,18 @@ def get_success_action():
return frappe.get_all("Success Action", fields=["*"])
def get_link_preview_doctypes():
- return [d.name for d in frappe.db.get_all('DocType', {'show_preview_popup': 1})]
\ No newline at end of file
+ from frappe.utils import cint
+
+ link_preview_doctypes = [d.name for d in frappe.db.get_all('DocType', {'show_preview_popup': 1})]
+ customizations = frappe.get_all("Property Setter",
+ fields=['doc_type', 'value'],
+ filters={'property': 'show_preview_popup'}
+ )
+
+ for custom in customizations:
+ if not cint(custom.value) and custom.doc_type in link_preview_doctypes:
+ link_preview_doctypes.remove(custom.doc_type)
+ else:
+ link_preview_doctypes.append(custom.doc_type)
+
+ return link_preview_doctypes
diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py
index ec30fc19b8..da9d67be3b 100644
--- a/frappe/commands/utils.py
+++ b/frappe/commands/utils.py
@@ -522,7 +522,7 @@ def run_ui_tests(context, app, headless=False):
password_env = 'CYPRESS_adminPassword={}'.format(admin_password) if admin_password else ''
# run for headless mode
- run_or_open = 'run --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open'
+ run_or_open = 'run --browser chrome --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open'
command = '{site_env} {password_env} yarn run cypress {run_or_open}'
formatted_command = command.format(site_env=site_env, password_env=password_env, run_or_open=run_or_open)
frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True)
diff --git a/frappe/core/doctype/data_import/test_exporter_new.py b/frappe/core/doctype/data_import/test_exporter_new.py
index 7464d6edc5..eabf371b07 100644
--- a/frappe/core/doctype/data_import/test_exporter_new.py
+++ b/frappe/core/doctype/data_import/test_exporter_new.py
@@ -20,7 +20,7 @@ class TestExporter(unittest.TestCase):
e = Exporter('Web Page', export_fields='All')
csv_array = e.get_csv_array()
header = csv_array[0]
- self.assertEqual(len(header), 24)
+ self.assertEqual(len(header), 28)
def test_exports_selected_fields(self):
diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json
index 4614dd09c4..6d8ee41a5a 100644
--- a/frappe/core/doctype/docfield/docfield.json
+++ b/frappe/core/doctype/docfield/docfield.json
@@ -11,9 +11,9 @@
"label",
"fieldtype",
"fieldname",
- "reqd",
"precision",
"length",
+ "reqd",
"search_index",
"in_list_view",
"in_standard_filter",
@@ -453,7 +453,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-04-15 02:26:03.310781",
+ "modified": "2020-04-19 21:54:13.783908",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index f7c9cbe28a..d922cfe166 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -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) + "
" * 2 + _("Only Options allowed for Data field are:") + "
"
diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py
index 8741101976..b35abfa861 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -1,46 +1,45 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
-from frappe import _
"""
record of files
naming for same name files: file.gif, file-1.gif, file-2.gif etc
"""
-import frappe
-import json
-import os
+from __future__ import unicode_literals
+
import base64
-import re
import hashlib
-import mimetypes
+import imghdr
import io
+import json
+import mimetypes
+import os
+import re
import shutil
+import zipfile
+
import requests
import requests.exceptions
-import imghdr
+from PIL import Image, ImageFile, ImageOps
+from six import PY2, StringIO, string_types, text_type
+from six.moves.urllib.parse import quote, unquote
-from frappe.utils import get_hook_method, get_files_path, random_string, encode, cstr, call_hook_method, cint
-from frappe import _
-from frappe import conf
-from frappe.utils.nestedset import NestedSet
+import frappe
+from frappe import _, conf
from frappe.model.document import Document
-from frappe.utils import strip
-from PIL import Image, ImageOps
-from six import StringIO, string_types
-from six.moves.urllib.parse import unquote, quote
-from six import text_type, PY2
-import zipfile
+from frappe.utils import call_hook_method, cint, cstr, encode, get_files_path, get_hook_method, random_string, strip
+
class MaxFileSizeReachedError(frappe.ValidationError):
pass
-
-class FolderNotEmpty(frappe.ValidationError): pass
+class FolderNotEmpty(frappe.ValidationError):
+ pass
exclude_from_linked_with = True
+ImageFile.LOAD_TRUNCATED_IMAGES = True
class File(Document):
@@ -697,7 +696,7 @@ def remove_file(fid=None, attached_to_doctype=None, attached_to_name=None, from_
def get_max_file_size():
- return conf.get('max_file_size') or 10485760
+ return cint(conf.get('max_file_size')) or 10485760
def remove_all(dt, dn, from_delete=False):
@@ -714,7 +713,10 @@ def has_permission(doc, ptype=None, user=None):
has_access = False
user = user or frappe.session.user
- if not doc.is_private or doc.owner == user or user == 'Administrator':
+ if ptype == 'create':
+ has_access = frappe.has_permission('File', 'create', user=user)
+
+ if not doc.is_private or doc.owner in [user, 'Guest'] or user == 'Administrator':
has_access = True
if doc.attached_to_doctype and doc.attached_to_name:
diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js
index b17548d994..b8e16bfe25 100644
--- a/frappe/core/doctype/user/user.js
+++ b/frappe/core/doctype/user/user.js
@@ -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({
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 7837c90d2b..8370af6808 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -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:
diff --git a/frappe/core/doctype/user_permission/test_user_permission.py b/frappe/core/doctype/user_permission/test_user_permission.py
index 870d3c7029..5ccc8752cf 100644
--- a/frappe/core/doctype/user_permission/test_user_permission.py
+++ b/frappe/core/doctype/user_permission/test_user_permission.py
@@ -9,7 +9,8 @@ import unittest
class TestUserPermission(unittest.TestCase):
def setUp(self):
- frappe.db.sql("DELETE FROM `tabUser Permission` WHERE `user`='test_bulk_creation_update@example.com'")
+ frappe.db.sql("""DELETE FROM `tabUser Permission`
+ WHERE `user` in ('test_bulk_creation_update@example.com', 'test_user_perm1@example.com')""")
def test_default_user_permission_validation(self):
user = create_user('test_default_permission@example.com')
@@ -20,6 +21,26 @@ class TestUserPermission(unittest.TestCase):
param = get_params(user, 'User', perm_user.name, is_default=1)
self.assertRaises(frappe.ValidationError, add_user_permissions, param)
+ def test_default_user_permission(self):
+ frappe.set_user('Administrator')
+ user = create_user('test_user_perm1@example.com', 'Website Manager')
+ for category in ['general', 'public']:
+ if not frappe.db.exists('Blog Category', category):
+ frappe.get_doc({'doctype': 'Blog Category',
+ 'category_name': category, 'title': category}).insert()
+
+ param = get_params(user, 'Blog Category', 'general', is_default=1)
+ add_user_permissions(param)
+
+ param = get_params(user, 'Blog Category', 'public')
+ add_user_permissions(param)
+
+ frappe.set_user('test_user_perm1@example.com')
+ doc = frappe.new_doc("Blog Post")
+
+ self.assertEquals(doc.blog_category, 'general')
+ frappe.set_user('Administrator')
+
def test_apply_to_all(self):
''' Create User permission for User having access to all applicable Doctypes'''
user = create_user('test_bulk_creation_update@example.com')
@@ -88,7 +109,7 @@ class TestUserPermission(unittest.TestCase):
self.assertIsNone(removed_applicable_second)
self.assertEquals(is_created, 1)
-def create_user(email):
+def create_user(email, role="System Manager"):
''' create user with role system manager '''
if frappe.db.exists('User', email):
return frappe.get_doc('User', email)
@@ -96,7 +117,7 @@ def create_user(email):
user = frappe.new_doc('User')
user.email = email
user.first_name = email.split("@")[0]
- user.add_roles("System Manager")
+ user.add_roles(role)
return user
def get_params(user, doctype, docname, is_default=0, applicable=None):
diff --git a/frappe/website/doctype/web_view_item/__init__.py b/frappe/core/doctype/video/__init__.py
similarity index 100%
rename from frappe/website/doctype/web_view_item/__init__.py
rename to frappe/core/doctype/video/__init__.py
diff --git a/frappe/core/doctype/video/test_video.py b/frappe/core/doctype/video/test_video.py
new file mode 100644
index 0000000000..0bed1e98d6
--- /dev/null
+++ b/frappe/core/doctype/video/test_video.py
@@ -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
diff --git a/frappe/core/doctype/video/video.js b/frappe/core/doctype/video/video.js
new file mode 100644
index 0000000000..36ea240a36
--- /dev/null
+++ b/frappe/core/doctype/video/video.js
@@ -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) {
+
+ // }
+});
diff --git a/frappe/core/doctype/video/video.json b/frappe/core/doctype/video/video.json
new file mode 100644
index 0000000000..26a407c05c
--- /dev/null
+++ b/frappe/core/doctype/video/video.json
@@ -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
+}
\ No newline at end of file
diff --git a/frappe/website/doctype/web_view_item/web_view_item.py b/frappe/core/doctype/video/video.py
similarity index 89%
rename from frappe/website/doctype/web_view_item/web_view_item.py
rename to frappe/core/doctype/video/video.py
index cc440305c0..fdbd3a1abe 100644
--- a/frappe/website/doctype/web_view_item/web_view_item.py
+++ b/frappe/core/doctype/video/video.py
@@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
-class WebViewItem(Document):
+class Video(Document):
pass
diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py
index c8a2352968..4a94de4ace 100644
--- a/frappe/core/page/background_jobs/background_jobs.py
+++ b/frappe/core/page/background_jobs/background_jobs.py
@@ -28,6 +28,7 @@ def get_info(show_failed=False):
if j.kwargs.get('site')==frappe.local.site:
jobs.append({
'job_name': j.kwargs.get('kwargs', {}).get('playbook_method') \
+ or j.kwargs.get('kwargs', {}).get('job_type') \
or str(j.kwargs.get('job_name')),
'status': j.get_status(), 'queue': name,
'creation': format_datetime(convert_utc_to_user_timezone(j.created_at)),
diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json
index b274033f80..394f38b56c 100644
--- a/frappe/custom/doctype/custom_field/custom_field.json
+++ b/frappe/custom/doctype/custom_field/custom_field.json
@@ -41,6 +41,7 @@
"in_list_view",
"in_standard_filter",
"in_global_search",
+ "in_preview",
"bold",
"report_hide",
"search_index",
@@ -371,12 +372,18 @@
"fieldname": "allow_in_quick_entry",
"fieldtype": "Check",
"label": "Allow in Quick Entry"
+ },
+ {
+ "default": "0",
+ "fieldname": "in_preview",
+ "fieldtype": "Check",
+ "label": "In Preview"
}
],
"icon": "fa fa-glass",
"idx": 1,
"links": [],
- "modified": "2020-03-16 14:52:43.954709",
+ "modified": "2020-04-10 11:57:10.392218",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",
diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json
index 51a5c0b85f..cd57aa23fe 100644
--- a/frappe/custom/doctype/customize_form/customize_form.json
+++ b/frappe/custom/doctype/customize_form/customize_form.json
@@ -20,6 +20,7 @@
"track_views",
"allow_auto_repeat",
"allow_import",
+ "show_preview_popup",
"image_view",
"column_break_5",
"title_field",
@@ -203,6 +204,12 @@
"depends_on": "doc_type",
"fieldname": "section_break_23",
"fieldtype": "Section Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_preview_popup",
+ "fieldtype": "Check",
+ "label": "Show Preview Popup"
}
],
"hide_toolbar": 1,
@@ -210,7 +217,7 @@
"idx": 1,
"issingle": 1,
"links": [],
- "modified": "2020-03-27 15:06:35.443861",
+ "modified": "2020-04-10 12:16:01.320411",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index 7d081953dd..9efa555152 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -32,6 +32,7 @@ doctype_properties = {
'track_views': 'Check',
'allow_auto_repeat': 'Check',
'allow_import': 'Check',
+ 'show_preview_popup': 'Check',
'email_append_to': 'Check',
'subject_field': 'Data',
'sender_field': 'Data'
@@ -53,6 +54,7 @@ docfield_properties = {
'in_list_view': 'Check',
'in_standard_filter': 'Check',
'in_global_search': 'Check',
+ 'in_preview': 'Check',
'bold': 'Check',
'hidden': 'Check',
'collapsible': 'Check',
diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json
index 350d159541..d7887cf8bd 100644
--- a/frappe/custom/doctype/customize_form_field/customize_form_field.json
+++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json
@@ -16,6 +16,7 @@
"in_list_view",
"in_standard_filter",
"in_global_search",
+ "in_preview",
"bold",
"allow_in_quick_entry",
"translatable",
@@ -381,12 +382,18 @@
"fieldtype": "Code",
"label": "Read Only Depends On",
"options": "JS"
+ },
+ {
+ "default": "0",
+ "fieldname": "in_preview",
+ "fieldtype": "Check",
+ "label": "In Preview"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-04-15 02:26:59.673750",
+ "modified": "2020-04-10 11:58:44.573537",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
index a130c1d6cf..275028fc15 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
@@ -59,6 +59,10 @@ frappe.ui.form.on('Dashboard Chart', {
if (frm.doc.report_name) {
frm.trigger('set_chart_report_filters');
}
+
+ if (!frappe.boot.developer_mode) {
+ frm.set_df_property("custom_options", "hidden", 1);
+ }
},
source: function(frm) {
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
index 676cdbe24a..cd32292783 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
@@ -33,6 +33,7 @@
"type",
"column_break_2",
"color",
+ "custom_options",
"section_break_10",
"last_synced_on"
],
@@ -124,7 +125,7 @@
"fieldname": "type",
"fieldtype": "Select",
"label": "Type",
- "options": "Line\nBar\nPercentage\nPie",
+ "options": "Line\nBar\nPercentage\nPie\nDonut",
"reqd": 1
},
{
@@ -213,10 +214,16 @@
"label": "Y Axis",
"mandatory_depends_on": "eval:doc.report_name && !doc.is_custom",
"options": "Dashboard Chart Field"
+ },
+ {
+ "description": "Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"]",
+ "fieldname": "custom_options",
+ "fieldtype": "Code",
+ "label": "Custom Options"
}
],
"links": [],
- "modified": "2020-04-08 18:54:36.739183",
+ "modified": "2020-04-20 23:49:11.389909",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
index b2a6f0a0ff..7bed8f4504 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
@@ -79,7 +79,7 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
to_date = chart.to_date
timegrain = time_interval or chart.time_interval
- filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json)
+ filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json) or []
# don't include cancelled documents
filters.append([chart.document_type, 'docstatus', '<', 2, False])
@@ -97,6 +97,10 @@ def create_report_chart(args):
_doc = frappe.new_doc('Dashboard Chart')
_doc.update(args)
+
+ if (args.get("custom_options")):
+ _doc.custom_options = json.dumps(args.get("custom_options"))
+
if frappe.db.exists('Dashboard Chart', args.chart_name):
args.chart_name = append_number_if_name_exists('Dashboard Chart', args.chart_name)
_doc.chart_name = args.chart_name
@@ -108,6 +112,7 @@ def create_report_chart(args):
@frappe.whitelist()
def add_chart_to_dashboard(args):
args = frappe.parse_json(args)
+
dashboard = frappe.get_doc('Dashboard', args.dashboard)
dashboard_link = frappe.new_doc('Dashboard Chart Link')
dashboard_link.chart = args.chart_name
@@ -362,6 +367,8 @@ class DashboardChart(Document):
self.check_required_field()
self.check_document_type()
+ self.validate_custom_options()
+
def check_required_field(self):
if not self.document_type:
frappe.throw(_("Document type is required to create a dashboard chart"))
@@ -378,3 +385,10 @@ class DashboardChart(Document):
def check_document_type(self):
if frappe.get_meta(self.document_type).issingle:
frappe.throw("You cannot create a dashboard chart from single DocTypes")
+
+ def validate_custom_options(self):
+ if self.custom_options:
+ try:
+ json.loads(self.custom_options)
+ except ValueError as error:
+ frappe.throw("Invalid json added in the custom options: %s" % error)
\ No newline at end of file
diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py
index 26fc6037fd..ba0e5c2216 100644
--- a/frappe/desk/form/meta.py
+++ b/frappe/desk/form/meta.py
@@ -196,8 +196,6 @@ class FormMeta(Meta):
self.get("__messages").update(messages, as_value=True)
def load_dashboard(self):
- if self.custom:
- return
self.set('__dashboard', self.get_dashboard_data())
def load_kanban_meta(self):
diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py
index 3a8815ca71..109dd25f4f 100644
--- a/frappe/desk/notifications.py
+++ b/frappe/desk/notifications.py
@@ -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
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index aaf859e7fd..164f6389eb 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -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 = {
@@ -299,6 +299,7 @@ def export_query():
_("You can try changing the filters of your report."))
return
+ data.columns = [col for col in data.columns if isinstance(col, dict) and not col.get('hidden')]
columns = get_columns_dict(data.columns)
from frappe.utils.xlsxutils import make_xlsx
@@ -310,7 +311,7 @@ def export_query():
frappe.response['type'] = 'binary'
-def build_xlsx_data(columns, data, visible_idx,include_indentation):
+def build_xlsx_data(columns, data, visible_idx, include_indentation):
result = [[]]
# add column headings
diff --git a/frappe/email/doctype/email_domain/email_domain.py b/frappe/email/doctype/email_domain/email_domain.py
index b6585d966b..08583dc228 100644
--- a/frappe/email/doctype/email_domain/email_domain.py
+++ b/frappe/email/doctype/email_domain/email_domain.py
@@ -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__)
diff --git a/frappe/exceptions.py b/frappe/exceptions.py
index ef75a36e03..9a1c1fb0b0 100644
--- a/frappe/exceptions.py
+++ b/frappe/exceptions.py
@@ -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
\ No newline at end of file
+class InvalidAuthorizationToken(CSRFTokenError): pass
diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py
index 558f7117c0..80dfef2693 100644
--- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py
+++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py
@@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe
from frappe import _, safe_encode
from frappe.model.document import Document
-
+from frappe.twofactor import (should_run_2fa, authenticate_for_2factor,confirm_otp_token)
class LDAPSettings(Document):
def validate(self):
@@ -237,6 +237,10 @@ def login():
user = ldap.authenticate(frappe.as_unicode(args.usr), frappe.as_unicode(args.pwd))
frappe.local.login_manager.user = user.name
+ if should_run_2fa(user.name):
+ authenticate_for_2factor(user.name)
+ if not confirm_otp_token(frappe.local.login_manager):
+ return False
frappe.local.login_manager.post_login()
# because of a GET request!
diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py
index 7af987f4bc..93ef78df7b 100644
--- a/frappe/model/__init__.py
+++ b/frappe/model/__init__.py
@@ -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:
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index 9ab1ef7799..feeb96898a 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -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 "" in value and not ("