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 ("" in value and not bool(BeautifulSoup(value, "html.parser").find()): # should be handled separately via the markdown converter function continue diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py index f697d8051a..91fb079fca 100644 --- a/frappe/model/create_new.py +++ b/frappe/model/create_new.py @@ -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) diff --git a/frappe/model/document.py b/frappe/model/document.py index 03b21ea667..04db33ca69 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -268,6 +268,10 @@ class Document(BaseDocument): if hasattr(self, "__islocal"): delattr(self, "__islocal") + # clear unsaved flag + if hasattr(self, "__unsaved"): + delattr(self, "__unsaved") + if not (frappe.flags.in_migrate or frappe.local.flags.in_install or frappe.flags.in_setup_wizard): follow_document(self.doctype, self.name, frappe.session.user) return self @@ -329,6 +333,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): @@ -583,6 +591,9 @@ class Document(BaseDocument): if high_permlevel_fields: self.reset_values_if_no_permlevel_access(has_access_to, high_permlevel_fields) + # If new record then don't reset the values for child table + if self.is_new(): return + # check for child tables for df in self.meta.get_table_fields(): high_permlevel_fields = frappe.get_meta(df.options).get_high_permlevel_fields() @@ -1318,6 +1329,9 @@ def make_event_update_log(doc, update_type): def check_doctype_has_consumers(doctype): """Check if doctype has event consumers for event streaming""" + if not frappe.db.exists("DocType", "Event Consumer"): + return False + event_consumers = frappe.get_all('Event Consumer') for event_consumer in event_consumers: consumer = frappe.get_doc('Event Consumer', event_consumer.name) diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 9c71f8c0b1..2321e0c22a 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -425,17 +425,19 @@ class Meta(Document): implemented in other Frappe applications via hooks. ''' data = frappe._dict() - try: - module = load_doctype_module(self.name, suffix='_dashboard') - if hasattr(module, 'get_data'): - data = frappe._dict(module.get_data()) - except ImportError: - pass + if not self.custom: + try: + module = load_doctype_module(self.name, suffix='_dashboard') + if hasattr(module, 'get_data'): + data = frappe._dict(module.get_data()) + except ImportError: + pass self.add_doctype_links(data) - for hook in frappe.get_hooks("override_doctype_dashboards", {}).get(self.name, []): - data = frappe.get_attr(hook)(data=data) + if not self.custom: + for hook in frappe.get_hooks("override_doctype_dashboards", {}).get(self.name, []): + data = frappe.get_attr(hook)(data=data) return data diff --git a/frappe/patches/v12_0/move_email_and_phone_to_child_table.py b/frappe/patches/v12_0/move_email_and_phone_to_child_table.py index 4388d3c849..12680609d5 100644 --- a/frappe/patches/v12_0/move_email_and_phone_to_child_table.py +++ b/frappe/patches/v12_0/move_email_and_phone_to_child_table.py @@ -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): diff --git a/frappe/printing/page/print_format_builder/print_format_builder.js b/frappe/printing/page/print_format_builder/print_format_builder.js index d0a3379609..4e049d120a 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder.js +++ b/frappe/printing/page/print_format_builder/print_format_builder.js @@ -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'); diff --git a/frappe/public/build.json b/frappe/public/build.json index 75a89e5010..7f55924a6b 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -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", diff --git a/frappe/public/js/frappe/form/controls/barcode.js b/frappe/public/js/frappe/form/controls/barcode.js index 859cbbb22a..c2314d6664 100644 --- a/frappe/public/js/frappe/form/controls/barcode.js +++ b/frappe/public/js/frappe/form/controls/barcode.js @@ -48,6 +48,7 @@ frappe.ui.form.ControlBarcode = frappe.ui.form.ControlData.extend({ const svg = this.barcode_area.find('svg')[0]; JsBarcode(svg, value, this.get_options(value)); $(svg).attr('data-barcode-value', value); + $(svg).attr('width', '100%'); return this.barcode_area.html(); } }, diff --git a/frappe/public/js/frappe/form/controls/code.js b/frappe/public/js/frappe/form/controls/code.js index 0648ad6e22..60825c82ad 100644 --- a/frappe/public/js/frappe/form/controls/code.js +++ b/frappe/public/js/frappe/form/controls/code.js @@ -9,6 +9,12 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ this.ace_editor_target = $('
') .appendTo(this.input_area); + this.expanded = false; + this.$expand_button = $(``).click(() => { + this.expanded = !this.expanded; + this.refresh_height(); + this.toggle_label(); + }).appendTo(this.$input_wrapper); // styling this.ace_editor_target.addClass('border rounded'); this.ace_editor_target.css('height', 300); @@ -26,6 +32,16 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ }, 300)); }, + refresh_height() { + this.ace_editor_target.css('height', this.expanded ? 600 : 300); + 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', @@ -34,7 +50,9 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ 'CSS': 'ace/mode/css', 'Markdown': 'ace/mode/markdown', 'SCSS': 'ace/mode/scss', - 'JSON': 'ace/mode/json' + 'JSON': 'ace/mode/json', + 'Golang': 'ace/mode/golang', + 'Go': 'ace/mode/golang' }; const language = this.df.options; diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js index a7f0050d65..c943ec89bb 100644 --- a/frappe/public/js/frappe/form/controls/data.js +++ b/frappe/public/js/frappe/form/controls/data.js @@ -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) { diff --git a/frappe/public/js/frappe/form/controls/multiselect_list.js b/frappe/public/js/frappe/form/controls/multiselect_list.js index 3e8dc21dca..cd86bdd767 100644 --- a/frappe/public/js/frappe/form/controls/multiselect_list.js +++ b/frappe/public/js/frappe/form/controls/multiselect_list.js @@ -18,6 +18,7 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({ this.$list_wrapper = $(template); this.$input = $(''); this.input = this.$input.get(0); + this.has_input = true; this.$list_wrapper.prependTo(this.input_area); this.$filter_input = this.$list_wrapper.find('input'); this.$list_wrapper.on('click', '.dropdown-menu', e => { diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index e714418375..82478db707 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -184,13 +184,7 @@ frappe.ui.form.Form = class FrappeForm { frappe.model.on(me.doctype, "*", function(fieldname, value, doc) { // set input if(doc.name===me.docname) { - if ((value==='' || value===null) && !doc[fieldname]) { - // both the incoming and outgoing values are falsy - // the texteditor, summernote, changes nulls to empty strings on render, - // so ignore those changes - } else { - me.dirty(); - } + me.dirty(); let field = me.fields_dict[fieldname]; field && field.refresh(fieldname); diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 0b8e7e8d23..48d7a3420e 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -22,9 +22,6 @@ export default class GridRow { if(me.grid.allow_on_grid_editing() && me.grid.is_editable()) { // pass } else { - if (!me.grid.is_editable()) { - me.docfields.map(df => df.read_only = 1); - } me.toggle_view(); return false; } diff --git a/frappe/public/js/frappe/form/sidebar/form_sidebar.js b/frappe/public/js/frappe/form/sidebar/form_sidebar.js index 02caf25557..a145e47149 100644 --- a/frappe/public/js/frappe/form/sidebar/form_sidebar.js +++ b/frappe/public/js/frappe/form/sidebar/form_sidebar.js @@ -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}", - ["" + frappe.user.full_name(this.frm.doc.modified_by) + "", - "
" + comment_when(this.frm.doc.modified)])); - this.sidebar.find(".created-by").html(__("{0} created this {1}", - ["" + frappe.user.full_name(this.frm.doc.owner) + "", - "
" + 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(), + "
" + 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(), + "
" + comment_when(this.frm.doc.creation), + ]) + ); this.refresh_like(); frappe.ui.form.set_user_image(this.frm); diff --git a/frappe/public/js/frappe/form/templates/form_sidebar.html b/frappe/public/js/frappe/form/templates/form_sidebar.html index b611557c43..30b2205bae 100644 --- a/frappe/public/js/frappe/form/templates/form_sidebar.html +++ b/frappe/public/js/frappe/form/templates/form_sidebar.html @@ -105,6 +105,7 @@ diff --git a/frappe/public/js/frappe/model/create_new.js b/frappe/public/js/frappe/model/create_new.js index 1a118be5db..7be7fc5baa 100644 --- a/frappe/public/js/frappe/model/create_new.js +++ b/frappe/public/js/frappe/model/create_new.js @@ -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)); diff --git a/frappe/public/js/frappe/utils/common.js b/frappe/public/js/frappe/utils/common.js index e919012664..6ad15e44bf 100644 --- a/frappe/public/js/frappe/utils/common.js +++ b/frappe/public/js/frappe/utils/common.js @@ -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 + }); +}; diff --git a/frappe/public/js/frappe/utils/datatype.js b/frappe/public/js/frappe/utils/datatype.js index 16f87b872f..1b9206f434 100644 --- a/frappe/public/js/frappe/utils/datatype.js +++ b/frappe/public/js/frappe/utils/datatype.js @@ -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; -}; \ No newline at end of file +}; diff --git a/frappe/public/js/frappe/utils/pretty_date.js b/frappe/public/js/frappe/utils/pretty_date.js index 060ae73a98..ef235ed3b1 100644 --- a/frappe/public/js/frappe/utils/pretty_date.js +++ b/frappe/public/js/frappe/utils/pretty_date.js @@ -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) { diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 0f27e97178..1afdbfd81c 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -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 diff --git a/frappe/public/js/frappe/views/breadcrumbs.js b/frappe/public/js/frappe/views/breadcrumbs.js index 2aef1a8218..1c1049391f 100644 --- a/frappe/public/js/frappe/views/breadcrumbs.js +++ b/frappe/public/js/frappe/views/breadcrumbs.js @@ -6,9 +6,10 @@ frappe.breadcrumbs = { preferred: { "File": "", + "Video": "", "Dashboard": "Customization", "Dashboard Chart": "Customization", - "Dashboard Chart Source": "Customization", + "Dashboard Chart Source": "Customization" }, module_map: { diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index 17b32b3a52..ba290417f5 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -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 => { diff --git a/frappe/public/js/frappe/views/desktop/desktop.js b/frappe/public/js/frappe/views/desktop/desktop.js index 9f0df6e172..d71d390d20 100644 --- a/frappe/public/js/frappe/views/desktop/desktop.js +++ b/frappe/public/js/frappe/views/desktop/desktop.js @@ -344,10 +344,6 @@ class DesktopPage { { color: "orange", description: __("No Records Created") - }, - { - color: "red", - description: __("Has Open Entries") } ].map(item => { return `
diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 08da956072..5105494862 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -183,7 +183,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } create_dashboard_chart(chart_args, dashboard_name, chart_name) { - let args = { 'dashboard': dashboard_name || null, 'chart_type': 'Report', @@ -191,8 +190,15 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { 'type': chart_args.chart_type || frappe.model.unscrub(chart_args.type), 'color': chart_args.color, 'filters_json': JSON.stringify(this.get_filter_values()), + 'custom_options': {} }; + for (let key in chart_args) { + if (key != "data") { + args['custom_options'][key] = chart_args[key]; + } + } + if (this.chart_fields) { let x_field_title = toTitle(chart_args.x_field); let y_field_title = toTitle(chart_args.y_fields[0]); @@ -1084,7 +1090,12 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { ], ({ file_format, include_indentation }) => { this.make_access_log('Export', file_format); if (file_format === 'CSV') { - const column_row = this.columns.map(col => col.label); + const column_row = this.columns.reduce((acc, col) => { + if (!col.hidden) { + acc.push(col.label); + } + return acc; + }, []); const data = this.get_data_for_csv(include_indentation); const out = [column_row].concat(data); diff --git a/frappe/public/js/frappe/widgets/chart_widget.js b/frappe/public/js/frappe/widgets/chart_widget.js index 9c03f08523..313b0ea293 100644 --- a/frappe/public/js/frappe/widgets/chart_widget.js +++ b/frappe/public/js/frappe/widgets/chart_widget.js @@ -457,7 +457,8 @@ export default class ChartWidget extends Widget { Line: "line", Bar: "bar", Percentage: "percentage", - Pie: "pie" + Pie: "pie", + Donut: "donut" }; let colors = []; @@ -490,6 +491,14 @@ export default class ChartWidget extends Widget { shortenYAxisNumbers: 1 } }; + + if (this.chart_doc.custom_options) { + let custom_options = JSON.parse(this.chart_doc.custom_options); + for (let key in custom_options) { + chart_args[key] = custom_options[key]; + } + } + if (!this.dashboard_chart) { this.dashboard_chart = new frappe.Chart( this.chart_wrapper[0], diff --git a/frappe/public/less/form.less b/frappe/public/less/form.less index b72d249aab..8e43b05122 100644 --- a/frappe/public/less/form.less +++ b/frappe/public/less/form.less @@ -770,6 +770,7 @@ h6.uppercase, .h6.uppercase { .help-box { margin-top: 3px; + margin-bottom: 6px; } pre { diff --git a/frappe/public/less/module.less b/frappe/public/less/module.less new file mode 100644 index 0000000000..f924778864 --- /dev/null +++ b/frappe/public/less/module.less @@ -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; + } + } +} diff --git a/frappe/public/less/sidebar.less b/frappe/public/less/sidebar.less index ac5a5c33d5..28dae1a948 100644 --- a/frappe/public/less/sidebar.less +++ b/frappe/public/less/sidebar.less @@ -273,7 +273,8 @@ body[data-route^="Module"] .main-menu { } .layout-side-section .form-sidebar { - .modified-by { + .modified-by, + .pageview-count { margin-bottom: 15px; } } diff --git a/frappe/public/scss/website.scss b/frappe/public/scss/website.scss index 6f82e25ee0..546110bd5c 100644 --- a/frappe/public/scss/website.scss +++ b/frappe/public/scss/website.scss @@ -23,6 +23,17 @@ footer { flex-shrink: 0; } +// make navbar padding consistent with the page +.navbar { + padding-left: 0; + padding-right: 0; + + .container { + padding-left: 15px; + padding-right: 15px; + } +} + .navbar.bg-dark { .dropdown-menu { font-size: .75rem; diff --git a/frappe/templates/includes/navbar/navbar_items.html b/frappe/templates/includes/navbar/navbar_items.html index 10d484df65..352fc23bbd 100644 --- a/frappe/templates/includes/navbar/navbar_items.html +++ b/frappe/templates/includes/navbar/navbar_items.html @@ -64,6 +64,24 @@ {% if not only_static %} {% block navbar_right_extension %}{% endblock %} {% endif %} + + {% if show_sidebar and sidebar_items %} +
+
+ {% for item in sidebar_items -%} + + {%- endfor %} +
+
+ {% endif %} + {% include "templates/includes/navbar/navbar_search.html" %} {% include "templates/includes/navbar/navbar_login.html" %} diff --git a/frappe/templates/styles/card_style.css b/frappe/templates/styles/card_style.css index 595d974011..cf90ff0bd5 100644 --- a/frappe/templates/styles/card_style.css +++ b/frappe/templates/styles/card_style.css @@ -31,7 +31,6 @@ } .ellipsis { - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; diff --git a/frappe/templates/web.html b/frappe/templates/web.html index d2d38a6320..2fdfc9564a 100644 --- a/frappe/templates/web.html +++ b/frappe/templates/web.html @@ -13,7 +13,7 @@
{% block page_container %} -
+
{% endmacro %} -{% macro container_attributes() %} -id="page-{{ name or route | e }}" data-path="{{ pathname | e }}" {%- if page_or_generator=="Generator" %}source-type="Generator" data-doctype="{{ doctype }}"{% endif %} -{% endmacro %} +{% macro container_attributes() -%} +id="page-{{ name or route | e }}" data-path="{{ pathname | e }}" + {%- if page_or_generator=="Generator" %}source-type="Generator" data-doctype="{{ doctype }}"{%- endif %} + {%- if source_content_type %}source-content-type="{{ source_content_type }}"{%- endif %} +{%- endmacro %} {% if show_sidebar %}
diff --git a/frappe/twofactor.py b/frappe/twofactor.py index e60113215b..b44092d6a1 100644 --- a/frappe/twofactor.py +++ b/frappe/twofactor.py @@ -374,11 +374,11 @@ def delete_qrimage(user, check_expiry=False): def delete_all_barcodes_for_users(): '''Task to delete all barcodes for user.''' - if not two_factor_is_enabled(): - return users = frappe.get_all('User', {'enabled':1}) for user in users: + if not two_factor_is_enabled(user=user.name): + continue delete_qrimage(user.name, check_expiry=True) def should_remove_barcode_image(barcode): diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 649d3bf72c..34432839bb 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -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() diff --git a/frappe/utils/change_log.py b/frappe/utils/change_log.py index c94a247796..776fb825c2 100644 --- a/frappe/utils/change_log.py +++ b/frappe/utils/change_log.py @@ -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 diff --git a/frappe/utils/logger.py b/frappe/utils/logger.py index d40e2565cb..5a77434cde 100755 --- a/frappe/utils/logger.py +++ b/frappe/utils/logger.py @@ -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 = {} - diff --git a/frappe/utils/response.py b/frappe/utils/response.py index 1dfbbe5516..fe7af072cf 100644 --- a/frappe/utils/response.py +++ b/frappe/utils/response.py @@ -218,6 +218,6 @@ def send_private_file(path): def handle_session_stopped(): frappe.respond_as_web_page(_("Updating"), - _("Your system is being updated. Please refresh again after a few moments"), + _("Your system is being updated. Please refresh again after a few moments."), http_status_code=503, indicator_color='orange', fullpage = True, primary_action=None) return frappe.website.render.render("message", http_status_code=503) diff --git a/frappe/website/context.py b/frappe/website/context.py index dcef22af43..dc3af34de9 100644 --- a/frappe/website/context.py +++ b/frappe/website/context.py @@ -221,24 +221,24 @@ def add_metatags(context): tags = frappe._dict(context.get("metatags") or {}) if tags: - if not "twitter:card" in tags: - tags["twitter:card"] = "summary_large_image" - if not "og:type" in tags: tags["og:type"] = "article" - if tags.get("name"): - tags["og:title"] = tags["twitter:title"] = tags["name"] + name = tags.get('name') or tags.get('title') + if name: + tags["og:title"] = tags["twitter:title"] = name - if tags.get("title"): - tags["og:title"] = tags["twitter:title"] = tags["title"] - - if tags.get("description"): - tags["og:description"] = tags["twitter:description"] = tags["description"] + description = tags.get("description") or context.description + if description: + tags['description'] = tags["og:description"] = tags["twitter:description"] = description image = tags.get('image', context.image or None) if image: tags["og:image"] = tags["twitter:image:src"] = tags["image"] = frappe.utils.get_url(image) + tags['twitter:card'] = "summary_large_image" + + if context.author or tags.get('author'): + tags['author'] = context.author or tags.get('author') if context.path: tags['og:url'] = tags['url'] = frappe.utils.get_url(context.path) @@ -246,11 +246,6 @@ def add_metatags(context): if context.published_on: tags['datePublished'] = context.published_on - if context.author: - tags['author'] = context.author - - if context.description: - tags['description'] = context.description tags['language'] = frappe.local.lang or 'en' diff --git a/frappe/website/doctype/blogger/blogger.json b/frappe/website/doctype/blogger/blogger.json index be4eb6399e..b8165a5908 100644 --- a/frappe/website/doctype/blogger/blogger.json +++ b/frappe/website/doctype/blogger/blogger.json @@ -1,274 +1,108 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "field:short_name", - "beta": 0, - "creation": "2013-03-25 16:00:51", - "custom": 0, - "description": "User ID of a Blogger", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "actions": [], + "allow_import": 1, + "autoname": "field:short_name", + "creation": "2013-03-25 16:00:51", + "description": "User ID of a Blogger", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "disabled", + "short_name", + "full_name", + "user", + "bio", + "avatar", + "posts" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "disabled", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Disabled", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Will be used in url (usually first name).", - "fieldname": "short_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Short Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "description": "Will be used in url (usually first name).", + "fieldname": "short_name", + "fieldtype": "Data", + "label": "Short Name", + "reqd": 1, + "unique": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "full_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Full Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "full_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Full Name", + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "user", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "User", - "length": 0, - "no_copy": 0, - "options": "User", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "bio", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Bio", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "bio", + "fieldtype": "Small Text", + "label": "Bio" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "avatar", - "fieldtype": "Attach", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Avatar", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "avatar", + "fieldtype": "Attach", + "label": "Avatar" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "posts", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Posts", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "posts", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Posts", + "no_copy": 1, + "read_only": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-user", - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 1, - "modified": "2018-10-10 14:40:40.407657", - "modified_by": "Administrator", - "module": "Website", - "name": "Blogger", - "owner": "Administrator", + ], + "icon": "fa fa-user", + "idx": 1, + "links": [ + { + "link_doctype": "Blog Post", + "link_fieldname": "blogger" + } + ], + "max_attachments": 1, + "modified": "2020-04-19 08:21:09.684300", + "modified_by": "Administrator", + "module": "Website", + "name": "Blogger", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 1, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Website Manager", - "set_user_permissions": 1, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Website Manager", + "set_user_permissions": 1, + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "Blogger", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "email": 1, + "print": 1, + "read": 1, + "role": "Blogger", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "title_field": "full_name", - "track_changes": 1, - "track_seen": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "full_name", + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/website/doctype/color/__init__.py b/frappe/website/doctype/color/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/doctype/color/color.js b/frappe/website/doctype/color/color.js new file mode 100644 index 0000000000..78b3f773d1 --- /dev/null +++ b/frappe/website/doctype/color/color.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Color', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/website/doctype/color/color.json b/frappe/website/doctype/color/color.json new file mode 100644 index 0000000000..f42898da12 --- /dev/null +++ b/frappe/website/doctype/color/color.json @@ -0,0 +1,44 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2020-04-19 02:25:37.010180", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "color" + ], + "fields": [ + { + "fieldname": "color", + "fieldtype": "Color", + "in_list_view": 1, + "label": "Color", + "reqd": 1 + } + ], + "links": [], + "modified": "2020-04-19 02:25:47.417772", + "modified_by": "Administrator", + "module": "Website", + "name": "Color", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Website Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/website/doctype/color/color.py b/frappe/website/doctype/color/color.py new file mode 100644 index 0000000000..245b9e9165 --- /dev/null +++ b/frappe/website/doctype/color/color.py @@ -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 Color(Document): + pass diff --git a/frappe/website/doctype/color/test_color.py b/frappe/website/doctype/color/test_color.py new file mode 100644 index 0000000000..2f2be331ad --- /dev/null +++ b/frappe/website/doctype/color/test_color.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 TestColor(unittest.TestCase): + pass diff --git a/frappe/website/doctype/web_page/web_page.json b/frappe/website/doctype/web_page/web_page.json index 645d83e155..9e348d2412 100644 --- a/frappe/website/doctype/web_page/web_page.json +++ b/frappe/website/doctype/web_page/web_page.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_guest_to_view": 1, "allow_import": 1, "creation": "2013-03-28 10:35:30", @@ -13,6 +14,7 @@ "slideshow", "cb1", "published", + "full_width", "show_title", "start_date", "end_date", @@ -39,6 +41,10 @@ "sb2", "header", "breadcrumbs", + "metatags_section", + "meta_title", + "meta_description", + "meta_image", "set_meta_tags" ], "fields": [ @@ -217,7 +223,7 @@ "depends_on": "eval:!doc.__islocal", "fieldname": "sb2", "fieldtype": "Section Break", - "label": "Header, Breadcrumbs and Meta Tags" + "label": "Header and Breadcrumbs" }, { "description": "HTML for header section. Optional", @@ -235,21 +241,49 @@ { "fieldname": "set_meta_tags", "fieldtype": "Button", - "label": "Set Meta Tags" + "label": "Add Custom Tags" }, { "default": "0", "fieldname": "dynamic_template", "fieldtype": "Check", "label": "Dynamic Template" + }, + { + "default": "0", + "fieldname": "full_width", + "fieldtype": "Check", + "label": "Full Width" + }, + { + "collapsible": 1, + "fieldname": "metatags_section", + "fieldtype": "Section Break", + "label": "Meta Tags" + }, + { + "fieldname": "meta_title", + "fieldtype": "Data", + "label": "Title" + }, + { + "fieldname": "meta_description", + "fieldtype": "Small Text", + "label": "Description" + }, + { + "fieldname": "meta_image", + "fieldtype": "Attach Image", + "label": "Image" } ], "has_web_view": 1, "icon": "fa fa-file-alt", "idx": 1, "is_published_field": "published", + "links": [], "max_attachments": 20, - "modified": "2019-10-02 13:58:50.825481", + "modified": "2020-04-19 12:26:21.546908", "modified_by": "Administrator", "module": "Website", "name": "Web Page", diff --git a/frappe/website/doctype/web_page/web_page.py b/frappe/website/doctype/web_page/web_page.py index ffa836e3c5..da4bf31f46 100644 --- a/frappe/website/doctype/web_page/web_page.py +++ b/frappe/website/doctype/web_page/web_page.py @@ -36,6 +36,7 @@ class WebPage(WebsiteGenerator): def get_context(self, context): context.main_section = get_html_content_based_on_type(self, 'main_section', self.content_type) + context.source_content_type = self.content_type self.render_dynamic(context) # if static page, get static content @@ -127,13 +128,11 @@ class WebPage(WebsiteGenerator): def set_metatags(self, context): context.metatags = { - "name": context.title + "name": self.meta_title or self.title, + "description": self.meta_description, + "image": self.meta_image or find_first_image(context.main_section or "") } - image = find_first_image(context.main_section or "") - if image: - context.metatags["image"] = image - def validate_dates(self): if self.end_date: if self.start_date and get_datetime(self.end_date) < get_datetime(self.start_date): diff --git a/frappe/website/doctype/web_page_view/__init__.py b/frappe/website/doctype/web_page_view/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/doctype/web_page_view/test_web_page_view.py b/frappe/website/doctype/web_page_view/test_web_page_view.py new file mode 100644 index 0000000000..d51727ec68 --- /dev/null +++ b/frappe/website/doctype/web_page_view/test_web_page_view.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 TestWebPageView(unittest.TestCase): + pass diff --git a/frappe/website/doctype/web_page_view/web_page_view.js b/frappe/website/doctype/web_page_view/web_page_view.js new file mode 100644 index 0000000000..77a047e408 --- /dev/null +++ b/frappe/website/doctype/web_page_view/web_page_view.js @@ -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) { + + // } +}); diff --git a/frappe/website/doctype/web_page_view/web_page_view.json b/frappe/website/doctype/web_page_view/web_page_view.json new file mode 100644 index 0000000000..7a1a210d62 --- /dev/null +++ b/frappe/website/doctype/web_page_view/web_page_view.json @@ -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 +} \ No newline at end of file diff --git a/frappe/website/doctype/web_page_view/web_page_view.py b/frappe/website/doctype/web_page_view/web_page_view.py new file mode 100644 index 0000000000..08625f9d6f --- /dev/null +++ b/frappe/website/doctype/web_page_view/web_page_view.py @@ -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") \ No newline at end of file diff --git a/frappe/website/doctype/web_view/templates/web_view_content.html b/frappe/website/doctype/web_view/templates/web_view_content.html index f2e750a472..a1ac143639 100644 --- a/frappe/website/doctype/web_view/templates/web_view_content.html +++ b/frappe/website/doctype/web_view/templates/web_view_content.html @@ -9,7 +9,7 @@ {%- endif -%} {%- macro render_element(element) -%} - {%- if element.element_type=='Content' -%} + {%- if element.element_type in ('Content', 'Web View') -%}
{{ element.web_content_html }}
@@ -25,17 +25,16 @@ {%- endmacro -%} {%- macro element_style(element) -%} - {%- if element.element_style -%} - style = "{{ element.element_style }}" + {%- if element.element_style or element.background_color -%} + style = "{{ element.element_style or '' }} {%if element.background_color %}background-color: {{ element.background_color }};{% endif %}" {%- endif -%} {%- endmacro -%} - +{%- macro render_sections(sections) -%} {%- for section in sections -%} -
-
+
+
{%- if section.section_intro -%} -
{{ section.section_intro }}
{%- endif -%} @@ -74,4 +73,11 @@ {%- endif -%}
-{%- endfor -%} \ No newline at end of file +{%- endfor -%} +{%- endmacro -%} + +{% if content_type == 'HTML' -%} +{{ content_html }} +{%- else -%} +{{ render_sections(sections) }} +{%- endif -%} \ No newline at end of file diff --git a/frappe/website/doctype/web_view/test_web_view.py b/frappe/website/doctype/web_view/test_web_view.py index 67b353844d..3dc072a6dd 100644 --- a/frappe/website/doctype/web_view/test_web_view.py +++ b/frappe/website/doctype/web_view/test_web_view.py @@ -14,6 +14,7 @@ class TestWebView(unittest.TestCase): @classmethod def setUpClass(cls): frappe.delete_doc_if_exists('Web View', 'test-web-view') + frappe.delete_doc_if_exists('Web View', 'html-web-view') frappe.delete_doc_if_exists('CSS Class', 'test-css-class') frappe.get_doc(dict( @@ -22,12 +23,25 @@ class TestWebView(unittest.TestCase): css = '.test-class { color: red; }' )).insert() + # simple html webview + frappe.get_doc(dict( + doctype = 'Web View', + title = 'HTML Web View', + route = 'html-web-view', + published = 1, + content_type = 'HTML', + content_html = '

Hello HTML

' + )).insert() + + # simple web view with components + frappe.get_doc(dict( doctype = 'Web View', title = 'Test Web View', route = 'test-web-view', published = 1, - items = [ + content_type = 'Components', + components = [ dict( element_type = 'Section', section_type = 'List' @@ -57,19 +71,27 @@ class TestWebView(unittest.TestCase): web_content_type = 'Markdown', web_content_markdown = 'Column 2' ), + dict( + element_type = 'Web View', + web_view = 'html-web-view', + ), ] )).insert() def test_web_view(self): html = get_page_content('test-web-view') - #print(html) self.assert_web_view_in_html(html) + def test_html_web_view(self): + html = get_page_content('html-web-view') + self.assertTrue('Hello HTML' in html) + def assert_web_view_in_html(self, html): self.assertTrue('

Heading

' in html) self.assertTrue('
Here is some HTML
' in html) self.assertTrue('Column 1' in html) self.assertTrue('Column 2' in html) + self.assertTrue('Hello HTML' in html) self.assertTrue('.test-class { color: red; }' in html) def test_web_view_in_footer(self): diff --git a/frappe/website/doctype/web_view/web_view.json b/frappe/website/doctype/web_view/web_view.json index 6d957fd0d9..d4ccbad0e4 100644 --- a/frappe/website/doctype/web_view/web_view.json +++ b/frappe/website/doctype/web_view/web_view.json @@ -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", @@ -12,18 +11,21 @@ "field_order": [ "title", "route", + "column_break_4", + "full_width", "published", - "items", - "css" + "section_break_6", + "content_type", + "content_html", + "components", + "style_section", + "css", + "metatags_section", + "meta_title", + "meta_description", + "meta_image" ], "fields": [ - { - "fieldname": "items", - "fieldtype": "Table", - "label": "Items", - "options": "Web View Item", - "reqd": 1 - }, { "fieldname": "title", "fieldtype": "Data", @@ -36,8 +38,7 @@ "fieldname": "route", "fieldtype": "Data", "in_list_view": 1, - "label": "Route", - "reqd": 1 + "label": "Route" }, { "default": "0", @@ -49,12 +50,73 @@ "fieldname": "css", "fieldtype": "Code", "label": "CSS" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "full_width", + "fieldtype": "Check", + "label": "Full Width" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break", + "label": "Content" + }, + { + "default": "Components", + "fieldname": "content_type", + "fieldtype": "Select", + "label": "Content Type", + "options": "Components\nHTML", + "reqd": 1 + }, + { + "depends_on": "eval:doc.content_type==='Components'", + "fieldname": "components", + "fieldtype": "Table", + "label": "Components", + "options": "Web View Component" + }, + { + "depends_on": "eval:doc.content_type===\"HTML\"", + "fieldname": "content_html", + "fieldtype": "HTML Editor", + "label": "Content HTML" + }, + { + "fieldname": "style_section", + "fieldtype": "Section Break", + "label": "Style" + }, + { + "fieldname": "metatags_section", + "fieldtype": "Section Break", + "label": "Meta Tags" + }, + { + "fieldname": "meta_title", + "fieldtype": "Data", + "label": "Title" + }, + { + "fieldname": "meta_description", + "fieldtype": "Small Text", + "label": "Description" + }, + { + "fieldname": "meta_image", + "fieldtype": "Attach Image", + "label": "Image" } ], "has_web_view": 1, "is_published_field": "published", "links": [], - "modified": "2020-04-15 23:58:12.208049", + "modified": "2020-04-22 00:54:23.413077", "modified_by": "Administrator", "module": "Website", "name": "Web View", diff --git a/frappe/website/doctype/web_view/web_view.py b/frappe/website/doctype/web_view/web_view.py index 6828057fe1..8b441579b6 100644 --- a/frappe/website/doctype/web_view/web_view.py +++ b/frappe/website/doctype/web_view/web_view.py @@ -10,49 +10,71 @@ import frappe class WebView(WebsiteGenerator): def get_context(self, context): - # group items into sections + # group components into sections + if self.content_type=='Components': + self.build_components(context) + + self.set_metatags(context) + return context + + def build_components(self, context): context.sections = [] context.css_rules = [] - for item in self.items: - if not context.sections and item.element_type!='Section': + for component in self.components: + if not context.sections and component.element_type!='Section': self.add_default_section(context) - if item.element_type=='Section': - self.add_section(context, item) + if component.element_type=='Section': + self.add_section(context, component) else: - self.add_item(context, item) + self.add_component(context, component) - self.add_css_class(context, item) + self.add_css_class(context, component) + self.add_color(component) + self.add_missing_semi(component) return context - def add_section(self, context, item): - item.elements = [] - context.sections.append(item) + def add_section(self, context, component): + component.elements = [] + context.sections.append(component) - if item.section_intro: - item.section_intro = markdown(item.section_intro) + if component.section_intro: + component.section_intro = markdown(component.section_intro) - def add_item(self, context, item): - if item.hide: + def add_component(self, context, component): + if component.hide: return - if item.web_content_type == 'Markdown': - item.web_content_html = markdown(item.web_content_markdown) + if component.element_type == 'Web View' and component.web_view: + component.web_content_html = frappe.get_doc('Web View', component.web_view).render_content() - if item.title: - item.element_id = frappe.scrub(item.title) + elif component.web_content_type == 'Markdown': + component.web_content_html = markdown(component.web_content_markdown) - context.sections[-1].elements.append(item) + if component.title: + component.element_id = frappe.scrub(component.title) - def add_css_class(self, context, item): + context.sections[-1].elements.append(component) + + def add_css_class(self, context, component): # add css class definitions selected by the user - if item.element_class and not item.hide: - css, is_dynamic = frappe.db.get_value('CSS Class', item.element_class, ['css', 'is_dynamic']) + if component.element_class and not component.hide: + css, is_dynamic = frappe.db.get_value('CSS Class', component.element_class, ['css', 'is_dynamic']) if is_dynamic: css = frappe.render_template(css, self.get_theme()) context.css_rules.append(css) + def add_color(self, component): + # convert to css color + if component.background_color and not component.hide: + component.background_color = frappe.db.get_value('Color', + component.background_color, 'color', cache=True) + + def add_missing_semi(self, component): + if component.element_style and not component.element_style.strip().endswith(';'): + component.element_style = component.element_style.strip() + ';' + def render_content(self): # webview can be rendered as an object (see footer) return frappe.render_template("frappe/website/doctype/web_view/templates/web_view_content.html", self.get_context(self.as_dict())) @@ -72,3 +94,11 @@ class WebView(WebsiteGenerator): title='Default Section', elements=[] )) + + def set_metatags(self, context): + context.metatags = { + "name": self.meta_title or context.title, + "description": self.meta_description, + "image": self.meta_image + } + diff --git a/frappe/website/doctype/web_view_component/__init__.py b/frappe/website/doctype/web_view_component/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/doctype/web_view_item/web_view_item.json b/frappe/website/doctype/web_view_component/web_view_component.json similarity index 70% rename from frappe/website/doctype/web_view_item/web_view_item.json rename to frappe/website/doctype/web_view_component/web_view_component.json index 0d33cbb8ce..15dd761c81 100644 --- a/frappe/website/doctype/web_view_item/web_view_item.json +++ b/frappe/website/doctype/web_view_component/web_view_component.json @@ -8,11 +8,14 @@ "element_type", "title", "hide", + "contain_section_width", "column_break_3", "columns", + "background_color", "element_class", "element_style", "section_break_5", + "web_view", "section_type", "web_content_type", "web_content_html", @@ -26,33 +29,35 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Element Type", - "options": "Section\nContent\nParagraph\nWeb List\nWeb Form", + "options": "Section\nContent\nImage\nWeb View", "reqd": 1 }, { + "default": "List", "depends_on": "eval:doc.element_type==='Section'", "fieldname": "section_type", "fieldtype": "Select", "in_list_view": 1, "label": "Section Type", - "options": "\nList\nTabbed\nGrid" + "options": "List\nTabbed\nGrid" }, { + "default": "Markdown", "depends_on": "eval:doc.element_type==='Content'", "fieldname": "web_content_type", "fieldtype": "Select", "in_list_view": 1, "label": "Web Content Type", - "options": "\nHTML\nMarkdown" + "options": "Markdown\nHTML" }, { - "depends_on": "eval:doc.web_content_type==='HTML'", + "depends_on": "eval:doc.element_type === 'Content' && doc.web_content_type === 'HTML'", "fieldname": "web_content_html", "fieldtype": "HTML Editor", "label": "Web Content HTML" }, { - "depends_on": "eval:doc.web_content_type==='Markdown'", + "depends_on": "eval:doc.element_type === 'Content' && doc.web_content_type === 'Markdown'", "fieldname": "web_content_markdown", "fieldtype": "Markdown Editor", "label": "Web Content Markdown" @@ -104,14 +109,34 @@ "fieldname": "element_style", "fieldtype": "Small Text", "label": "Element Style" + }, + { + "default": "0", + "depends_on": "eval:doc.element_type==='Section'", + "fieldname": "contain_section_width", + "fieldtype": "Check", + "label": "Contain Section Width" + }, + { + "fieldname": "background_color", + "fieldtype": "Link", + "label": "Background Color", + "options": "Color" + }, + { + "depends_on": "eval:doc.element_type==='Web View'", + "fieldname": "web_view", + "fieldtype": "Link", + "label": "Web View", + "options": "Web View" } ], "istable": 1, "links": [], - "modified": "2020-03-28 14:21:50.014823", + "modified": "2020-04-19 03:02:53.233036", "modified_by": "Administrator", "module": "Website", - "name": "Web View Item", + "name": "Web View Component", "owner": "Administrator", "permissions": [], "quick_entry": 1, diff --git a/frappe/website/doctype/web_view_component/web_view_component.py b/frappe/website/doctype/web_view_component/web_view_component.py new file mode 100644 index 0000000000..dcf1726c4a --- /dev/null +++ b/frappe/website/doctype/web_view_component/web_view_component.py @@ -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 WebViewComponent(Document): + pass diff --git a/frappe/website/doctype/website_settings/website_settings.js b/frappe/website/doctype/website_settings/website_settings.js index 38e1ff993a..be294258f4 100644 --- a/frappe/website/doctype/website_settings/website_settings.js +++ b/frappe/website/doctype/website_settings/website_settings.js @@ -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") { diff --git a/frappe/website/doctype/website_settings/website_settings.json b/frappe/website/doctype/website_settings/website_settings.json index f9ed247c0d..708d2a0473 100644 --- a/frappe/website/doctype/website_settings/website_settings.json +++ b/frappe/website/doctype/website_settings/website_settings.json @@ -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", diff --git a/frappe/website/doctype/website_settings/website_settings.py b/frappe/website/doctype/website_settings/website_settings.py index 49b93fae1d..ead48425ed 100644 --- a/frappe/website/doctype/website_settings/website_settings.py +++ b/frappe/website/doctype/website_settings/website_settings.py @@ -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) diff --git a/frappe/website/doctype/website_theme/website_theme.json b/frappe/website/doctype/website_theme/website_theme.json index f7c6a9a1f2..78a845fadc 100644 --- a/frappe/website/doctype/website_theme/website_theme.json +++ b/frappe/website/doctype/website_theme/website_theme.json @@ -14,8 +14,10 @@ "google_font", "font_size", "font_properties", - "use_full_width", - "column_break_7", + "button_rounded_corners", + "button_shadows", + "button_gradients", + "column_break_11", "primary_color", "text_color", "light_color", @@ -99,29 +101,29 @@ "fieldtype": "Data", "label": "Font Size" }, - { - "fieldname": "column_break_7", - "fieldtype": "Column Break" - }, { "fieldname": "primary_color", - "fieldtype": "Color", - "label": "Primary Color" + "fieldtype": "Link", + "label": "Primary Color", + "options": "Color" }, { "fieldname": "text_color", - "fieldtype": "Color", - "label": "Text Color" + "fieldtype": "Link", + "label": "Text Color", + "options": "Color" }, { "fieldname": "dark_color", - "fieldtype": "Color", - "label": "Dark Color" + "fieldtype": "Link", + "label": "Dark Color", + "options": "Color" }, { "fieldname": "background_color", - "fieldtype": "Color", - "label": "Background Color" + "fieldtype": "Link", + "label": "Background Color", + "options": "Color" }, { "fieldname": "stylesheet_section", @@ -135,8 +137,9 @@ }, { "fieldname": "light_color", - "fieldtype": "Color", - "label": "Light Color" + "fieldtype": "Link", + "label": "Light Color", + "options": "Color" }, { "default": "300,600", @@ -145,14 +148,30 @@ "label": "Font Properties" }, { - "description": "Content will not be inside a \"container\" class, you will have to add your own containers for different sections.", - "fieldname": "use_full_width", - "fieldtype": "Data", - "label": "Use Full Width" + "default": "1", + "fieldname": "button_rounded_corners", + "fieldtype": "Check", + "label": "Button Rounded Corners" + }, + { + "default": "0", + "fieldname": "button_shadows", + "fieldtype": "Check", + "label": "Button Shadows" + }, + { + "default": "0", + "fieldname": "button_gradients", + "fieldtype": "Check", + "label": "Button Gradients" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" } ], "links": [], - "modified": "2020-03-19 09:46:48.750150", + "modified": "2020-04-19 05:18:49.820803", "modified_by": "Administrator", "module": "Website", "name": "Website Theme", diff --git a/frappe/website/doctype/website_theme/website_theme.py b/frappe/website/doctype/website_theme/website_theme.py index d0ce582482..9b1c9115d6 100644 --- a/frappe/website/doctype/website_theme/website_theme.py +++ b/frappe/website/doctype/website_theme/website_theme.py @@ -25,7 +25,7 @@ class WebsiteTheme(Document): def is_standard_and_not_valid_user(self): return (not self.custom and not frappe.local.conf.get('developer_mode') - and not (frappe.flags.in_import or frappe.flags.in_test)) + and not (frappe.flags.in_import or frappe.flags.in_test or frappe.flags.in_migrate)) def on_trash(self): if self.is_standard_and_not_valid_user(): @@ -61,10 +61,13 @@ class WebsiteTheme(Document): from subprocess import Popen, PIPE folder_path = join_path(frappe.utils.get_bench_path(), 'sites', 'assets', 'css') - self.delete_old_theme_files(folder_path) + + if not self.custom: + self.delete_old_theme_files(folder_path) # add a random suffix - file_name = frappe.scrub(self.name) + '_' + frappe.generate_hash('Website Theme', 8) + '.css' + suffix = frappe.generate_hash('Website Theme', 8) if self.custom else 'style' + file_name = frappe.scrub(self.name) + '_' + suffix + '.css' output_path = join_path(folder_path, file_name) content = get_scss(self) diff --git a/frappe/website/doctype/website_theme/website_theme_template.scss b/frappe/website/doctype/website_theme/website_theme_template.scss index e1728eee36..1bb4685b98 100644 --- a/frappe/website/doctype/website_theme/website_theme_template.scss +++ b/frappe/website/doctype/website_theme/website_theme_template.scss @@ -1,21 +1,23 @@ {% if google_font %} @import url('https://fonts.googleapis.com/css?family={{ google_font.replace(' ', '+') }}:{{ font_properties }}&display=swap'); + $font-family-sans-serif: "{{ google_font }}", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -{% endif %} -{% if primary_color %}$primary: {{ primary_color }};{% endif %} -{% if dark_color %}$dark: {{ dark_color }};{% endif %} -{% if text_color %}$body-color: {{ text_color }};{% endif %} -{% if background_color %}$body-bg: {{ background_color }};{% endif %} +{% endif -%} -$enable-shadows: {{ enable_shadows and "true" or "false" }}; -$enable-gradients: {{ enable_gradients and "true" or "false" }}; -$enable-rounded: {{ enable_rounded and "true" or "false" }}; +{% if primary_color %}$primary: {{ frappe.db.get_value('Color', primary_color, 'color') }};{% endif -%} +{% if dark_color %}$dark: {{ frappe.db.get_value('Color', dark_color, 'color') }};{% endif -%} +{% if text_color %}$body-color: {{ frappe.db.get_value('Color', text_color, 'color') }};{% endif -%} +{% if background_color %}$body-bg: {{ frappe.db.get_value('Color', background_color, 'color') }};{% endif -%} + +$enable-shadows: {{ button_shadows and "true" or "false" }}; +$enable-gradients: {{ button_gradients and "true" or "false" }}; +$enable-rounded: {{ button_rounded_corners and "true" or "false" }}; @import "frappe/public/scss/website"; +{% if font_size -%} body { - {% if font_size %} font-size: {{ font_size }}; - {% endif %} } +{%- endif %} diff --git a/frappe/website/website_theme/standard/standard.json b/frappe/website/website_theme/standard/standard.json index e610d82582..a799f25425 100644 --- a/frappe/website/website_theme/standard/standard.json +++ b/frappe/website/website_theme/standard/standard.json @@ -1,26 +1,20 @@ { - "apply_style": 0, - "apply_text_styles": 0, - "creation": "2015-02-19 13:37:33.925909", - "css": ".navbar-header {\n display: none;\n}", - "custom": 0, - "docstatus": 0, - "doctype": "Website Theme", - "font_size": "14px", - "footer_color": "", - "footer_text_color": "", - "heading_style": "", - "heading_webfont": "", - "idx": 26, - "link_color": "", - "modified": "2016-12-29 05:40:17.289226", - "modified_by": "Administrator", - "module": "Website", - "name": "Standard", - "owner": "Administrator", - "text_color": "", - "text_webfont": "", - "theme": "Standard", - "top_bar_color": "", - "top_bar_text_color": "" + "button_gradients": 0, + "button_rounded_corners": 1, + "button_shadows": 0, + "creation": "2015-02-19 13:37:33.925909", + "custom": 0, + "docstatus": 0, + "doctype": "Website Theme", + "font_properties": "300,600", + "font_size": "", + "idx": 27, + "modified": "2020-04-21 02:10:31.761219", + "modified_by": "Administrator", + "module": "Website", + "name": "Standard", + "owner": "Administrator", + "theme": "Standard", + "theme_scss": "$enable-shadows: false;\n$enable-gradients: false;\n$enable-rounded: true;\n\n@import \"frappe/public/scss/website\";\n\n", + "theme_url": "/assets/css/standard_style.css" } \ No newline at end of file diff --git a/frappe/www/update-password.html b/frappe/www/update-password.html index f0ee0688d4..d12be86d12 100644 --- a/frappe/www/update-password.html +++ b/frappe/www/update-password.html @@ -9,7 +9,7 @@ {{ _("Reset Password") if frappe.db.get_default('company') else _("Set Password")}}
-
+ @@ -32,8 +32,8 @@