diff --git a/README.md b/README.md index 860958087e..7545249610 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

- + frappe

@@ -33,8 +33,8 @@ Full-stack web application framework that uses Python and MariaDB on the server side and a tightly integrated client side library. Built for [ERPNext](https://erpnext.com) ### Table of Contents -* [Installation](#installation) -* [Documentation](https://frappe.io/docs) +* [Installation](https://frappeframework.com/docs/user/en/installation) +* [Documentation](https://frappeframework.com/docs) * [License](#license) ### Installation @@ -49,7 +49,7 @@ Full-stack web application framework that uses Python and MariaDB on the server ### Website For details and documentation, see the website -[https://frappe.io](https://frappe.io) +[https://frappeframework.com](https://frappeframework.com) ### License This repository has been released under the [MIT License](LICENSE). diff --git a/cypress/integration/control_duration.js b/cypress/integration/control_duration.js index f304abd3d9..edad759216 100644 --- a/cypress/integration/control_duration.js +++ b/cypress/integration/control_duration.js @@ -4,14 +4,14 @@ context('Control Duration', () => { cy.visit('/desk#workspace/Website'); }); - function get_dialog_with_duration(show_days=1, show_seconds=1) { + function get_dialog_with_duration(hide_days=0, hide_seconds=0) { return cy.dialog({ title: 'Duration', fields: [{ 'fieldname': 'duration', 'fieldtype': 'Duration', - 'show_seconds': show_days, - 'show_days': show_seconds + 'hide_days': hide_days, + 'hide_seconds': hide_seconds }] }); } @@ -37,7 +37,7 @@ context('Control Duration', () => { }); it('should hide days or seconds according to duration options', () => { - get_dialog_with_duration(0, 0).as('dialog'); + get_dialog_with_duration(1, 1).as('dialog'); cy.get('.frappe-control[data-fieldname=duration] input').first().click(); cy.get('.duration-input[data-duration=days]').should('not.be.visible'); cy.get('.duration-input[data-duration=seconds]').should('not.be.visible'); diff --git a/cypress/integration/grid_pagination.js b/cypress/integration/grid_pagination.js index f03384cb93..b383f30bb8 100644 --- a/cypress/integration/grid_pagination.js +++ b/cypress/integration/grid_pagination.js @@ -40,12 +40,12 @@ context('Grid Pagination', () => { cy.get('@table').find('.current-page-number').should('contain', '20'); cy.get('@table').find('.total-page-number').should('contain', '20'); }); - it('deletes all rows', ()=> { - cy.visit('/desk#Form/Contact/Test Contact'); - cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); - cy.get('@table').find('.grid-heading-row .grid-row-check').click({force: true}); - cy.get('@table').find('button.grid-remove-all-rows').click(); - cy.get('.modal-dialog .btn-primary').contains('Yes').click(); - cy.get('@table').find('.grid-body .grid-row').should('have.length', 0); - }); + // it('deletes all rows', ()=> { + // cy.visit('/desk#Form/Contact/Test Contact'); + // cy.get('.frappe-control[data-fieldname="phone_nos"]').as('table'); + // cy.get('@table').find('.grid-heading-row .grid-row-check').click({force: true}); + // cy.get('@table').find('button.grid-remove-all-rows').click(); + // cy.get('.modal-dialog .btn-primary').contains('Yes').click(); + // cy.get('@table').find('.grid-body .grid-row').should('have.length', 0); + // }); }); \ No newline at end of file diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 86db7cdc8f..343dc6e2bc 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -502,7 +502,17 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), if coverage: # Generate coverage report only for app that is being tested source_path = os.path.join(get_bench_path(), 'apps', app or 'frappe') - cov = Coverage(source=[source_path], omit=['*.html', '*.js', '*.xml', '*.css', '*/doctype/*/*_dashboard.py', '*/patches/*']) + cov = Coverage(source=[source_path], omit=[ + '*.html', + '*.js', + '*.xml', + '*.css', + '*.less', + '*.scss', + '*.vue', + '*/doctype/*/*_dashboard.py', + '*/patches/*' + ]) cov.start() ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests, diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index 20e4774add..232d485f36 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -444,24 +444,48 @@ def update_parent_document_on_communication(doc): status_field = parent.meta.get_field("status") if status_field: - options = (status_field.options or '').splitlines() + options = (status_field.options or "").splitlines() # if status has a "Replied" option, then update the status for received communication - if ('Replied' in options) and doc.sent_or_received=="Received": + if ("Replied" in options) and doc.sent_or_received == "Received": parent.db_set("status", "Open") + parent.run_method("handle_hold_time", "Replied") apply_assignment_rule(parent) else: # update the modified date for document parent.update_modified() update_mins_to_first_communication(parent, doc) - parent.run_method('notify_communication', doc) + set_avg_response_time(parent, doc) + parent.run_method("notify_communication", doc) parent.notify_update() def update_mins_to_first_communication(parent, communication): - if parent.meta.has_field('mins_to_first_response') and not parent.get('mins_to_first_response'): + if parent.meta.has_field("mins_to_first_response") and not parent.get("mins_to_first_response"): if is_system_user(communication.sender): first_responded_on = communication.creation - if parent.meta.has_field('first_responded_on') and communication.sent_or_received == "Sent": - parent.db_set('first_responded_on', first_responded_on) - parent.db_set('mins_to_first_response', round(time_diff_in_seconds(first_responded_on, parent.creation) / 60), 2) + if parent.meta.has_field("first_responded_on") and communication.sent_or_received == "Sent": + parent.db_set("first_responded_on", first_responded_on) + parent.db_set("mins_to_first_response", round(time_diff_in_seconds(first_responded_on, parent.creation) / 60), 2) + +def set_avg_response_time(parent, communication): + if parent.meta.has_field("avg_response_time") and communication.sent_or_received == "Sent": + # avg response time for all the responses + communications = frappe.get_list("Communication", filters={ + "reference_doctype": parent.doctype, + "reference_name": parent.name + }, + fields=["sent_or_received", "name", "creation"], + order_by="creation" + ) + + if len(communications): + response_times = [] + for i in range(len(communications)): + if communications[i].sent_or_received == "Sent" and communications[i-1].sent_or_received == "Received": + response_time = round(time_diff_in_seconds(communications[i].creation, communications[i-1].creation), 2) + if response_time > 0: + response_times.append(response_time) + if response_times: + avg_response_time = sum(response_times) / len(response_times) + parent.db_set("avg_response_time", avg_response_time) \ No newline at end of file diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index 83d3c18453..aab59a5a0a 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -13,8 +13,8 @@ "fieldname", "precision", "length", - "show_days", - "show_seconds", + "hide_days", + "hide_seconds", "reqd", "search_index", "in_list_view", @@ -453,18 +453,18 @@ "fieldtype": "Column Break" }, { - "default": "1", - "depends_on": "eval:doc.fieldtype === \"Duration\";", - "fieldname": "show_days", + "default": "0", + "depends_on": "eval:doc.fieldtype=='Duration'", + "fieldname": "hide_days", "fieldtype": "Check", - "label": "Show Days" + "label": "Hide Days" }, { - "default": "1", - "depends_on": "eval:doc.fieldtype === \"Duration\";", - "fieldname": "show_seconds", + "default": "0", + "depends_on": "eval:doc.fieldtype=='Duration'", + "fieldname": "hide_seconds", "fieldtype": "Check", - "label": "Show Seconds" + "label": "Hide Seconds" }, { "default": "0", @@ -477,7 +477,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-05-15 09:06:25.224411", + "modified": "2020-02-06 09:06:25.224413", "modified_by": "Administrator", "module": "Core", "name": "DocField", diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 904deb9990..6ca3cccdba 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -688,6 +688,9 @@ def validate_fields(meta): def check_link_table_options(docname, d): if frappe.flags.in_patch: return + + if frappe.flags.in_fixtures: return + if d.fieldtype in ("Link",) + table_fields: if not d.options: frappe.throw(_("{0}: Options required for Link or Table type field {1} in row {2}").format(docname, d.label, d.idx), DoctypeLinkError) @@ -908,6 +911,8 @@ 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 frappe.flags.in_fixtures: return if docfield.fieldtype not in ['Table MultiSelect', 'Table']: return doctype = docfield.options diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json index 77490c8c43..6fa7b29161 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -16,8 +16,8 @@ "column_break_6", "fieldtype", "precision", - "show_seconds", - "show_days", + "hide_seconds", + "hide_days", "options", "fetch_from", "fetch_if_empty", @@ -383,22 +383,18 @@ "label": "In Preview" }, { - "default": "1", - "depends_on": "eval:doc.fieldtype === \"Duration\";", - "fieldname": "show_seconds", + "default": "0", + "depends_on": "eval:doc.fieldtype=='Duration'", + "fieldname": "hide_seconds", "fieldtype": "Check", - "label": "Show Seconds", - "show_days": 1, - "show_seconds": 1 + "label": "Hide Seconds" }, { - "default": "1", - "depends_on": "eval:doc.fieldtype === \"Duration\";", - "fieldname": "show_days", + "default": "0", + "depends_on": "eval:doc.fieldtype=='Duration'", + "fieldname": "hide_days", "fieldtype": "Check", - "label": "Show Days", - "show_days": 1, - "show_seconds": 1 + "label": "Hide Days" }, { "default": "0", @@ -411,7 +407,7 @@ "icon": "fa fa-glass", "idx": 1, "links": [], - "modified": "2020-05-15 23:43:00.123572", + "modified": "2020-02-06 23:43:00.123575", "modified_by": "Administrator", "module": "Custom", "name": "Custom Field", diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 6a54d9c7e6..d4eeba3f93 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -77,7 +77,9 @@ docfield_properties = { 'allow_bulk_edit': 'Check', 'auto_repeat': 'Link', 'allow_in_quick_entry': 'Check', - 'hide_border': 'Check' + 'hide_border': 'Check', + 'hide_days': 'Check', + 'hide_seconds': 'Check' } allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'), 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 f422c36e61..267213517c 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -11,8 +11,8 @@ "label", "fieldtype", "fieldname", - "show_seconds", - "show_days", + "hide_seconds", + "hide_days", "reqd", "unique", "in_list_view", @@ -393,22 +393,18 @@ "label": "In Preview" }, { - "default": "1", - "depends_on": "eval:doc.fieldtype === \"Duration\";", - "fieldname": "show_seconds", + "default": "0", + "depends_on": "eval:doc.fieldtype=='Duration'", + "fieldname": "hide_seconds", "fieldtype": "Check", - "label": "Show Seconds", - "show_days": 1, - "show_seconds": 1 + "label": "Hide Seconds" }, { - "default": "1", - "depends_on": "eval:doc.fieldtype === \"Duration\";", - "fieldname": "show_days", + "default": "0", + "depends_on": "eval:doc.fieldtype=='Duration'", + "fieldname": "hide_days", "fieldtype": "Check", - "label": "Show Days", - "show_days": 1, - "show_seconds": 1 + "label": "Hide Days" }, { "default": "0", @@ -421,7 +417,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-05-15 23:45:46.810869", + "modified": "2020-06-02 23:45:46.810868", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql index bd93069a3f..af537e0612 100644 --- a/frappe/database/mariadb/framework_mariadb.sql +++ b/frappe/database/mariadb/framework_mariadb.sql @@ -64,6 +64,8 @@ CREATE TABLE `tabDocField` ( `length` int(11) NOT NULL DEFAULT 0, `translatable` int(1) NOT NULL DEFAULT 0, `hide_border` int(1) NOT NULL DEFAULT 0, + `hide_days` int(1) NOT NULL DEFAULT 0, + `hide_seconds` int(1) NOT NULL DEFAULT 0, PRIMARY KEY (`name`), KEY `parent` (`parent`), KEY `label` (`label`), diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql index 76309e7347..8f77ed6230 100644 --- a/frappe/database/postgres/framework_postgres.sql +++ b/frappe/database/postgres/framework_postgres.sql @@ -64,6 +64,8 @@ CREATE TABLE "tabDocField" ( "length" bigint NOT NULL DEFAULT 0, "translatable" smallint NOT NULL DEFAULT 0, "hide_border" smallint NOT NULL DEFAULT 0, + "hide_days" smallint NOT NULL DEFAULT 0, + "hide_seconds" smallint NOT NULL DEFAULT 0, PRIMARY KEY ("name") ) ; diff --git a/frappe/hooks.py b/frappe/hooks.py index 200280f6de..f5a8701089 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -56,6 +56,8 @@ website_route_rules = [ {"from_route": "/profile", "to_route": "me"}, ] +base_template = "templates/base.html" + write_file_keys = ["file_url", "file_name"] notification_config = "frappe.core.notifications.get_notification_config" @@ -270,7 +272,10 @@ setup_wizard_exception = [ ] before_migrate = ['frappe.patches.v11_0.sync_user_permission_doctype_before_migrate.execute'] -after_migrate = ['frappe.website.doctype.website_theme.website_theme.generate_theme_files_if_not_exist'] +after_migrate = [ + 'frappe.website.doctype.website_theme.website_theme.generate_theme_files_if_not_exist', + 'frappe.modules.full_text_search.build_index_for_all_routes' +] otp_methods = ['OTP App','Email','SMS'] user_privacy_documents = [ diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py index 4b595b1abf..f177aa6620 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py @@ -56,7 +56,8 @@ def take_backup_to_dropbox(retry_count=0, upload_db_backup=True): did_not_upload, error_log = backup_to_dropbox(upload_db_backup) if did_not_upload: raise Exception - send_email(True, "Dropbox", "Dropbox Settings", "send_notifications_to") + if cint(frappe.db.get_value("Dropbox Settings", None, "send_email_for_successful_backup")): + send_email(True, "Dropbox", "Dropbox Settings", "send_notifications_to") except JobTimeoutException: if retry_count < 2: args = { diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 0c5ec75597..1cc3abba5b 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -483,6 +483,9 @@ class Meta(Document): def get_row_template(self): return self.get_web_template(suffix='_row') + def get_list_template(self): + return self.get_web_template(suffix='_list') + def get_web_template(self, suffix=''): '''Returns the relative path of the row template for this doctype''' module_name = frappe.scrub(self.module) diff --git a/frappe/modules/full_text_search.py b/frappe/modules/full_text_search.py new file mode 100644 index 0000000000..fce9983907 --- /dev/null +++ b/frappe/modules/full_text_search.py @@ -0,0 +1,106 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe +from whoosh.index import create_in, open_dir +from whoosh.fields import TEXT, ID, Schema +from whoosh.qparser import MultifieldParser, FieldsPlugin, WildcardPlugin +from whoosh.query import Prefix +from bs4 import BeautifulSoup +from frappe.website.render import render_page +from frappe.utils import set_request, cint +from frappe.utils.global_search import get_routes_to_index + + +def build_index_for_all_routes(): + print("Building search index for all web routes...") + routes = get_routes_to_index() + documents = [get_document_to_index(route) for route in routes] + build_index("web_routes", documents) + + +@frappe.whitelist(allow_guest=True) +def web_search(index_name, query, scope=None, limit=20): + limit = cint(limit) + return search(index_name, query, scope, limit) + + +def get_document_to_index(route): + frappe.set_user("Guest") + frappe.local.no_cache = True + + try: + set_request(method="GET", path=route) + content = render_page(route) + soup = BeautifulSoup(content, "html.parser") + page_content = soup.find(class_="page_content") + text_content = page_content.text if page_content else "" + title = soup.title.text.strip() if soup.title else route + + frappe.set_user("Administrator") + + return frappe._dict(title=title, content=text_content, path=route) + except ( + frappe.PermissionError, + frappe.DoesNotExistError, + frappe.ValidationError, + Exception, + ): + pass + + +def build_index(index_name, documents): + schema = Schema( + title=TEXT(stored=True), path=ID(stored=True), content=TEXT(stored=True) + ) + + index_dir = get_index_path(index_name) + frappe.create_folder(index_dir) + + ix = create_in(index_dir, schema) + writer = ix.writer() + + for document in documents: + if document: + writer.add_document( + title=document.title, path=document.path, content=document.content + ) + + writer.commit() + + +def search(index_name, text, scope=None, limit=20): + index_dir = get_index_path(index_name) + ix = open_dir(index_dir) + + results = None + out = [] + with ix.searcher() as searcher: + parser = MultifieldParser(["title", "content"], ix.schema) + parser.remove_plugin_class(FieldsPlugin) + parser.remove_plugin_class(WildcardPlugin) + query = parser.parse(text) + + filter_scoped = None + if scope: + filter_scoped = Prefix("path", scope) + results = searcher.search(query, limit=limit, filter=filter_scoped) + + for r in results: + title_highlights = r.highlights("title") + content_highlights = r.highlights("content") + out.append( + frappe._dict( + title=r["title"], + path=r["path"], + title_highlights=title_highlights, + content_highlights=content_highlights, + ) + ) + + return out + + +def get_index_path(index_name): + return frappe.get_site_path("indexes", index_name) diff --git a/frappe/patches.txt b/frappe/patches.txt index fb5bf447b7..582b369343 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -288,3 +288,4 @@ execute:frappe.delete_doc("DocType", "Onboarding Slide") execute:frappe.delete_doc("DocType", "Onboarding Slide Field") execute:frappe.delete_doc("DocType", "Onboarding Slide Help Link") frappe.patches.v13_0.update_date_filters_in_user_settings +frappe.patches.v13_0.update_duration_options diff --git a/frappe/patches/v13_0/update_duration_options.py b/frappe/patches/v13_0/update_duration_options.py new file mode 100644 index 0000000000..60eef8fc93 --- /dev/null +++ b/frappe/patches/v13_0/update_duration_options.py @@ -0,0 +1,28 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc('core', 'doctype', 'DocField') + + if frappe.db.has_column('DocField', 'show_days'): + frappe.db.sql(""" + UPDATE + tabDocField + SET + hide_days = 1 WHERE show_days = 0 + """) + frappe.db.sql_ddl('alter table tabDocField drop column show_days') + + if frappe.db.has_column('DocField', 'show_seconds'): + frappe.db.sql(""" + UPDATE + tabDocField + SET + hide_seconds = 1 WHERE show_seconds = 0 + """) + frappe.db.sql_ddl('alter table tabDocField drop column show_seconds') + + frappe.clear_cache(doctype='DocField') \ No newline at end of file diff --git a/frappe/public/css/hljs-night-owl.css b/frappe/public/css/hljs-night-owl.css new file mode 100644 index 0000000000..932ad2e46f --- /dev/null +++ b/frappe/public/css/hljs-night-owl.css @@ -0,0 +1,183 @@ +/* + +Night Owl for highlight.js (c) Carl Baxter + +An adaptation of Sarah Drasner's Night Owl VS Code Theme +https://github.com/sdras/night-owl-vscode-theme + +Copyright (c) 2018 Sarah Drasner + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +.hljs { + display: block; + overflow-x: auto; + padding: 1rem 1.25rem; + background: #011627; + color: #d6deeb; + border-radius: 0.5rem; + } + + /* General Purpose */ + .hljs-keyword { + color: #c792ea; + font-style: italic; + } + .hljs-built_in { + color: #addb67; + font-style: italic; + } + .hljs-type { + color: #82aaff; + } + .hljs-literal { + color: #ff5874; + } + .hljs-number { + color: #F78C6C; + } + .hljs-regexp { + color: #5ca7e4; + } + .hljs-string { + color: #ecc48d; + } + .hljs-subst { + color: #d3423e; + } + .hljs-symbol { + color: #82aaff; + } + .hljs-class { + color: #ffcb8b; + } + .hljs-function { + color: #82AAFF; + } + .hljs-title { + color: #DCDCAA; + font-style: italic; + } + .hljs-params { + color: #7fdbca; + } + + /* Meta */ + .hljs-comment { + color: #637777; + font-style: italic; + } + .hljs-doctag { + color: #7fdbca; + } + .hljs-meta { + color: #82aaff; + } + .hljs-meta-keyword { + color: #82aaff; + } + .hljs-meta-string { + color: #ecc48d; + } + + /* Tags, attributes, config */ + .hljs-section { + color: #82b1ff; + } + .hljs-tag, + .hljs-name, + .hljs-builtin-name { + color: #7fdbca; + } + .hljs-attr { + color: #7fdbca; + } + .hljs-attribute { + color: #80cbc4; + } + .hljs-variable { + color: #addb67; + } + + /* Markup */ + .hljs-bullet { + color: #d9f5dd; + } + .hljs-code { + color: #80CBC4; + } + .hljs-emphasis { + color: #c792ea; + font-style: italic; + } + .hljs-strong { + color: #addb67; + font-weight: bold; + } + .hljs-formula { + color: #c792ea; + } + .hljs-link { + color: #ff869a; + } + .hljs-quote { + color: #697098; + font-style: italic; + } + + /* CSS */ + .hljs-selector-tag { + color: #ff6363; + } + + .hljs-selector-id { + color: #fad430; + } + + .hljs-selector-class { + color: #addb67; + font-style: italic; + } + + .hljs-selector-attr, + .hljs-selector-pseudo { + color: #c792ea; + font-style: italic; + } + + /* Templates */ + .hljs-template-tag { + color: #c792ea; + } + .hljs-template-variable { + color: #addb67; + } + + /* diff */ + .hljs-addition { + color: #addb67ff; + font-style: italic; + } + + .hljs-deletion { + color: #EF535090; + font-style: italic; + } diff --git a/frappe/public/js/frappe/form/controls/duration.js b/frappe/public/js/frappe/form/controls/duration.js index 58df8e15e6..e70afd6e65 100644 --- a/frappe/public/js/frappe/form/controls/duration.js +++ b/frappe/public/js/frappe/form/controls/duration.js @@ -13,10 +13,10 @@ frappe.ui.form.ControlDuration = frappe.ui.form.ControlData.extend({
` ); this.$wrapper.append(this.$picker); - this.build_numeric_input("days", !this.duration_options.show_days); + this.build_numeric_input("days", this.duration_options.hide_days); this.build_numeric_input("hours", false); this.build_numeric_input("minutes", false); - this.build_numeric_input("seconds", !this.duration_options.show_seconds); + this.build_numeric_input("seconds", this.duration_options.hide_seconds); this.set_duration_picker_value(this.value); this.$picker.hide(); this.bind_events(); @@ -130,10 +130,10 @@ frappe.ui.form.ControlDuration = frappe.ui.form.ControlData.extend({ if (this.inputs) { total_duration.minutes = parseInt(this.inputs.minutes.val()); total_duration.hours = parseInt(this.inputs.hours.val()); - if (this.duration_options.show_days) { + if (!this.duration_options.hide_days) { total_duration.days = parseInt(this.inputs.days.val()); } - if (this.duration_options.show_seconds) { + if (!this.duration_options.hide_seconds) { total_duration.seconds = parseInt(this.inputs.seconds.val()); } } diff --git a/frappe/public/js/frappe/ui/filters/filters.js b/frappe/public/js/frappe/ui/filters/filters.js index f8f0535b83..a775413d39 100644 --- a/frappe/public/js/frappe/ui/filters/filters.js +++ b/frappe/public/js/frappe/ui/filters/filters.js @@ -202,8 +202,8 @@ frappe.ui.FilterList = Class.extend({ value = {0:"No", 1:"Yes"}[cint(value)]; } else if (field.df.original_type === "Duration") { let duration_options = { - show_days: field.df.show_days, - show_seconds: field.df.show_seconds + hide_days: field.df.hide_days, + hide_seconds: field.df.hide_seconds }; value = frappe.utils.get_formatted_duration(value, duration_options); } diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index f4dde5804f..38c22c9c9f 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -856,7 +856,7 @@ Object.assign(frappe.utils, { minutes: Math.floor(secs % 3600 / 60), seconds: Math.floor(secs % 60) }; - if (!duration_options.show_days) { + if (duration_options.hide_days) { total_duration.hours = Math.floor(secs / 3600); total_duration.days = 0; } @@ -882,8 +882,8 @@ Object.assign(frappe.utils, { get_duration_options: function(docfield) { let duration_options = { - show_days: docfield.show_days, - show_seconds: docfield.show_seconds + hide_days: docfield.hide_days, + hide_seconds: docfield.hide_seconds }; return duration_options; } diff --git a/frappe/public/js/frappe/views/desktop/desktop.js b/frappe/public/js/frappe/views/desktop/desktop.js index 51add61f07..60dbc928c9 100644 --- a/frappe/public/js/frappe/views/desktop/desktop.js +++ b/frappe/public/js/frappe/views/desktop/desktop.js @@ -26,14 +26,25 @@ export default class Desktop { } make_container() { - this.container = $(`
+ this.container = $(` +
-
+
+
+
+ +
+
+
+
`); this.container.appendTo(this.wrapper); this.sidebar = this.container.find(".desk-sidebar"); this.body = this.container.find(".desk-body"); + this.current_title = this.container.find(".current-title"); + this.mobile_list = this.container.find(".mobile-list"); + this.page_switcher = this.container.find(".page-switcher"); } fetch_desktop_settings() { @@ -73,7 +84,9 @@ export default class Desktop { this.current_page = item.name; } let $item = get_sidebar_item(item); - $item.appendTo(this.sidebar); + + $item.appendTo(this.mobile_list); + $item.clone().appendTo(this.sidebar); this.sidebar_items[item.name] = $item; }; @@ -84,6 +97,7 @@ export default class Desktop { `` ); $title.appendTo(this.sidebar); + $title.clone().appendTo(this.mobile_list); }; this.sidebar_categories.forEach(category => { @@ -94,6 +108,11 @@ export default class Desktop { }); } }); + if (frappe.is_mobile) { + this.page_switcher.on('click', () => { + this.mobile_list.toggle(); + }); + } } show_page(page) { @@ -106,6 +125,8 @@ export default class Desktop { this.sidebar_items[page].addClass("selected"); } this.current_page = page; + this.mobile_list.hide(); + this.current_title.empty().append(this.current_page); localStorage.current_desk_page = page; this.pages[page] ? this.pages[page].show() : this.make_page(page); } diff --git a/frappe/public/js/frappe/web_form/webform_script.js b/frappe/public/js/frappe/web_form/webform_script.js index 53d9701774..c3211de99f 100644 --- a/frappe/public/js/frappe/web_form/webform_script.js +++ b/frappe/public/js/frappe/web_form/webform_script.js @@ -95,6 +95,11 @@ frappe.ready(function() { }; df.fields = form_data[df.fieldname]; + $.each(df.fields || [], function(_i, field) { + if (field.fieldtype === "Link") { + field.only_select = true; + } + }); if (df.fieldtype === "Attach") { df.is_private = true; diff --git a/frappe/public/less/desktop.less b/frappe/public/less/desktop.less index b66a0ad8fc..e68a093d25 100644 --- a/frappe/public/less/desktop.less +++ b/frappe/public/less/desktop.less @@ -3,6 +3,40 @@ .desk-container { margin-top: 20px; + .page-switcher { + border-radius: 5px; + display: none; + border: 1px solid @border-color; + background-color: @panel-bg; + padding: 8px 15px; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + } + + .mobile-list { + display: none; + border-radius: 5px; + padding: 8px 15px; + border: 1px solid @border-color; + + .sidebar-item { + font-size: 12px; + font-weight: bold; + margin-bottom: 1px; + display: flex; + padding: 10px 15px; + border-radius: 4px; + text-decoration: none; + cursor: pointer; + text-rendering: optimizelegibility; + + &.selected { + background-color: @panel-bg; + } + } + } + .desk-sidebar { width: 20rem; display: block; @@ -103,6 +137,9 @@ .desk-body { padding-left: 15px !important; } + .page-switcher { + display: flex; + } } } diff --git a/frappe/public/scss/base.scss b/frappe/public/scss/base.scss index 36a1df55ac..0b01a83b02 100644 --- a/frappe/public/scss/base.scss +++ b/frappe/public/scss/base.scss @@ -4,6 +4,7 @@ html { body { -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; font-size: 16px; color: $body-color; } @@ -18,6 +19,7 @@ h1 { font-weight: 800; line-height: 1.25; letter-spacing: -0.025em; + margin-bottom: 1rem; @include media-breakpoint-up(sm) { line-height: 2.5rem; @@ -32,6 +34,7 @@ h1 { h2 { font-size: $font-size-xl; font-weight: bold; + margin-bottom: 0.75rem; @include media-breakpoint-up(sm) { font-size: $font-size-2xl; diff --git a/frappe/public/scss/blog.scss b/frappe/public/scss/blog.scss new file mode 100644 index 0000000000..046158f23b --- /dev/null +++ b/frappe/public/scss/blog.scss @@ -0,0 +1,94 @@ +.blog-list { + display: flex; + flex-wrap: wrap; + margin-right: -15px; + margin-left: -15px; + + &.result { + border-bottom: none; + } +} + +.blog-card { + margin-bottom: 2rem; + position: relative; + width: 100%; + + .card-body { + display: flex; + flex-direction: column; + justify-content: space-between; + } + + .card-img-top { + width: 100%; + overflow: hidden; + height: 12rem; + + img { + width: 100%; + min-height: 100%; + } + + .default-cover { + height: 100%; + width: 100%; + padding: 1rem; + display: flex; + align-items: center; + justify-content: center; + background: $gray-200; + + font-size: 1.2rem; + font-weight: 500; + color: $gray-600; + } + } + + .blog-card-footer { + display: flex; + align-items: center; + margin-top: 0.5rem; + + .avatar { + margin-right: 0.5rem; + border-radius: 50%; + } + } +} + +.blog-container { + font-size: 1rem; + max-width: 800px; + margin: 0px auto; + + .blog-title { + margin-top: 1rem; + + @include media-breakpoint-up(xl) { + line-height: 1; + font-size: $font-size-4xl; + } + } + + .blog-footer { + display: flex; + justify-content: space-between; + color: $text-muted; + margin-top: 3rem; + } + + .blog-intro { + font-size: 1.125rem; + font-weight: 400; + } + + .blog-content { + margin-bottom: 1rem; + + .blog-header { + margin-bottom: 3rem; + margin-top: 3rem; + } + } +} diff --git a/frappe/public/scss/doc.scss b/frappe/public/scss/doc.scss new file mode 100644 index 0000000000..1eb3422042 --- /dev/null +++ b/frappe/public/scss/doc.scss @@ -0,0 +1,278 @@ +$navbar-height: 7.625rem; +$navbar-height-lg: 4.5rem; + +.doc-layout { + padding-left: 1.5rem; + padding-right: 1.5rem; + padding-top: $navbar-height; + // border-bottom: 1px solid $gray-200; + + @include media-breakpoint-up(lg) { + padding-top: $navbar-height-lg; + } +} + +.sidebar-column { + display: none; + + @include media-breakpoint-up(lg) { + display: block; + } +} + +.doc-container { + max-width: 1280px; + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +.navbar-expand-lg .doc-container { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +.doc-navbar { + background-color: white; + padding-left: 0; + padding-right: 0; + + .navbar-toggler { + margin-left: 0.75rem; + } + + .web-sidebar { + display: block; + border-top: 1px solid $gray-200; + + @include media-breakpoint-up(lg) { + display: none; + } + } + + .navbar-collapse { + height: calc(100vh - #{$navbar-height-lg}); + overflow: auto; + + @include media-breakpoint-up(lg) { + height: auto; + overflow: initial; + } + } + + .navbar-nav { + margin-left: -1rem; + margin-top: 0.75rem; + margin-bottom: 1.5rem; + + @include media-breakpoint-up(lg) { + margin-top: 0; + margin-bottom: 0; + } + } +} + +.doc-search-container { + display: flex; + margin-top: 0.75rem; + + @include media-breakpoint-up(lg) { + margin-top: 0; + } +} + +.doc-search { + position: relative; + width: 100%; + + @include media-breakpoint-up(lg) { + padding-left: 4rem; + padding-right: 4rem; + } + + .search-icon { + position: absolute; + left: 0; + top: 0; + width: 2.5rem; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + } + + svg { + color: $gray-600; + } + + input { + padding-left: 2.5rem; + } + + .dropdown-menu { + .dropdown-item { + padding: 1rem 0.75rem; + } + + .match { + background-color: $primary-light; + color: $primary; + font-weight: 500; + padding: 0 0.125rem; + } + } +} + +.doc-sidebar { + position: sticky; + top: $navbar-height; + padding-bottom: 4rem; + height: 100vh; + overflow: hidden; + + .web-sidebar { + height: 100%; + overflow: auto; + padding-top: 3rem; + padding-bottom: 4rem; + } + + @include media-breakpoint-up(lg) { + top: $navbar-height-lg; + } +} + +.doc-main .page-content-wrapper { + padding: 0 0 2rem 0; + + @include media-breakpoint-up(lg) { + padding: 0rem 4rem 4rem 4rem; + } +} + +.doc-sidebar-logo { + padding-top: 2.5rem; + padding-bottom: 2rem; +} + +.page-toc { + font-size: $font-size-sm; + + h5 { + font-size: $font-size-sm; + margin-bottom: 0.5rem; + color: $gray-500; + } + + > div { + padding-top: 3rem; + padding-bottom: 4rem; + position: sticky; + top: $navbar-height; + + @include media-breakpoint-up(lg) { + top: $navbar-height-lg; + } + } + + ul { + padding-left: 0; + list-style-type: none; + } + + li > ul { + padding-left: 0.5rem; + } + + a { + display: block; + padding: 0.25rem 0; + + color: $gray-600; + text-decoration: none; + font-weight: 500; + @include transition(); + + &:hover { + color: $gray-800; + } + } +} + +// typography styles for documentation content +.doc-content .from-markdown { + > :first-child { + margin-top: 3rem; + } + + h1 { + font-size: $font-size-3xl; + font-weight: 500; + } + + h1 + p { + font-size: $font-size-lg; + } + + h2 { + font-size: $font-size-2xl; + font-weight: 400; + } + + h3 { + font-size: $font-size-xl; + font-weight: 500; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + &::before { + height: 6rem; + margin-top: -6rem; + content: ''; + display: block; + visibility: hidden; + } + } + + h4 { + font-size: $font-size-lg; + font-weight: 500; + } + + strong { + font-weight: 600; + } + + table { + border-color: $gray-200; + } + + table thead { + background-color: $light; + } + + .table-bordered, + .table-bordered th, + .table-bordered td { + border-left: none; + border-right: none; + border-color: $gray-200; + } + + .table-bordered thead th, + .table-bordered thead td { + border-bottom-width: 1px; + } +} + +// next links +.btn-next-wrapper { + border-top: 1px solid $gray-200; + margin-top: 2rem; + padding-top: 1rem; + text-align: right; +} diff --git a/frappe/public/scss/markdown.scss b/frappe/public/scss/markdown.scss index 595b7f96a3..a77b8b941e 100644 --- a/frappe/public/scss/markdown.scss +++ b/frappe/public/scss/markdown.scss @@ -1,4 +1,5 @@ .from-markdown { + color: $gray-700; line-height: 1.625; > * + * { @@ -32,12 +33,11 @@ } > blockquote { - padding: 0.75rem 1rem; + padding: 1.25rem 1rem; font-size: $font-size-sm; font-weight: 500; - color: $gray-900; - border-left: 4px solid $yellow; - background-color: lighten($yellow, 42%); + border: 1px solid $gray-200; + border-left: 3px solid $yellow; border-top-left-radius: 0.1rem; border-bottom-left-radius: 0.1rem; border-top-right-radius: 0.375rem; @@ -49,11 +49,17 @@ margin-bottom: 0; } + b, strong { + color: $gray-800; + } + + h1, h2, h3, h4, h5, h6 { + color: $gray-900; + } + h1 + p { - max-width: 42rem; margin-top: 0.75rem; font-size: $font-size-base; - color: $gray-900; @include media-breakpoint-up(sm) { margin-top: 1.25rem; @@ -104,6 +110,7 @@ tr > td, tr > th { font-size: $font-size-sm; + padding: 0.5rem; } th:empty { @@ -114,11 +121,10 @@ border: 1px solid $gray-400; border-radius: 0.375rem; } -} -// apply margin on first h1 if container is full width without top margin -main:not(.my-5) .from-markdown { - h1:first-child { - margin-top: 5rem; + code:not(.hljs) { + padding: 0 0.25rem; + background: $light; + border-radius: 0.125rem; } } diff --git a/frappe/public/scss/page-builder.scss b/frappe/public/scss/page-builder.scss index a028e34158..defeb19e6e 100644 --- a/frappe/public/scss/page-builder.scss +++ b/frappe/public/scss/page-builder.scss @@ -1,13 +1,19 @@ .hero-subtitle { @extend .lead; + font-weight: 400; + color: $gray-600; max-width: 42rem; + font-size: 1rem; + + @include media-breakpoint-up(sm) { + font-size: 1.25rem; + } } .section-description { max-width: 56rem; margin-top: 0.5rem; font-size: $font-size-base; - color: $gray-900; @include media-breakpoint-up(lg) { font-size: $font-size-lg; @@ -88,16 +94,14 @@ } .card { - .card-title { - color: $black; - } - - .card-body { - color: $gray-900; - } + @include transition(); &:hover { - border-color: $gray-600; + border-color: $gray-500; + } + + .card-title { + line-height: 1; } &.card-sm { @@ -156,12 +160,20 @@ } .nav-tabs { + flex-wrap: nowrap; + overflow-x: auto; + overflow-y: hidden; + // 1 pixel bottom padding so that the 2px active border is visible + padding-bottom: 1px; + .nav-link { - color: $gray-700; + color: $gray-800; font-weight: 500; border: none; padding: 1rem 0.5rem; margin-right: 2rem; + white-space: nowrap; + @include transition(); &:hover { color: $primary; @@ -171,7 +183,7 @@ .nav-link.active, .nav-item.show .nav-link { color: darken($primary, 5%); - background-color: #fff; + background-color: transparent; border-bottom: 2px solid $primary; } } @@ -183,7 +195,7 @@ .section-cta { padding: 3rem 2rem; text-align: center; - background-color: lighten($primary, 42%); + background-color: $primary-light; border-radius: 0.75rem; @include media-breakpoint-up(sm) { @@ -210,7 +222,6 @@ margin: 0 auto; margin-top: 0.5rem; font-size: $font-size-base; - color: $gray-900; @include media-breakpoint-up(md) { font-size: $font-size-lg; } @@ -220,7 +231,50 @@ margin: 0 auto; margin-top: 0.5rem; font-size: $font-size-xs; + } +} + +.section-small-cta { + padding: 1.8rem; + background-color: lighten($primary, 42%); + border-radius: 0.75rem; + display: flex; + flex-direction: column; + text-align: center; + + @include media-breakpoint-up(sm) { + flex-direction: column; + text-align: left; + } + + @include media-breakpoint-up(md) { + flex-direction: row; + justify-content: space-between; + + div { + align-self: center; + } + } + + .title { + max-width: 36rem; + font-size: $font-size-xl; + font-weight: 800; + line-height: 1.25; + @include media-breakpoint-up(md) { + font-size: $font-size-2xl; + } + } + .subtitle { + max-width: 36rem; + font-size: $font-size-base; color: $gray-900; + margin-bottom: 1.2rem; + + @include media-breakpoint-up(md) { + font-size: $font-size-lg; + margin-bottom: 0px; + } } } @@ -266,19 +320,77 @@ margin-right: auto; margin-top: 2rem; max-width: 52rem; - font-size: $font-size-2xl; + font-size: $font-size-lg; font-weight: 500; + + @include media-breakpoint-up(lg) { + font-size: $font-size-2xl; + } } .testimonial-by { - font-size: $font-size-lg; + font-size: $font-size-base; margin-top: 2rem; &:before { content: '—' } + + @include media-breakpoint-up(lg) { + font-size: $font-size-lg; + } } .split-section-content { margin-top: 2rem; } + +.section-image-grid { + display: flex; + flex-wrap: wrap; + width: 100%; + + // Offset for padding + margin-right: -2px; + margin-left: -2px; + + .image-container { + overflow: hidden; + border: 2px solid #fff; + border-radius: $border-radius; + + width: 100%; + max-height: 8rem; + + img { + width: 100%; + object-fit: cover; + } + + @include media-breakpoint-up(sm) { + &.wide { + max-width: 75%; + width: 75%; + max-height: 15rem; + height: 15rem; + + img { + width: 100%; + object-fit: cover; + } + } + + &.narrow { + max-width: 25%; + width: 25%; + max-height: 15rem; + height: 15rem; + + img { + height: 100%; + object-fit: cover; + } + } + } + } +} diff --git a/frappe/public/scss/sidebar.scss b/frappe/public/scss/sidebar.scss index 72f64a912e..4dc8e64631 100644 --- a/frappe/public/scss/sidebar.scss +++ b/frappe/public/scss/sidebar.scss @@ -6,13 +6,41 @@ .sidebar-item a { display: block; - padding: 0.25rem 0; + padding: 0.25rem 0.5rem; + margin-top: 0.25rem; + border-radius: 0.375rem; font-size: $font-size-sm; - color: $gray-700; + color: $gray-600; text-decoration: none; font-weight: 500; + @include transition(); + + &:hover { + color: $gray-900; + } } .sidebar-item a.active { color: $primary; + background-color: $primary-light; +} + +.sidebar-item-icon { + width: 24px; + height: 24px; + display: inline-block; +} + +.sidebar-group { + margin-bottom: 1rem; + + h6 { + font-size: $font-size-sm; + margin-bottom: 0.75rem; + } + + > ul { + padding-left: 0.5rem; + margin-bottom: 2rem; + } } diff --git a/frappe/public/scss/variables.scss b/frappe/public/scss/variables.scss index e5f3a47f6f..1339af29a9 100644 --- a/frappe/public/scss/variables.scss +++ b/frappe/public/scss/variables.scss @@ -1,20 +1,23 @@ -$gray-100: #fafbfc !default; -$gray-150: #f5f7fa !default; -$gray-200: #ebecf1 !default; -$gray-300: #d1d8dd !default; -$gray-400: #ced4da !default; -$gray-500: #adb5bd !default; -$gray-600: #8d99a6 !default; -$gray-700: #495057 !default; -$gray-800: #36414c !default; -$gray-900: #2e3338 !default; -$primary: #2490ef !default; +$gray-50: #F9FAFA !default; +$gray-100: #F4F5F6 !default; +$gray-200: #EEF0F2 !default; +$gray-300: #E2E6E9 !default; +$gray-400: #C8CFD5 !default; +$gray-500: #A6B1B9 !default; +$gray-600: #74808B !default; +$gray-700: #4C5A67 !default; +$gray-800: #313B44 !default; +$gray-900: #192734 !default; $black: #000 !default; +$primary: #2490ef !default; +$primary-light: lighten($primary, 42%) !default; +$light: $gray-50 !default; -$body-color: $gray-800 !default; +$body-color: $gray-700 !default; $text-muted: $gray-600 !default; $border-color: $gray-300 !default; +$headings-color: $gray-900 !default; $font-size-xs: 0.75rem !default; $font-size-sm: 0.875rem !default; @@ -33,20 +36,32 @@ $btn-font-size-lg: 1.125rem !default; $btn-line-height-lg: 1 !default; $btn-border-radius-lg: 0.5rem !default; $btn-border-radius: 0.375rem !default; -$btn-font-size: $font-size-sm; +$btn-font-size: $font-size-sm !default; $btn-padding-x: 1rem !default; $btn-padding-y: 0.5rem !default; $btn-font-weight: 500 !default; $navbar-nav-link-padding-x: 1rem !default; -$navbar-padding-y: 1rem; +$navbar-padding-y: 1rem !default; $card-border-radius: 0.75rem !default; -$card-spacer-y: 1rem !default; +$card-spacer-y: 0.5rem !default; $dropdown-font-size: $font-size-sm !default; $dropdown-border-radius: 0.375rem !default; $dropdown-item-padding-y: 0.5rem !default; $dropdown-item-padding-x: 0.5rem !default; +$grid-breakpoints: ( + xs: 0, + sm: 576px, + md: 768px, + lg: 992px, + xl: 1200px, + 2xl: 1440px +) !default; + @import '~bootstrap/scss/functions'; @import '~bootstrap/scss/variables'; +@import "~bootstrap/scss/mixins"; + +$code-color: $purple; diff --git a/frappe/public/scss/website-image.scss b/frappe/public/scss/website-image.scss index 8c32e821fe..d416c05650 100644 --- a/frappe/public/scss/website-image.scss +++ b/frappe/public/scss/website-image.scss @@ -55,6 +55,12 @@ img:after { width: 100%; } +.website-image-extra-small { + @include website-image; + width: 2.5rem; + height: 2.5rem; +} + .website-image-small { @include website-image; width: 5rem; diff --git a/frappe/public/scss/website.scss b/frappe/public/scss/website.scss index 48bfbe8de0..5164fb2eba 100644 --- a/frappe/public/scss/website.scss +++ b/frappe/public/scss/website.scss @@ -5,8 +5,10 @@ @import 'multilevel-dropdown'; @import 'website-image'; @import 'page-builder'; +@import 'blog'; @import 'markdown'; @import 'sidebar'; +@import 'doc'; .container { padding-left: 1.25rem; @@ -15,26 +17,26 @@ @include media-breakpoint-up(sm) { .container { - padding-left: 1rem; - padding-right: 1rem; - } -} - -@include media-breakpoint-up(md) { - .container { - padding-left: 1rem; - padding-right: 1rem; + padding-left: 0; + padding-right: 0; } } @include media-breakpoint-up(lg) { .container { - padding-left: 1rem; - padding-right: 1rem; + padding-left: 2.5rem; + padding-right: 2.5rem; } } @include media-breakpoint-up(xl) { + .container { + padding-left: 5rem; + padding-right: 5rem; + } +} + +@include media-breakpoint-up(2xl) { .container { padding-left: 1.5rem; padding-right: 1.5rem; @@ -46,7 +48,7 @@ } .navbar-light .navbar-nav .nav-link { - color: $gray-900; + color: $gray-700; font-size: $font-size-sm; font-weight: 500; @@ -150,7 +152,7 @@ a.card { .footer-link, .footer-child-item a { font-weight: 500; - color: $gray-900; + color: $gray-700; &:hover { color: $primary; @@ -159,8 +161,9 @@ a.card { } .footer-col-left, .footer-col-right { - padding-top: 1rem; + padding-top: 0.8rem; padding-bottom: 1rem; + line-height: 2; } .footer-col-right { @@ -281,7 +284,6 @@ h5.modal-title { } .btn-primary-light { - $primary-light: lighten($primary, 42%); @include button-variant( $background: $primary-light, $border: $primary-light, diff --git a/frappe/templates/doc.html b/frappe/templates/doc.html new file mode 100644 index 0000000000..bb3cb6ec77 --- /dev/null +++ b/frappe/templates/doc.html @@ -0,0 +1,187 @@ +{% extends "templates/base.html" %} +{%- from "templates/includes/navbar/navbar_items.html" import render_item -%} + +{% macro page_content() %} +{%- block page_content -%}{%- endblock -%} +{% endmacro %} + +{%- block head_include %} + +{% endblock -%} + +{%- block navbar -%} + +{%- endblock -%} + +{% block content %} + +{% macro main_content() %} +
+ {% block page_container %} +
+
+ {{ page_content() }} +
+
+ {% endblock %} +
+{% 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 %} + +
+
+ +
+ {{ main_content() }} +
+
+
+
On this page
+ {{ page_toc_html }} +
+
+
+
+ +{% endblock %} + +{%- block script -%} + +{%- endblock -%} diff --git a/frappe/templates/includes/blog/blog.html b/frappe/templates/includes/blog/blog.html deleted file mode 100644 index 5afaeb6ab8..0000000000 --- a/frappe/templates/includes/blog/blog.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "templates/web.html" %} - -{% block title %}{{ blog_title or _("Blog") }}{% endblock %} -{% block header %}

{{ blog_title or _("Blog") }}

{% endblock %} -{% block hero %}{% endblock %} - -{% block page_content %} - - -
-
- {% include "templates/includes/list/list.html" %} -
-
-{% endblock %} - -{% block script %} - -{% endblock %} diff --git a/frappe/templates/includes/blog/blogger.html b/frappe/templates/includes/blog/blogger.html index 68df22786d..ef8f8257e8 100644 --- a/frappe/templates/includes/blog/blogger.html +++ b/frappe/templates/includes/blog/blogger.html @@ -1,7 +1,7 @@ {% from "frappe/templates/includes/macros.html" import square_image_with_fallback %}
- {{ square_image_with_fallback(src=blogger_info.avatar, size='72px', alt=blogger_info.full_name, class='align-self-start mr-3 rounded') }} + {{ square_image_with_fallback(src=blogger_info.avatar, size='small', alt=blogger_info.full_name, class='align-self-start mr-3 rounded') }}
{{ blogger_info.full_name }} diff --git a/frappe/templates/includes/breadcrumbs.html b/frappe/templates/includes/breadcrumbs.html index 3fda731372..4cb3ef5c0c 100644 --- a/frappe/templates/includes/breadcrumbs.html +++ b/frappe/templates/includes/breadcrumbs.html @@ -1,4 +1,4 @@ -{% if not no_breadcrumbs and parents %} +{%- if not no_breadcrumbs and parents -%}
-{% endif %} +{%- endif -%} diff --git a/frappe/templates/includes/comments/comment.html b/frappe/templates/includes/comments/comment.html index 3fe3d7df58..1deb49bb3e 100644 --- a/frappe/templates/includes/comments/comment.html +++ b/frappe/templates/includes/comments/comment.html @@ -1,7 +1,7 @@ {% from "frappe/templates/includes/macros.html" import square_image_with_fallback %}
- {{ square_image_with_fallback(src=frappe.get_gravatar(comment.comment_email or comment.sender), size='48px', alt=comment.sender_full_name, class='align-self-start mr-3') }} + {{ square_image_with_fallback(src=frappe.get_gravatar(comment.comment_email or comment.sender), size='extra-small', alt=comment.sender_full_name, class='align-self-start mr-3') }}
diff --git a/frappe/templates/includes/macros.html b/frappe/templates/includes/macros.html index 3e822b8bf3..767bd59ec9 100644 --- a/frappe/templates/includes/macros.html +++ b/frappe/templates/includes/macros.html @@ -1,18 +1,6 @@ -{% macro square_image_with_fallback(src=None, size=None, alt=None, class="") %} +{% macro square_image_with_fallback(src=None, size='small', alt=None, class="") %} {% if src %} -{{ alt or '' }} + {% else %}
{% endif %} diff --git a/frappe/templates/includes/web_sidebar.html b/frappe/templates/includes/web_sidebar.html index d7816eff34..86893b1310 100644 --- a/frappe/templates/includes/web_sidebar.html +++ b/frappe/templates/includes/web_sidebar.html @@ -1,46 +1,82 @@ +{% macro render_sidebar_item(item) %} +
  • + {%- if item.group_title -%} + +
    {{ item.group_title }}
    + {{ render_sidebar_items(item.group_items) }} + + {%- else -%} + + {% if item.type != 'input' %} + {%- set item_route = item.route[1:] if item.route[0] == '/' else item.route -%} + + {{ _(item.title or item.label) }} + + {% else %} +
    + +
    + {% endif %} + + {%- endif -%} +
  • +{% endmacro %} + +{% macro render_sidebar_items(items) %} +{%- if items | len > 0 -%} +
      + {% for item in items -%} + {{ render_sidebar_item(item) }} + {%- endfor %} +
    +{%- endif -%} +{% endmacro %} + +{% macro my_account() %} +{% if frappe.user != 'Guest' %} + +{% endif %} +{% endmacro %} +
    + {% if sidebar_title %} +
  • + {{ sidebar_title }} +
  • + {% endif %}
    diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 68afd9aaee..a34f09a2be 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -341,7 +341,7 @@ def format_datetime(datetime_string, format_string=None): formatted_datetime = datetime.strftime('%Y-%m-%d %H:%M:%S') return formatted_datetime -def format_duration(seconds, show_days=True): +def format_duration(seconds, hide_days=False): total_duration = { 'days': math.floor(seconds / (3600 * 24)), 'hours': math.floor(seconds % (3600 * 24) / 3600), @@ -349,7 +349,7 @@ def format_duration(seconds, show_days=True): 'seconds': math.floor(seconds % 60) } - if not show_days: + if hide_days: total_duration['hours'] = math.floor(seconds / 3600) total_duration['days'] = 0 @@ -776,6 +776,8 @@ def image_to_base64(image, extn): from io import BytesIO buffered = BytesIO() + if extn.lower() == 'jpg': + extn = 'JPEG' image.save(buffered, extn) img_str = base64.b64encode(buffered.getvalue()) return img_str @@ -1204,6 +1206,7 @@ def md_to_html(markdown_text): 'fenced-code-blocks': None, 'tables': None, 'header-ids': None, + 'toc': None, 'highlightjs-lang': None, 'html-classes': { 'table': 'table table-bordered', diff --git a/frappe/utils/formatters.py b/frappe/utils/formatters.py index d7646eeb71..d68102ae9e 100644 --- a/frappe/utils/formatters.py +++ b/frappe/utils/formatters.py @@ -91,7 +91,7 @@ def format_value(value, df=None, doc=None, currency=None, translated=False): return ', '.join(values) elif df.get("fieldtype") == "Duration": - show_days = df.show_days - return format_duration(value, show_days) + hide_days = df.hide_days + return format_duration(value, hide_days) return value diff --git a/frappe/utils/global_search.py b/frappe/utils/global_search.py index 0272ae16f4..e945039d0d 100644 --- a/frappe/utils/global_search.py +++ b/frappe/utils/global_search.py @@ -274,6 +274,10 @@ def update_global_search(doc): sync_value_in_queue(value) def update_global_search_for_all_web_pages(): + if frappe.conf.get('disable_global_search'): + return + + print('Update global search for all web pages...') routes_to_index = get_routes_to_index() for route in routes_to_index: add_route_to_global_search(route) diff --git a/frappe/website/context.py b/frappe/website/context.py index 5663199545..9d0f3d2067 100644 --- a/frappe/website/context.py +++ b/frappe/website/context.py @@ -120,7 +120,7 @@ def build_context(context): # determine templates to be used if not context.base_template_path: app_base = frappe.get_hooks("base_template") - context.base_template_path = app_base[0] if app_base else "templates/base.html" + context.base_template_path = app_base[-1] if app_base else "templates/base.html" if context.title_prefix and context.title and not context.title.startswith(context.title_prefix): context.title = '{0} - {1}'.format(context.title_prefix, context.title) diff --git a/frappe/website/doctype/blog_post/blog_post.js b/frappe/website/doctype/blog_post/blog_post.js index 7b260af8bc..7aa83f536d 100644 --- a/frappe/website/doctype/blog_post/blog_post.js +++ b/frappe/website/doctype/blog_post/blog_post.js @@ -3,6 +3,10 @@ frappe.ui.form.on('Blog Post', { refresh: function(frm) { + frappe.db.get_single_value('Blog Settings', 'show_cta_in_blog').then(value => { + frm.set_df_property("hide_cta", "hidden", !value); + }); + generate_google_search_preview(frm); }, title: function(frm) { diff --git a/frappe/website/doctype/blog_post/blog_post.json b/frappe/website/doctype/blog_post/blog_post.json index 3d24879c62..8821be1555 100644 --- a/frappe/website/doctype/blog_post/blog_post.json +++ b/frappe/website/doctype/blog_post/blog_post.json @@ -8,14 +8,16 @@ "engine": "InnoDB", "field_order": [ "title", - "published_on", - "published", - "read_time", - "disable_comments", - "column_break_3", "blog_category", "blogger", "route", + "read_time", + "column_break_3", + "published_on", + "published", + "featured", + "hide_cta", + "disable_comments", "section_break_5", "blog_intro", "content_type", @@ -83,7 +85,7 @@ "fieldtype": "Section Break" }, { - "description": "Description for listing page, in plain text, only a couple of lines. (max 140 characters)", + "description": "Description for listing page, in plain text, only a couple of lines. (max 200 characters)", "fieldname": "blog_intro", "fieldtype": "Small Text", "label": "Blog Intro" @@ -143,7 +145,8 @@ { "fieldname": "meta_image", "fieldtype": "Attach Image", - "label": "Meta Image" + "label": "Meta Image", + "mandatory_depends_on": "eval:doc.featured" }, { "fieldname": "section_break_20", @@ -165,8 +168,22 @@ "description": "in minutes", "fieldname": "read_time", "fieldtype": "Int", + "hidden": 1, "label": "Read Time", "read_only": 1 + }, + { + "default": "0", + "fieldname": "featured", + "fieldtype": "Check", + "label": "Featured" + }, + { + "default": "0", + "fieldname": "hide_cta", + "fieldtype": "Check", + "hidden": 1, + "label": "Hide CTA" } ], "has_web_view": 1, @@ -175,7 +192,7 @@ "is_published_field": "published", "links": [], "max_attachments": 5, - "modified": "2020-04-30 17:32:41.055883", + "modified": "2020-06-01 13:37:57.465434", "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 4596c60710..beffcdca25 100644 --- a/frappe/website/doctype/blog_post/blog_post.py +++ b/frappe/website/doctype/blog_post/blog_post.py @@ -30,22 +30,32 @@ class BlogPost(WebsiteGenerator): if not self.blog_intro: content = get_html_content_based_on_type(self, 'content', self.content_type) - self.blog_intro = content[:140] + self.blog_intro = content[:200] self.blog_intro = strip_html_tags(self.blog_intro) if self.blog_intro: - self.blog_intro = self.blog_intro[:140] + self.blog_intro = self.blog_intro[:200] + + if not self.meta_description: + self.meta_description = self.blog_intro[:140] + else: + self.meta_description = self.meta_description[:140] if self.published and not self.published_on: self.published_on = today() - # update posts - frappe.db.sql("""UPDATE `tabBlogger` SET `posts`=(SELECT COUNT(*) FROM `tabBlog Post` - WHERE IFNULL(`blogger`,'')=`tabBlogger`.`name`) - WHERE `name`=%s""", (self.blogger,)) + if self.featured: + if not self.meta_image: + frappe.throw(_("A featured post must have a cover image")) + self.reset_featured_for_other_blogs() self.set_read_time() + def reset_featured_for_other_blogs(self): + all_posts = frappe.get_all("Blog Post", {"featured": 1}) + for post in all_posts: + frappe.db.set_value("Blog Post", post.name, "featured", 0) + def on_update(self): super(BlogPost, self).on_update() clear_cache("writers") @@ -58,10 +68,14 @@ class BlogPost(WebsiteGenerator): if not cint(self.published): raise Exception("This blog has not been published yet!") + context.no_breadcrumbs = True + # temp fields context.full_name = get_fullname(self.owner) context.updated = global_date_format(self.published_on) context.social_links = self.fetch_social_links_info() + context.cta = self.fetch_cta() + context.enable_cta = not self.hide_cta and frappe.db.get_single_value("Blog Settings", "show_cta_in_blog", cache=True) if self.blogger: context.blogger_info = frappe.get_doc("Blogger", self.blogger).as_dict() @@ -90,27 +104,34 @@ class BlogPost(WebsiteGenerator): {"name": "Blog", "route": "/blog"}, {"label": context.category.title, "route":context.category.route}] + def fetch_cta(self): + if frappe.db.get_single_value("Blog Settings", "show_cta_in_blog", cache=True): + blog_settings = frappe.get_cached_doc("Blog Settings") + + return { + "show_cta_in_blog": 1, + "title": blog_settings.title, + "subtitle": blog_settings.subtitle, + "cta_label": blog_settings.cta_label, + "cta_url": blog_settings.cta_url + } + + return {} def fetch_social_links_info(self): + if not frappe.db.get_single_value("Blog Settings", "enable_social_sharing", cache=True): + return [] + url = frappe.local.site + "/" +self.route - social_url_map = { - "twitter": "https://twitter.com/intent/tweet?text=" +self.title + "&url=" + url, - "facebook": "https://www.facebook.com/sharer.php?u=" + url, - "linkedin": "https://www.linkedin.com/sharing/share-offsite/?url=" + url, - "email": "mailto:?subject=" + self.title + "&body=" + url, - } - social_link = [] - for link in frappe.get_cached_doc("Blog Settings").social_share_settings: - social_media = link.social_link_type + social_links = [ + { "icon": "twitter", "link": "https://twitter.com/intent/tweet?text=" + self.title + "&url=" + url }, + { "icon": "facebook", "link": "https://www.facebook.com/sharer.php?u=" + url }, + { "icon": "linkedin", "link": "https://www.linkedin.com/sharing/share-offsite/?url=" + url }, + { "icon": "envelope", "link": "mailto:?subject=" + self.title + "&body=" + url } + ] - social_link.append({ - 'icon': social_media if not social_media == 'email' else 'envelope', - 'url': social_url_map.get(social_media), - 'color': link.color, - 'background': link.background_color - }) - return social_link + return social_links def load_comments(self, context): context.comment_list = get_comment_list(self.doctype, self.name) @@ -133,8 +154,8 @@ class BlogPost(WebsiteGenerator): def get_list_context(context=None): list_context = frappe._dict( - template = "templates/includes/blog/blog.html", get_list = get_blog_list, + no_breadcrumbs = True, hide_filters = True, children = get_children(), # show_search = True, @@ -161,7 +182,8 @@ def get_list_context(context=None): else: list_context.parents = [{"name": _("Home"), "route": "/"}] - list_context.update(frappe.get_doc("Blog Settings", "Blog Settings").as_dict(no_default_fields=True)) + list_context.update(frappe.get_doc("Blog Settings").as_dict(no_default_fields=True)) + return list_context def get_children(): @@ -201,6 +223,9 @@ def get_blog_list(doctype, txt=None, filters=None, limit_start=0, limit_page_len select t1.title, t1.name, t1.blog_category, t1.route, t1.published_on, t1.read_time, t1.published_on as creation, + t1.read_time as read_time, + t1.featured as featured, + t1.meta_image as cover_image, t1.content as content, t1.content_type as content_type, t1.content_html as content_html, @@ -216,7 +241,7 @@ def get_blog_list(doctype, txt=None, filters=None, limit_start=0, limit_page_len where ifnull(t1.published,0)=1 and t1.blogger = t2.name %(condition)s - order by published_on desc, name asc + order by featured desc, published_on desc, name asc limit %(start)s, %(page_len)s""" % { "start": limit_start, "page_len": limit_page_length, "condition": (" and " + " and ".join(conditions)) if conditions else "" @@ -225,9 +250,9 @@ def get_blog_list(doctype, txt=None, filters=None, limit_start=0, limit_page_len posts = frappe.db.sql(query, as_dict=1) for post in posts: - post.content = get_html_content_based_on_type(post, 'content', post.content_type) - post.cover_image = find_first_image(post.content) + if not post.cover_image: + post.cover_image = find_first_image(post.content) post.published = global_date_format(post.creation) post.content = strip_html_tags(post.content) @@ -240,7 +265,7 @@ def get_blog_list(doctype, txt=None, filters=None, limit_start=0, limit_page_len post.avatar = post.avatar or "" post.category = frappe.db.get_value('Blog Category', post.blog_category, - ['route', 'title'], as_dict=True) + ['name', 'route', 'title'], as_dict=True) if post.avatar and (not "http:" in post.avatar and not "https:" in post.avatar) and not post.avatar.startswith("/"): post.avatar = "/" + post.avatar diff --git a/frappe/website/doctype/blog_post/templates/blog_post.html b/frappe/website/doctype/blog_post/templates/blog_post.html index 12e5ccf2d7..dd3e59c3c1 100644 --- a/frappe/website/doctype/blog_post/templates/blog_post.html +++ b/frappe/website/doctype/blog_post/templates/blog_post.html @@ -6,38 +6,57 @@ {% block page_content %}
    -
    +
    -
    - -

    {{ title }}

    -

    - {{ blog_intro }} -

    -
    - -
    - {{ frappe.format_date(published_on) }} - {% if read_time %} - · - {{ read_time }} min read - {% endif %} - {% if social_links %} - - {% endif %} +
    + +

    {{ title }}

    +

    + {{ blog_intro }} +

    +
    + + {%- if read_time -%} +  · + {{ read_time }} min read + {%- endif -%}
    -
    - {{ content }} +
    +
    + {{ content }}
    + {%- if enable_cta -%} + {{ web_blocks([ + { + 'template': "Section With Small CTA", + 'values': cta, + 'add_container': 0, + 'add_top_padding': 0, + 'add_bottom_padding': 0, + 'css_class': "my-5" + } + ]) + }} + {%- endif -%} + {% if blogger_info %}
    @@ -45,7 +64,7 @@ {% endif %} {% if not disable_comments %} -
    +
    {% include 'templates/includes/comments/comments.html' %}
    {% endif %} @@ -55,30 +74,3 @@ frappe.ready(() => frappe.set_search_path("/blog")) {% endblock %} - -{% block style %} - -{% endblock %} diff --git a/frappe/website/doctype/blog_post/templates/blog_post_list.html b/frappe/website/doctype/blog_post/templates/blog_post_list.html new file mode 100644 index 0000000000..8df47b7187 --- /dev/null +++ b/frappe/website/doctype/blog_post/templates/blog_post_list.html @@ -0,0 +1,42 @@ +{% extends "templates/web.html" %} +{% block title %}{{ blog_title or _("Blog") }}{% endblock %} +{% block hero %}{% endblock %} + +{% block page_content %} + +{{ web_blocks([ + { + 'template': "Hero", + 'values': { + 'title': blog_title or _("Blog"), + 'subtitle': blog_introduction or '', + }, + 'add_container': 0, + 'add_top_padding': 0, + 'add_bottom_padding': 0, + 'css_class': "py-5" + } + ]) +}} + +
    +
    +
    + {% if not result -%} +
    + {{ no_result_message or _("Nothing to show") }} +
    + {% else %} + {% for item in result %} + {{ item }} + {% endfor %} + {% endif %} +
    + +
    +
    +{% endblock %} + +{% block script %} + +{% endblock %} diff --git a/frappe/website/doctype/blog_post/templates/blog_post_row.html b/frappe/website/doctype/blog_post/templates/blog_post_row.html index dffe0ef81d..7daf27adc8 100644 --- a/frappe/website/doctype/blog_post/templates/blog_post_row.html +++ b/frappe/website/doctype/blog_post/templates/blog_post_row.html @@ -1,38 +1,41 @@ {%- set post = doc -%} -
    -
    -
    -
    -
    -
    -
    {{ post.category.title }}
    -

    {{ post.title }}

    -

    {{ post.intro }}

    -
    -
    - {{ post.full_name }} - · - {{ frappe.format_date(post.published_on) }} - {% if post.comments %} - · - {% if post.comments == 1 %} - {{ _('1 comment') }} - {% else %} - {{ _('{0} comments').format(post.comments) }} - {% endif %} - {% endif %} - {% if post.read_time %} - · - {{ _('{0} min read').format(post.read_time) }} - {% endif %} -
    +
    +
    +
    + {% if post.cover_image %} + {{post.title}} - Cover Image + {% else %} +
    + {{ post.title }}
    -
    - {% if post.cover_image %} - {{post.title}} - Cover Image - {% endif %} + {% endif %} +
    +
    +
    +
    + {%- if post.featured -%} + {{ _('Featured') }} · + {%- endif -%} + {{ post.category.title }} +
    + {%- if post.featured -%} +
    {{ post.title }}
    + {%- else -%} +
    {{ post.title }}
    + {%- endif -%} +

    {{ post.intro }}

    +
    +
    +
    -
    +
    \ No newline at end of file diff --git a/frappe/website/doctype/blog_post/test_blog_post.py b/frappe/website/doctype/blog_post/test_blog_post.py index aecc813e9d..15634a7caf 100644 --- a/frappe/website/doctype/blog_post/test_blog_post.py +++ b/frappe/website/doctype/blog_post/test_blog_post.py @@ -19,7 +19,7 @@ class TestBlogPost(unittest.TestCase): self.assertTrue(response.status_code, 200) html = response.get_data().decode() - self.assertTrue('
    ' in html) + self.assertTrue('
    ' in html) def test_generator_not_found(self): pages = frappe.get_all('Blog Post', fields=['name', 'route'], diff --git a/frappe/website/doctype/blog_settings/blog_settings.json b/frappe/website/doctype/blog_settings/blog_settings.json index f0e51de170..73ea3ce877 100644 --- a/frappe/website/doctype/blog_settings/blog_settings.json +++ b/frappe/website/doctype/blog_settings/blog_settings.json @@ -7,9 +7,15 @@ "field_order": [ "blog_title", "blog_introduction", - "writers_introduction", - "section_break_4", - "social_share_settings" + "column_break", + "enable_social_sharing", + "show_cta_in_blog", + "cta_section", + "title", + "subtitle", + "column_break_11", + "cta_label", + "cta_url" ], "fields": [ { @@ -23,27 +29,62 @@ "label": "Blog Introduction" }, { - "fieldname": "writers_introduction", - "fieldtype": "Small Text", - "label": "Writers Introduction" + "default": "0", + "fieldname": "enable_social_sharing", + "fieldtype": "Check", + "label": "Enable Social Sharing" }, { "collapsible": 1, - "fieldname": "section_break_4", - "fieldtype": "Section Break" + "fieldname": "column_break", + "fieldtype": "Column Break" }, { - "fieldname": "social_share_settings", - "fieldtype": "Table", - "label": "Social Share Settings", - "options": "Social Link Settings" + "default": "0", + "fieldname": "show_cta_in_blog", + "fieldtype": "Check", + "label": "Show CTA in Blog" + }, + { + "depends_on": "eval:doc.show_cta_in_blog", + "fieldname": "cta_section", + "fieldtype": "Section Break", + "label": "CTA" + }, + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "mandatory_depends_on": "eval:doc.show_cta_in_blog" + }, + { + "fieldname": "subtitle", + "fieldtype": "Data", + "label": "Subtitle", + "mandatory_depends_on": "eval:doc.show_cta_in_blog" + }, + { + "fieldname": "cta_label", + "fieldtype": "Data", + "label": "CTA Label", + "mandatory_depends_on": "eval:doc.show_cta_in_blog" + }, + { + "fieldname": "cta_url", + "fieldtype": "Data", + "label": "CTA URL", + "mandatory_depends_on": "eval:doc.show_cta_in_blog" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" } ], "icon": "fa fa-cog", "idx": 1, "issingle": 1, "links": [], - "modified": "2020-05-04 09:10:41.815238", + "modified": "2020-06-01 15:57:21.564652", "modified_by": "Administrator", "module": "Website", "name": "Blog Settings", @@ -57,6 +98,13 @@ "role": "Website Manager", "share": 1, "write": 1 + }, + { + "email": 1, + "print": 1, + "read": 1, + "role": "Blogger", + "share": 1 } ], "sort_field": "modified", diff --git a/frappe/website/doctype/blog_settings/test_blog_settings.py b/frappe/website/doctype/blog_settings/test_blog_settings.py new file mode 100644 index 0000000000..e4ddb85c4b --- /dev/null +++ b/frappe/website/doctype/blog_settings/test_blog_settings.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 TestBlogSettings(unittest.TestCase): + pass diff --git a/frappe/website/doctype/blogger/blogger.json b/frappe/website/doctype/blogger/blogger.json index b8165a5908..f7494e7ec5 100644 --- a/frappe/website/doctype/blogger/blogger.json +++ b/frappe/website/doctype/blogger/blogger.json @@ -13,8 +13,7 @@ "full_name", "user", "bio", - "avatar", - "posts" + "avatar" ], "fields": [ { @@ -51,20 +50,13 @@ }, { "fieldname": "avatar", - "fieldtype": "Attach", + "fieldtype": "Attach Image", "label": "Avatar" - }, - { - "fieldname": "posts", - "fieldtype": "Int", - "in_list_view": 1, - "label": "Posts", - "no_copy": 1, - "read_only": 1 } ], "icon": "fa fa-user", "idx": 1, + "image_field": "avatar", "links": [ { "link_doctype": "Blog Post", @@ -72,7 +64,7 @@ } ], "max_attachments": 1, - "modified": "2020-04-19 08:21:09.684300", + "modified": "2020-05-28 19:22:40.959895", "modified_by": "Administrator", "module": "Website", "name": "Blogger", diff --git a/frappe/website/js/website.js b/frappe/website/js/website.js index d400e7633c..c95345770d 100644 --- a/frappe/website/js/website.js +++ b/frappe/website/js/website.js @@ -112,13 +112,6 @@ $.extend(frappe, { opts.args.cmd = opts.method; } - // stringify - $.each(opts.args, function(key, val) { - if(typeof val != "string") { - opts.args[key] = JSON.stringify(val); - } - }); - if(!opts.no_spinner) { //NProgress.start(); } @@ -329,6 +322,22 @@ $.extend(frappe, { add_switch_to_desk: function() { $('.switch-to-desk').removeClass('hidden'); }, + add_link_to_headings: function() { + $('.doc-content .from-markdown').find('h2, h3, h4, h5, h6').each((i, $heading) => { + let id = $heading.id; + let $a = $('') + .prop('href', '#' + id) + .attr('aria-hidden', 'true') + .html(` + + + + + `); + $($heading).append($a); + }); + }, setup_lazy_images: function() { // Use IntersectionObserver to only load images that are visible in the viewport // Fallback for browsers that don't support it @@ -445,6 +454,7 @@ $(document).on("page-change", function() { frappe.trigger_ready(); frappe.bind_filters(); frappe.highlight_code_blocks(); + frappe.add_link_to_headings(); frappe.make_navbar_active(); // scroll to hash if (window.location.hash) { diff --git a/frappe/website/render.py b/frappe/website/render.py index c1bca3f5c5..d5269ed1cb 100644 --- a/frappe/website/render.py +++ b/frappe/website/render.py @@ -216,7 +216,6 @@ def build_page(path): if context.source: html = frappe.render_template(context.source, context) - elif context.template: if path.endswith('min.js'): html = frappe.get_jloader().get_source(frappe.get_jenv(), context.template)[0] diff --git a/frappe/website/router.py b/frappe/website/router.py index 4a9db0868f..b291671a4a 100644 --- a/frappe/website/router.py +++ b/frappe/website/router.py @@ -270,13 +270,18 @@ def setup_source(page_info): if page_info.template.endswith('.md'): source = frappe.utils.md_to_html(source) + page_info.page_toc_html = source.toc_html if not page_info.show_sidebar: source = '
    ' + source + '
    ' - # if only content - if page_info.template.endswith('.html') or page_info.template.endswith('.md'): - html = extend_from_base_template(page_info, source) + if not page_info.base_template: + page_info.base_template = get_base_template(page_info.route) + + if page_info.template.endswith(('.html', '.md', )) and \ + '{%- extends' not in source and '{% extends' not in source: + # set the source only if it contains raw content + html = source # load css/js files js, css = '', '' @@ -300,22 +305,23 @@ def setup_source(page_info): # show table of contents setup_index(page_info) -def extend_from_base_template(page_info, source): - '''Extend the content with appropriate base template if required. - - For easy composition, the users will only add the content of the page, - not its template. But if the user has explicitly put Jinja blocks, or tags, - or comment tags like - then the system will not try and put it inside the "web.template" +def get_base_template(path=None): ''' + Returns the `base_template` for given `path`. + The default `base_template` for any web route is `templates/web.html` defined in `hooks.py`. + This can be overridden for certain routes in `custom_app/hooks.py` based on regex pattern. + ''' + if not path: + path = frappe.local.request.path - if (('' not in source) and ('{% block' not in source) - and ('