diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index 78f452db21..2daed59074 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -16,7 +16,7 @@ global_cache_keys = ("app_hooks", "installed_apps", 'scheduler_events', 'time_zone', 'webhooks', 'active_domains', 'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version', 'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts', - 'sitemap_routes') + 'sitemap_routes', 'db_tables') user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang", "defaults", "user_permissions", "home_page", "linked_with", diff --git a/frappe/core/doctype/data_import/test_exporter_new.py b/frappe/core/doctype/data_import/test_exporter_new.py index d17cef390b..faa48a35f4 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), 35) + self.assertEqual(len(header), 36) def test_exports_selected_fields(self): diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 0509ea9af7..904deb9990 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -712,9 +712,10 @@ def validate_fields(meta): if d.fieldtype == "Currency" and cint(d.width) < 100: frappe.throw(_("Max width for type Currency is 100px in row {0}").format(d.idx)) - def check_in_list_view(d): + def check_in_list_view(is_table, d): if d.in_list_view and (d.fieldtype in not_allowed_in_list_view): - frappe.throw(_("'In List View' not allowed for type {0} in row {1}").format(d.fieldtype, d.idx)) + property_label = 'In Grid View' if is_table else 'In List View' + frappe.throw(_("'{0}' not allowed for type {1} in row {2}").format(property_label, d.fieldtype, d.idx)) def check_in_global_search(d): if d.in_global_search and d.fieldtype in no_value_fields: @@ -906,6 +907,16 @@ def validate_fields(meta): frappe.msgprint(text_str + df_options_str, title="Invalid Data Field", raise_exception=True) + def check_child_table_option(docfield): + if docfield.fieldtype not in ['Table MultiSelect', 'Table']: return + + doctype = docfield.options + meta = frappe.get_meta(doctype) + + if not meta.istable: + frappe.throw(_('Option {0} for field {1} is not a child table') + .format(frappe.bold(doctype), frappe.bold(docfield.fieldname)), title=_("Invalid Option")) + fields = meta.get("fields") fieldname_list = [d.fieldname for d in fields] @@ -929,11 +940,12 @@ def validate_fields(meta): check_link_table_options(meta.get("name"), d) check_dynamic_link_options(d) check_hidden_and_mandatory(meta.get("name"), d) - check_in_list_view(d) + check_in_list_view(meta.get('istable'), d) check_in_global_search(d) check_illegal_default(d) check_unique_and_text(meta.get("name"), d) check_illegal_depends_on_conditions(d) + check_child_table_option(d) check_table_multiselect_option(d) scrub_options_in_select(d) scrub_fetch_from(d) diff --git a/frappe/database/database.py b/frappe/database/database.py index b083ff1014..101b97c915 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -124,6 +124,8 @@ class Database(object): # in transaction validations self.check_transaction_status(query) + self.clear_db_table_cache(query) + # autocommit if auto_commit: self.commit() @@ -277,6 +279,11 @@ class Database(object): ret.append(frappe._dict(zip(keys, values))) return ret + @staticmethod + def clear_db_table_cache(query): + if query and query.strip().split()[0].lower() in {'drop', 'create'}: + frappe.cache().delete_key('db_tables') + @staticmethod def needs_formatting(result, formatted): """Returns true if the first row in the result has a Date, Datetime, Long Int.""" @@ -769,7 +776,16 @@ class Database(object): return ("tab" + doctype) in self.get_tables() def get_tables(self): - return [d[0] for d in self.sql("select table_name from information_schema.tables where table_schema not in ('pg_catalog', 'information_schema')")] + tables = frappe.cache().get_value('db_tables') + if not tables: + table_rows = self.sql(""" + SELECT table_name + FROM information_schema.tables + WHERE table_schema NOT IN ('pg_catalog', 'information_schema') + """) + tables = {d[0] for d in table_rows} + frappe.cache().set_value('db_tables', tables) + return tables def a_row_exists(self, doctype): """Returns True if atleast one row exists.""" diff --git a/frappe/database/schema.py b/frappe/database/schema.py index 28e055f382..52dc2ba917 100644 --- a/frappe/database/schema.py +++ b/frappe/database/schema.py @@ -137,16 +137,14 @@ class DBTable: if frappe.db.is_missing_column(e): # Unknown column 'column_name' in 'field list' continue - else: - raise + raise if max_length and max_length[0][0] and max_length[0][0] > new_length: if col.fieldname in self.columns: self.columns[col.fieldname].length = current_length - - frappe.msgprint(_("""Reverting length to {0} for '{1}' in '{2}'; - Setting the length as {3} will cause truncation of data.""") - .format(current_length, col.fieldname, self.doctype, new_length)) + info_message = _("Reverting length to {0} for '{1}' in '{2}'. Setting the length as {3} will cause truncation of data.") \ + .format(current_length, col.fieldname, self.doctype, new_length) + frappe.msgprint(info_message) def is_new(self): return self.table_name not in frappe.db.get_tables() diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js index 812582ae05..184fe5e6cb 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -18,24 +18,29 @@ frappe.ui.form.on('Number Card', { }); frm.set_value('filters_json', '[]'); frm.set_value('aggregate_function_based_on', ''); - if (frm.doc.document_type) { - frm.trigger('set_options'); - } + frm.trigger('set_options'); }, set_options: function(frm) { let aggregate_based_on_fields = []; const doctype = frm.doc.document_type; - frappe.model.with_doctype(doctype, () => { - frappe.get_meta(doctype).fields.map(df => { - if (frappe.model.numeric_fieldtypes.includes(df.fieldtype)) { - aggregate_based_on_fields.push({label: df.label, value: df.fieldname}); - } - }); + if (doctype) { + frappe.model.with_doctype(doctype, () => { + frappe.get_meta(doctype).fields.map(df => { + if (frappe.model.numeric_fieldtypes.includes(df.fieldtype)) { + if (df.fieldtype == 'Currency') { + if (!df.options || df.options !== 'Company:company:default_currency') { + return; + } + } + aggregate_based_on_fields.push({label: df.label, value: df.fieldname}); + } + }); - frm.set_df_property('aggregate_function_based_on', 'options', aggregate_based_on_fields); - }); + frm.set_df_property('aggregate_function_based_on', 'options', aggregate_based_on_fields); + }); + } }, render_filters_table: function(frm) { diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py index 41be5fac47..2c5655beda 100644 --- a/frappe/desk/doctype/number_card/number_card.py +++ b/frappe/desk/doctype/number_card/number_card.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document +from frappe.utils import cint class NumberCard(Document): pass @@ -67,7 +68,7 @@ def get_result(doc, to_date=None): res = frappe.db.get_all(doc.document_type, fields=fields, filters=filters) number = res[0]['result'] if res else 0 - return number + return cint(number) @frappe.whitelist() def get_percentage_difference(doc, result): @@ -141,4 +142,3 @@ def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters): search_conditions=search_conditions, conditions=conditions ), values) - diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index c0a198f5e5..082b16c17a 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -10,7 +10,7 @@ import socket import time from frappe import _ from frappe.model.document import Document -from frappe.utils import validate_email_address, cint, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html +from frappe.utils import validate_email_address, cint, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html, add_days from frappe.utils.user import is_system_user from frappe.utils.jinja import render_template from frappe.email.smtp import SMTPServer @@ -533,28 +533,37 @@ class EmailAccount(Document): parent = None in_reply_to = (email.mail.get("In-Reply-To") or "").strip(" <>") - if in_reply_to and "@{0}".format(frappe.local.site) in in_reply_to: - # reply to a communication sent from the system - email_queue = frappe.db.get_value('Email Queue', dict(message_id=in_reply_to), ['communication','reference_doctype', 'reference_name']) - if email_queue: - parent_communication, parent_doctype, parent_name = email_queue - if parent_communication: - communication.in_reply_to = parent_communication + if in_reply_to: + if "@{0}".format(frappe.local.site) in in_reply_to: + # reply to a communication sent from the system + email_queue = frappe.db.get_value('Email Queue', dict(message_id=in_reply_to), ['communication','reference_doctype', 'reference_name']) + if email_queue: + parent_communication, parent_doctype, parent_name = email_queue + if parent_communication: + communication.in_reply_to = parent_communication + else: + reference, domain = in_reply_to.split("@", 1) + parent_doctype, parent_name = 'Communication', reference + + if frappe.db.exists(parent_doctype, parent_name): + parent = frappe._dict(doctype=parent_doctype, name=parent_name) + + # set in_reply_to of current communication + if parent_doctype=='Communication': + # communication.in_reply_to = email_queue.communication + + if parent.reference_name: + # the true parent is the communication parent + parent = frappe.get_doc(parent.reference_doctype, + parent.reference_name) else: - reference, domain = in_reply_to.split("@", 1) - parent_doctype, parent_name = 'Communication', reference - - if frappe.db.exists(parent_doctype, parent_name): - parent = frappe._dict(doctype=parent_doctype, name=parent_name) - - # set in_reply_to of current communication - if parent_doctype=='Communication': - # communication.in_reply_to = email_queue.communication - - if parent.reference_name: - # the true parent is the communication parent - parent = frappe.get_doc(parent.reference_doctype, - parent.reference_name) + comm = frappe.db.get_value('Communication', + dict( + message_id=in_reply_to, + creation=['>=', add_days(get_datetime(), -30)]), + ['reference_doctype', 'reference_name'], as_dict=1) + if comm: + parent = frappe._dict(doctype=comm.reference_doctype, name=comm.reference_name) return parent diff --git a/frappe/handler.py b/frappe/handler.py index 6e0bf7a6be..e5a7f742ae 100755 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -14,6 +14,12 @@ from frappe.core.doctype.server_script.server_script_utils import run_server_scr from werkzeug.wrappers import Response from six import string_types +ALLOWED_MIMETYPES = ('image/png', 'image/jpeg', 'application/pdf', 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.spreadsheet') + + def handle(): """handle request""" validate_auth() @@ -148,12 +154,14 @@ def uploadfile(): @frappe.whitelist(allow_guest=True) def upload_file(): + user = None if frappe.session.user == 'Guest': if frappe.get_system_settings('allow_guests_to_upload_files'): ignore_permissions = True else: return else: + user = frappe.get_doc("User", frappe.session.user) ignore_permissions = False files = frappe.request.files @@ -175,11 +183,11 @@ def upload_file(): frappe.local.uploaded_file = content frappe.local.uploaded_filename = filename - if frappe.session.user == 'Guest': + if frappe.session.user == 'Guest' or (user and not user.has_desk_access()): import mimetypes filetype = mimetypes.guess_type(filename)[0] - if filetype not in ['image/png', 'image/jpeg', 'application/pdf']: - frappe.throw("You can only upload JPG, PNG or PDF files.") + if filetype not in ALLOWED_MIMETYPES: + frappe.throw(_("You can only upload JPG, PNG, PDF, or Microsoft documents.")) if method: method = frappe.get_attr(method) diff --git a/frappe/patches.txt b/frappe/patches.txt index 97a79df889..dfae76a671 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -274,3 +274,4 @@ frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders() frappe.patches.v13_0.website_theme_custom_scss frappe.patches.v13_0.set_existing_dashboard_charts_as_public +frappe.patches.v13_0.set_path_for_homepage_in_web_page_view diff --git a/frappe/patches/v13_0/set_existing_dashboard_charts_as_public.py b/frappe/patches/v13_0/set_existing_dashboard_charts_as_public.py index 36e83e14e3..9b7442167d 100644 --- a/frappe/patches/v13_0/set_existing_dashboard_charts_as_public.py +++ b/frappe/patches/v13_0/set_existing_dashboard_charts_as_public.py @@ -1,7 +1,7 @@ import frappe def execute(): - frappe.reload_doctype('Dashboard Chart') + frappe.reload_doc('desk', 'doctype', 'dashboard_chart') if not frappe.db.table_exists('Dashboard Chart'): return diff --git a/frappe/patches/v13_0/set_path_for_homepage_in_web_page_view.py b/frappe/patches/v13_0/set_path_for_homepage_in_web_page_view.py new file mode 100644 index 0000000000..66f878e4bf --- /dev/null +++ b/frappe/patches/v13_0/set_path_for_homepage_in_web_page_view.py @@ -0,0 +1,5 @@ +import frappe + +def execute(): + frappe.reload_doc('website', 'doctype', 'web_page_view', force=True) + frappe.db.sql("""UPDATE `tabWeb Page View` set path="/" where path=''""") diff --git a/frappe/public/js/frappe/chat.js b/frappe/public/js/frappe/chat.js index 8b1c09ac93..f54b9e5cbe 100644 --- a/frappe/public/js/frappe/chat.js +++ b/frappe/public/js/frappe/chat.js @@ -2523,7 +2523,7 @@ class extends Component { h("div",{class:"input-group input-group-lg"}, !frappe._.is_empty(props.actions) ? h("div",{class:"input-group-btn dropup"}, - h(frappe.components.Button,{ class: "dropdown-toggle", "data-toggle": "dropdown"}, + h(frappe.components.Button,{ class: (frappe.session.user === "Guest" ? "disabled" : "dropdown-toggle"), "data-toggle": "dropdown"}, h(frappe.components.FontAwesome, { class: "text-muted", type: "paperclip", fixed: true }) ), h("div",{ class:"dropdown-menu dropdown-menu-left", onclick: e => e.stopPropagation() }, diff --git a/frappe/public/js/frappe/utils/common.js b/frappe/public/js/frappe/utils/common.js index 6ad15e44bf..1cdabf23e0 100644 --- a/frappe/public/js/frappe/utils/common.js +++ b/frappe/public/js/frappe/utils/common.js @@ -261,26 +261,22 @@ frappe.utils.xss_sanitise = function (string, options) { } frappe.utils.sanitise_redirect = (url) => { - const is_absolute = ((url) => { - // https://github.com/sindresorhus/is-absolute-url - // Don't match Windows paths `c:\` - if (/^[a-zA-Z]:\\/.test(url)) { - return false; - } - - // Scheme: https://tools.ietf.org/html/rfc3986#section-3.1 - // Absolute URL: https://tools.ietf.org/html/rfc3986#section-4.3 - return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url); - }); - const is_external = (() => { return (url) => { function domain(url) { - let base_domain = /https?:\/\/((?:[\w\d]+\.)+[\w\d]{2,})/i.exec(url); + let base_domain = /^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:/\n?]+)/img.exec(url); return base_domain == null ? "" : base_domain[1]; } - return domain(location.href) !== domain(url); + function is_absolute(url) { + // returns true for url that have a defined scheme + // anything else, eg. internal urls return false + return /^(?:[a-z]+:)?\/\//i.test(url); + } + + // check for base domain only if the url is absolute + // return true for relative url (except protocol-relative urls) + return is_absolute(url) ? domain(location.href) !== domain(url) : true; } })(); @@ -291,11 +287,16 @@ frappe.utils.sanitise_redirect = (url) => { return url.replace(REGEX_SCRIPT, ""); }); - if (is_absolute(url) && is_external(url)) { - return ''; - } + url = frappe.utils.strip_url(url); - return sanitise_javascript(frappe.utils.xss_sanitise(url, {strategies: ["js"]})); + return is_external(url) ? "" : sanitise_javascript(frappe.utils.xss_sanitise(url, {strategies: ["js"]})); +}; + +frappe.utils.strip_url = (url) => { + // strips invalid characters from the beginning of the URL + // in our case, the url can start with either a protocol, //, or even # + // so anything except those characters can be considered invalid + return url.replace(/^[^A-Za-z0-9(//)#]+/g, ''); } frappe.utils.new_auto_repeat_prompt = function(frm) { diff --git a/frappe/public/js/frappe/views/dashboard/dashboard_view.js b/frappe/public/js/frappe/views/dashboard/dashboard_view.js index 40cbd1986f..3e6a44a953 100644 --- a/frappe/public/js/frappe/views/dashboard/dashboard_view.js +++ b/frappe/public/js/frappe/views/dashboard/dashboard_view.js @@ -71,12 +71,14 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView { { chart_type: ['in', ['Count', 'Sum', 'Group By']], document_type: this.doctype, + is_public: 1, }, 'charts' ), () => this.fetch_dashboard_items('Number Card', { document_type: this.doctype, + is_public: 1, }, 'number_cards' ), @@ -427,6 +429,11 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView { date_fields.push({label: df.label, value: df.fieldname}); } if (frappe.model.numeric_fieldtypes.includes(df.fieldtype)) { + if (df.fieldtype == 'Currency') { + if (!df.options || df.options !== 'Company:company:default_currency') { + return; + } + } value_fields.push({label: df.label, value: df.fieldname}); aggregate_function_fields.push({label: df.label, value: df.fieldname}); } diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index 741b7cc6ea..ba96a1959e 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -32,8 +32,10 @@ export default class NumberCardWidget extends Widget { frappe.model.with_doc('Number Card', this.name).then(card => { if (!card) { if (this.document_type) { - this.create_number_card(); - this.render_card(); + frappe.run_serially([ + () => this.create_number_card(), + () => this.render_card(), + ]); } else { // widget doesn't exist so delete this.delete(false); @@ -50,13 +52,14 @@ export default class NumberCardWidget extends Widget { create_number_card() { this.set_doc_args(); - frappe.xcall( + return frappe.xcall( 'frappe.desk.doctype.number_card.number_card.create_number_card', { 'args': this.card_doc } ).then(doc => { this.name = doc.name; + this.card_doc.name = this.name; this.widget.attr('data-widget-name', this.name); }); } diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js index a9ea8637cf..31215a40c3 100644 --- a/frappe/public/js/frappe/widgets/widget_dialog.js +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -372,6 +372,11 @@ class NumberCardDialog extends WidgetDialog { if (this.document_type) { frappe.get_meta(this.document_type).fields.map(df => { if (frappe.model.numeric_fieldtypes.includes(df.fieldtype)) { + if (df.fieldtype == 'Currency') { + if (!df.options || df.options !== 'Company:company:default_currency') { + return; + } + } aggregate_function_fields.push({label: df.label, value: df.fieldname}); } }); diff --git a/frappe/public/less/controls.less b/frappe/public/less/controls.less index d88e6adaec..564c77c07f 100644 --- a/frappe/public/less/controls.less +++ b/frappe/public/less/controls.less @@ -162,6 +162,6 @@ top: 8px; height: 15px; right: 12px; - top: 8px; + pointer-events: none; } } diff --git a/frappe/public/tailwind.css b/frappe/public/tailwind.css index 270b70a2fc..89595f95ba 100644 --- a/frappe/public/tailwind.css +++ b/frappe/public/tailwind.css @@ -13,11 +13,10 @@ details.hide-summary-arrow summary::-webkit-details-marker { } .from-markdown { - @apply text-gray-900; @apply leading-relaxed; > * + * { - @apply mt-6; + @apply mt-4; } > :first-child { @@ -50,28 +49,45 @@ details.hide-summary-arrow summary::-webkit-details-marker { @apply px-4 py-3 text-sm font-medium text-gray-900 border border-gray-400 rounded-md bg-gray-50; } - > h1 { - @apply text-4xl; - @apply mt-16; - @apply mb-4; - @apply leading-none; - @apply font-bold; + h1 { + @apply mt-16 mb-4 text-3xl font-extrabold leading-tight tracking-tight; + @screen sm { + @apply text-4xl leading-10; + } + @screen xl { + @apply text-5xl leading-none; + } } - > h2 { - @apply mt-16; - @apply mb-4; - @apply leading-none; - @apply font-bold; - @apply text-3xl; + h1 + p { + @apply max-w-2xl mt-3 text-base text-gray-900; + + @screen sm { + @apply mt-5 text-lg; + } + @screen md { + @apply mt-5 text-xl; + } } - > h3 { - @apply mt-16; - @apply mb-4; - @apply leading-none; - @apply font-bold; - @apply text-2xl; + h2 { + @apply mb-4 text-2xl font-bold leading-tight mt-14; + } + + h3 { + @apply mt-12 mb-4 text-xl font-semibold leading-tight; + } + + h4 { + @apply mt-10 mb-4 text-lg font-semibold leading-tight; + } + + h5 { + @apply mt-8 mb-4 text-base font-semibold leading-tight; + } + + h6 { + @apply mt-6 mb-4 text-sm font-semibold leading-tight; } > a, @@ -84,9 +100,17 @@ details.hide-summary-arrow summary::-webkit-details-marker { } } + table { + @apply w-full my-8 border-t; + } + + tbody { + @apply border-t; + } + tr > td, tr > th { - @apply px-4 py-2 border border-gray-400; + @apply py-4 pr-6 text-sm leading-6 text-left border-b; } th:empty { diff --git a/frappe/templates/components/web_block.html b/frappe/templates/components/web_block.html index d898417970..f0ed8f2df4 100644 --- a/frappe/templates/components/web_block.html +++ b/frappe/templates/components/web_block.html @@ -7,6 +7,7 @@ web_block.css_class ]) -%} +{%- if not web_block.hide_block -%} <{{htmltag}} class="{{ classes }}" data-section-idx="{{ web_block.idx | e }}" data-section-template="{{ web_block.web_template | e }}"> {%- if web_block.add_container -%} @@ -17,3 +18,4 @@ {%- endif -%} +{%- endif -%} diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index 74d6337f74..24b3399097 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -183,7 +183,7 @@ login.login_handlers = (function() { login.set_indicator('{{ _("Success") }}', 'green'); window.location.href = frappe.utils.sanitise_redirect(frappe.utils.get_url_arg("redirect-to")) || data.home_page; } else if(data.message == 'Password Reset'){ - window.location.href = data.redirect_to; + window.location.href = frappe.utils.sanitise_redirect(data.redirect_to); } else if(data.message=="No App") { login.set_indicator("{{ _("Success") }}", 'green'); if(localStorage) { @@ -194,7 +194,7 @@ login.login_handlers = (function() { } if(data.redirect_to) { - window.location.href = data.redirect_to; + window.location.href = frappe.utils.sanitise_redirect(data.redirect_to); } if(last_visited && last_visited != "/login") { diff --git a/frappe/tests/test_permissions.py b/frappe/tests/test_permissions.py index 7e9416f68a..364469f168 100644 --- a/frappe/tests/test_permissions.py +++ b/frappe/tests/test_permissions.py @@ -201,7 +201,7 @@ class TestPermissions(unittest.TestCase): doc = frappe.get_doc("DocType", "Blog Post") # change one property from the child table - doc.fields[-1].fieldtype = 'HTML' + doc.fields[-1].fieldtype = 'Check' self.assertRaises(frappe.CannotChangeConstantError, doc.save) frappe.clear_cache(doctype='DocType') diff --git a/frappe/twofactor.py b/frappe/twofactor.py index b44092d6a1..253636764f 100644 --- a/frappe/twofactor.py +++ b/frappe/twofactor.py @@ -30,7 +30,7 @@ def two_factor_is_enabled(user=None): if bypass_two_factor_auth and user: user_doc = frappe.get_doc("User", user) restrict_ip_list = user_doc.get_restricted_ip_list() #can be None or one or more than one ip address - if restrict_ip_list: + if restrict_ip_list and frappe.local.request_ip: for ip in restrict_ip_list: if frappe.local.request_ip.startswith(ip): enabled = False diff --git a/frappe/website/doctype/blog_post/blog_post.js b/frappe/website/doctype/blog_post/blog_post.js index e1b8341139..adc03ca77e 100644 --- a/frappe/website/doctype/blog_post/blog_post.js +++ b/frappe/website/doctype/blog_post/blog_post.js @@ -3,6 +3,40 @@ frappe.ui.form.on('Blog Post', { refresh: function(frm) { - + generate_google_search_preview(frm); + }, + title: function(frm) { + generate_google_search_preview(frm); + }, + meta_description: function(frm) { + generate_google_search_preview(frm); + }, + blog_intro: function(frm) { + generate_google_search_preview(frm); } }); + +function generate_google_search_preview(frm) { + let google_preview = frm.get_field("google_preview"); + let seo_title = (frm.doc.title).slice(0, 60); + let seo_description = (frm.doc.meta_description || frm.doc.blog_intro || "").slice(0, 160); + let date = frm.doc.published_on ? new frappe.datetime.datetime(frm.doc.published_on).moment.format('ll') + ' - ' : ''; + let route_array = frm.doc.route.split('/'); + route_array.pop(); + + google_preview.html(` + +
+ + ${frappe.boot.sitename} + › ${route_array.join(' › ')} + +
+ ${ seo_title } +
+

+ ${ date } ${ seo_description } +

+
+ `); +} diff --git a/frappe/website/doctype/blog_post/blog_post.json b/frappe/website/doctype/blog_post/blog_post.json index 9944cbf4b2..04e349a2b0 100644 --- a/frappe/website/doctype/blog_post/blog_post.json +++ b/frappe/website/doctype/blog_post/blog_post.json @@ -21,7 +21,13 @@ "content", "content_md", "content_html", - "email_sent" + "email_sent", + "meta_tags", + "meta_description", + "column_break_18", + "meta_image", + "section_break_20", + "google_preview" ], "fields": [ { @@ -123,6 +129,36 @@ "fieldname": "disable_comments", "fieldtype": "Check", "label": "Disable Comments" + }, + { + "fieldname": "meta_description", + "fieldtype": "Small Text", + "label": "Meta Description" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "fieldname": "meta_image", + "fieldtype": "Attach Image", + "label": "Meta Image" + }, + { + "fieldname": "section_break_20", + "fieldtype": "Section Break" + }, + { + "description": "This is an example Google SERP Preview.", + "fieldname": "google_preview", + "fieldtype": "HTML", + "label": "Google Snippet Preview", + "read_only": 1 + }, + { + "fieldname": "meta_tags", + "fieldtype": "Section Break", + "label": "Meta Tags" } ], "has_web_view": 1, @@ -131,7 +167,7 @@ "is_published_field": "published", "links": [], "max_attachments": 5, - "modified": "2020-04-08 19:58:13.672332", + "modified": "2020-04-29 17:32:41.055883", "modified_by": "Administrator", "module": "Website", "name": "Blog Post", diff --git a/frappe/website/doctype/blog_post/blog_post.py b/frappe/website/doctype/blog_post/blog_post.py index 148ba15be7..e2e24afbab 100644 --- a/frappe/website/doctype/blog_post/blog_post.py +++ b/frappe/website/doctype/blog_post/blog_post.py @@ -65,16 +65,18 @@ class BlogPost(WebsiteGenerator): context.content = get_html_content_based_on_type(self, 'content', self.content_type) - context.description = self.blog_intro or strip_html_tags(context.content[:140]) + + #if meta description is not present, then blog intro or first 140 characters of the blog will be set as description + context.description = self.meta_description or self.blog_intro or strip_html_tags(context.content[:140]) context.metatags = { "name": self.title, "description": context.description, } + #if meta image is not present, then first image inside the blog will be set as the meta image image = find_first_image(context.content) - if image: - context.metatags["image"] = image + context.metatags["image"] = self.meta_image or image or None self.load_comments(context) @@ -95,7 +97,6 @@ class BlogPost(WebsiteGenerator): else: context.comment_text = _('{0} comments').format(len(context.comment_list)) - def get_list_context(context=None): list_context = frappe._dict( template = "templates/includes/blog/blog.html", diff --git a/frappe/website/doctype/blog_post/templates/blog_post.html b/frappe/website/doctype/blog_post/templates/blog_post.html index 285223a2af..ab3c2afa1a 100644 --- a/frappe/website/doctype/blog_post/templates/blog_post.html +++ b/frappe/website/doctype/blog_post/templates/blog_post.html @@ -15,7 +15,7 @@

- {{ description }} + {{ blog_intro }}

{{ content }} diff --git a/frappe/website/doctype/web_page_block/web_page_block.json b/frappe/website/doctype/web_page_block/web_page_block.json index b415567bd0..77f2e25469 100644 --- a/frappe/website/doctype/web_page_block/web_page_block.json +++ b/frappe/website/doctype/web_page_block/web_page_block.json @@ -12,7 +12,8 @@ "column_break_5", "add_container", "add_padding", - "add_shade" + "add_shade", + "hide_block" ], "fields": [ { @@ -54,18 +55,24 @@ "default": "0", "fieldname": "add_shade", "fieldtype": "Check", - "label": "Shaded Section" + "label": "Add Gray Background" }, { "default": "1", "fieldname": "add_container", "fieldtype": "Check", "label": "Add Container" + }, + { + "default": "0", + "fieldname": "hide_block", + "fieldtype": "Check", + "label": "Hide Block" } ], "istable": 1, "links": [], - "modified": "2020-04-19 16:16:44.524042", + "modified": "2020-04-29 15:08:25.976179", "modified_by": "Administrator", "module": "Website", "name": "Web Page Block", diff --git a/frappe/website/doctype/web_page_view/web_page_view.py b/frappe/website/doctype/web_page_view/web_page_view.py index 08625f9d6f..93cf1d7bb8 100644 --- a/frappe/website/doctype/web_page_view/web_page_view.py +++ b/frappe/website/doctype/web_page_view/web_page_view.py @@ -19,7 +19,7 @@ def make_view_log(path, referrer=None, browser=None, version=None, url=None, use if referrer.startswith(url): is_unique = False - if path.startswith('/'): + if path != "/" and path.startswith('/'): path = path[1:] if is_tracking_enabled(): diff --git a/frappe/website/doctype/web_template_field/web_template_field.json b/frappe/website/doctype/web_template_field/web_template_field.json index 09335adf3a..900655e207 100644 --- a/frappe/website/doctype/web_template_field/web_template_field.json +++ b/frappe/website/doctype/web_template_field/web_template_field.json @@ -31,7 +31,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Fieldtype", - "options": "Attach Image\nCheck\nData\nInt\nSelect\nSmall Text\nText\nMarkdown Editor", + "options": "Attach Image\nCheck\nData\nInt\nSelect\nSmall Text\nText\nMarkdown Editor\nSection Break\nColumn Break", "reqd": 1 }, { @@ -48,7 +48,7 @@ ], "istable": 1, "links": [], - "modified": "2020-04-24 17:05:25.322767", + "modified": "2020-04-29 14:53:23.192395", "modified_by": "Administrator", "module": "Website", "name": "Web Template Field", diff --git a/frappe/website/web_template/hero/hero.html b/frappe/website/web_template/hero/hero.html index a3fb4aa466..e7b69ab782 100644 --- a/frappe/website/web_template/hero/hero.html +++ b/frappe/website/web_template/hero/hero.html @@ -9,22 +9,12 @@

{%- endif -%} {%- if primary_action or secondary_action -%} -
+
{%- if primary_action -%} - + {{ c('button', label=primary_action_label, url=primary_action, variant="primary", size="large") }} {%- endif -%} {%- if secondary_action -%} - + {{ c('button', label=secondary_action_label, url=secondary_action, variant="secondary", size="large", class="ml-4") }} {%- endif -%}
{%- endif -%} diff --git a/frappe/website/web_template/hero_with_right_image/hero_with_right_image.html b/frappe/website/web_template/hero_with_right_image/hero_with_right_image.html index 57a1a02af7..8efc46a1b7 100644 --- a/frappe/website/web_template/hero_with_right_image/hero_with_right_image.html +++ b/frappe/website/web_template/hero_with_right_image/hero_with_right_image.html @@ -1,5 +1,5 @@
-
+

@@ -22,8 +22,16 @@

{%- if image -%} - + {{ c('image_with_blur', + class=["hidden md:block max-h-144", "w-full md:w-6/12" if contain_image else "md:max-w-md lg:max-w-lg xl:max-w-xl xxl:max-w-2xl"], + src=image, + alt="") + }} {%- endif -%}
+ +{%- if not contain_image -%} + +{%- endif -%} diff --git a/frappe/website/web_template/hero_with_right_image/hero_with_right_image.json b/frappe/website/web_template/hero_with_right_image/hero_with_right_image.json index bb841a3156..3cb4701e7c 100644 --- a/frappe/website/web_template/hero_with_right_image/hero_with_right_image.json +++ b/frappe/website/web_template/hero_with_right_image/hero_with_right_image.json @@ -21,6 +21,12 @@ "label": "Image", "reqd": 0 }, + { + "fieldname": "contain_image", + "fieldtype": "Check", + "label": "Restrict Image inside Container", + "reqd": 0 + }, { "fieldname": "primary_action_label", "fieldtype": "Data", @@ -47,7 +53,7 @@ } ], "idx": 0, - "modified": "2020-04-26 15:08:26.937576", + "modified": "2020-04-29 14:12:31.613545", "modified_by": "Administrator", "name": "Hero with Right Image", "owner": "Administrator", diff --git a/frappe/website/web_template/navbar_with_links_on_right/navbar_with_links_on_right.html b/frappe/website/web_template/navbar_with_links_on_right/navbar_with_links_on_right.html index ffa8be24da..01652f8c80 100644 --- a/frappe/website/web_template/navbar_with_links_on_right/navbar_with_links_on_right.html +++ b/frappe/website/web_template/navbar_with_links_on_right/navbar_with_links_on_right.html @@ -6,8 +6,8 @@
- -