Merge branch 'develop' into fix-none-type-get-workflow

This commit is contained in:
Suraj Shetty 2020-06-10 12:08:05 +05:30 committed by GitHub
commit a12c97c4f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 1838 additions and 397 deletions

View file

@ -1,7 +1,7 @@
<div align="center">
<img src=".github/frappe-framework-logo.png" height="150">
<h1>
<a href="https://frappe.io">
<a href="https://frappeframework.com">
frappe
</a>
</h1>
@ -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).

View file

@ -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');

View file

@ -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);
// });
});

View file

@ -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,

View file

@ -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)

View file

@ -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",

View file

@ -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

View file

@ -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",

View file

@ -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'),

View file

@ -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",

View file

@ -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`),

View file

@ -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")
) ;

View file

@ -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 = [

View file

@ -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 = {

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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')

View file

@ -0,0 +1,183 @@
/*
Night Owl for highlight.js (c) Carl Baxter <carl@cbax.tech>
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;
}

View file

@ -13,10 +13,10 @@ frappe.ui.form.ControlDuration = frappe.ui.form.ControlData.extend({
</div>`
);
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());
}
}

View file

@ -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);
}

View file

@ -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;
}

View file

@ -26,14 +26,25 @@ export default class Desktop {
}
make_container() {
this.container = $(`<div class="desk-container row">
this.container = $(`
<div class="desk-container row">
<div class="desk-sidebar"></div>
<div class="desk-body"></div>
<div class="desk-body">
<div class="page-switcher">
<div class="current-title"></div>
<i class="fa fa-chevron-down text-muted"></i>
</div>
<div class="mobile-list">
</div>
</div>
</div>`);
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 {
`<div class="sidebar-group-title h6 uppercase">${__(name)}</div>`
);
$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);
}

View file

@ -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;

View file

@ -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;
}
}
}

View file

@ -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;

View file

@ -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;
}
}
}

278
frappe/public/scss/doc.scss Normal file
View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}
}
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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,

187
frappe/templates/doc.html Normal file
View file

@ -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 %}
<link rel="stylesheet" href="/assets/frappe/css/hljs-night-owl.css">
{% endblock -%}
{%- block navbar -%}
<nav class="navbar navbar-light navbar-expand-lg doc-navbar fixed-top">
<div class="container-fluid doc-container">
<div class="row no-gutters w-100">
<div class="col-12 col-lg-2">
<a class="navbar-brand" href="{{ url_prefix }}{{ home_page or "/" }}">
{%- if brand_html -%}
{{ brand_html }}
{%- elif banner_image -%}
<img src='{{ banner_image }}'>
{%- else -%}
<span>{{ (frappe.get_hooks("brand_html") or [_("Home")])[0] }}</span>
{%- endif -%}
</a>
</div>
<div class="col-12 col-lg-8">
<div class="doc-search-container">
<div class="doc-search">
<div class="dropdown">
<div class="search-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-search">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</div>
<input type="search" class="form-control" placeholder="Search the docs (Press ? to focus)" />
<div class="overflow-hidden shadow dropdown-menu w-100">
</div>
</div>
</div>
<button class="navbar-toggler" type="button"
data-toggle="collapse"
data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
<div class="col-12 col-lg-2">
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav">
{%- set items = docs_navbar_items or [] -%}
{%- for item in items -%}
{{ render_item(item, parent=True) }}
{%- endfor -%}
</ul>
{% include "templates/includes/web_sidebar.html" %}
</div>
</div>
</div>
</div>
</nav>
{%- endblock -%}
{% block content %}
{% macro main_content() %}
<div class="page-content-wrapper">
{% block page_container %}
<main>
<div class="page_content page-content doc-content">
{{ page_content() }}
</div>
</main>
{% endblock %}
</div>
{% 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 %}
<div class="container-fluid doc-layout doc-container">
<div class="row no-gutters" {{ container_attributes() }}>
<div class="sidebar-column col-sm-2">
<aside class="doc-sidebar">
{% block page_sidebar %}
{% include "templates/includes/web_sidebar.html" %}
{% endblock %}
</aside>
</div>
<div class="main-column doc-main col-12 col-lg-10 col-xl-8">
{{ main_content() }}
</div>
<div class="page-toc col-sm-2 d-none d-xl-block">
<div>
<h5>On this page</h5>
{{ page_toc_html }}
</div>
</div>
</div>
</div>
{% endblock %}
{%- block script -%}
<script>
frappe.ready(() => {
setup_search();
$('.web-footer .container')
.removeClass('container')
.addClass('container-fluid doc-container');
});
function setup_search() {
let $dropdown = $('.doc-search .dropdown');
let $dropdown_menu = $('.doc-search .dropdown-menu');
let $input = $('.doc-search input');
$(document).on('keypress', e => {
if (e.key === '/') {
e.preventDefault();
$input.focus();
}
});
$input.on('input', frappe.utils.debounce(() => {
if (!$input.val()) {
clear_dropdown();
return;
}
frappe.call({
method: 'frappe.modules.full_text_search.web_search',
args: {
index_name: 'web_routes',
scope: '{{ docs_search_scope or "" }}' || null,
query: $input.val(),
limit: 5
}
}).then(r => {
let results = r.message || [];
let dropdown_html;
if (results.length == 0) {
dropdown_html = `<div class="dropdown-item">No results found</div>`;
} else {
dropdown_html = results.map(r => {
return `<a class="dropdown-item" href="/${r.path}">
<h6>${r.title_highlights || r.title}</h6>
<div style="white-space: normal;">${r.content_highlights}</div>
</a>`
}).join('')
}
$dropdown_menu.html(dropdown_html);
$dropdown_menu.addClass('show');
});
}, 500));
$input.on('focus', () => {
if (!$input.val()) {
clear_dropdown();
}
});
$input.on('blur', () => {
setTimeout(() => {
clear_dropdown();
}, 300);
});
function clear_dropdown() {
$dropdown_menu.html('');
$dropdown_menu.removeClass('show');
}
}
</script>
{%- endblock -%}

View file

@ -1,19 +0,0 @@
{% extends "templates/web.html" %}
{% block title %}{{ blog_title or _("Blog") }}{% endblock %}
{% block header %}<h1>{{ blog_title or _("Blog") }}</h1>{% endblock %}
{% block hero %}{% endblock %}
{% block page_content %}
<!-- no-header -->
<!-- no-breadcrumbs -->
<div class="blog-list-content">
<div id="blog-list">
{% include "templates/includes/list/list.html" %}
</div>
</div>
{% endblock %}
{% block script %}
<script>{% include "templates/includes/list/list.js" %}</script>
{% endblock %}

View file

@ -1,7 +1,7 @@
{% from "frappe/templates/includes/macros.html" import square_image_with_fallback %}
<div class="media">
{{ 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') }}
<div class="media-body">
<h5 class="mt-0">
<a href="/blog?blogger={{ blogger_info.name }}" class="text-dark">{{ blogger_info.full_name }}</a>

View file

@ -1,4 +1,4 @@
{% if not no_breadcrumbs and parents %}
{%- if not no_breadcrumbs and parents -%}
<div class="container mt-3">
<nav aria-label="breadcrumb">
<ol class="breadcrumb" itemscope itemtype="http://schema.org/BreadcrumbList">
@ -17,4 +17,4 @@
</ol>
</nav>
</div>
{% endif %}
{%- endif -%}

View file

@ -1,7 +1,7 @@
{% from "frappe/templates/includes/macros.html" import square_image_with_fallback %}
<div class="comment-row media">
{{ 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') }}
<div class="media-body">
<div class="d-flex justify-content-between align-items-start">
<span class="font-weight-bold text-muted">

View file

@ -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 %}
<img
{% if size %}
width="{{size}}"
height="{{size}}"
{% endif %}
{% if src %}
src="{{ src }}"
{% endif %}
class="{{ class }} "
alt="{{ alt or '' }}"
>
<img class="rounded-lg website-image-{{ size }} mr-2" src="{{ src }}">
{% else %}
<div class="no-image bg-light {{ class }} " {% if size %}style="width: {{size}}; height: {{size}};"{% endif %}></div>
{% endif %}

View file

@ -1,46 +1,82 @@
{% macro render_sidebar_item(item) %}
<li class="{{ 'sidebar-group' if item.group_title else 'sidebar-item' }}">
{%- if item.group_title -%}
<h6>{{ item.group_title }}</h6>
{{ render_sidebar_items(item.group_items) }}
{%- else -%}
{% if item.type != 'input' %}
{%- set item_route = item.route[1:] if item.route[0] == '/' else item.route -%}
<a href="{{ item.route }}" class="{{ 'active' if pathname == item_route else '' }}"
{% if item.target %}target="{{ item.target }}" {% endif %}>
{{ _(item.title or item.label) }}
</a>
{% else %}
<form action='{{ item.route }}' class="mr-3">
<input name='q' class='form-control' type='text' style="outline: none"
placeholder="{{ _(item.title or item.label) }}">
</form>
{% endif %}
{%- endif -%}
</li>
{% endmacro %}
{% macro render_sidebar_items(items) %}
{%- if items | len > 0 -%}
<ul class="list-unstyled">
{% for item in items -%}
{{ render_sidebar_item(item) }}
{%- endfor %}
</ul>
{%- endif -%}
{% endmacro %}
{% macro my_account() %}
{% if frappe.user != 'Guest' %}
<ul class="list-unstyled">
<li class="sidebar-item">
<a href="/me">{{ _("My Account") }}</a>
</li>
</ul>
{% endif %}
{% endmacro %}
<div class="web-sidebar">
{% if sidebar_title %}
<li class="title">
{{ sidebar_title }}
</li>
{% endif %}
<div class="sidebar-items">
<ul class="list-unstyled">
{% if sidebar_title %}
<li class="title">
{{ sidebar_title }}
</li>
{% endif %}
{% for item in sidebar_items -%}
<li class="sidebar-item">
{% if item.type != 'input' %}
{%- set item_route = item.route[1:] if item.route[0] == '/' else item.route -%}
<a href="{{ item.route }}" class="{{ 'active' if pathname == item_route else '' }}"
{% if item.target %}target="{{ item.target }}"{% endif %}>
{{ _(item.title or item.label) }}
</a>
{% else %}
<form action='{{ item.route }}' class="mr-3">
<input name='q' class='form-control' type='text' style="outline: none"
placeholder="{{ _(item.title or item.label) }}">
</form>
{% endif %}
</li>
{%- endfor %}
{% if frappe.user != 'Guest' %}
<li class="sidebar-item">
<a href="/me">{{ _("My Account") }}</a>
</li>
{% endif %}
</ul>
{{ render_sidebar_items(sidebar_items) }}
{{ my_account() }}
</div>
</div>
<script>
frappe.ready(function() {
$('.sidebar-item a').each(function(index) {
const active_class = 'active'
const non_active_class = ''
if(this.href.trim() == window.location) {
$(this).removeClass(non_active_class).addClass(active_class);
} else {
$(this).removeClass(active_class).addClass(non_active_class);
}
});
});
frappe.ready(function () {
$('.sidebar-item a').each(function (index) {
const active_class = 'active'
const non_active_class = ''
let page_href = window.location.href;
if (page_href.indexOf('#') !== -1) {
page_href = page_href.slice(0, page_href.indexOf('#'));
}
if (this.href.trim() == page_href) {
$(this).removeClass(non_active_class).addClass(active_class);
} else {
$(this).removeClass(active_class).addClass(non_active_class);
}
});
// scroll the active sidebar item into view
let active_sidebar_item = $('.sidebar-item a.active');
if (active_sidebar_item.length > 0) {
active_sidebar_item.get(0)
.scrollIntoView({behavior: "auto", block: "center", inline: "nearest"});
}
});
</script>

View file

@ -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',

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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) {

View file

@ -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",

View file

@ -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

View file

@ -6,38 +6,57 @@
{% block page_content %}
<div class="blog-container">
<article class="blog-content mb-3" itemscope itemtype="http://schema.org/BlogPosting">
<article class="blog-content" itemscope itemtype="http://schema.org/BlogPosting">
<!-- begin blog content -->
<div class="blog-info">
<span class="text-center">
<h1 itemprop="headline" class="blog-header">{{ title }}</h1>
<p class="lead">
{{ blog_intro }}
</p>
</span>
<div class="text-muted small meta-info">
{{ frappe.format_date(published_on) }}
{% if read_time %}
&middot;
{{ read_time }} min read
{% endif %}
{% if social_links %}
<div class="social-links">
{% for link in social_links %}
<a href="{{ link.url }}" class="fa fa-{{ link.icon }}"
style='color:{{ link.color }}; background-color: {{ link.background }};'
target="_blank"></a>
{% endfor %}
</div>
{% endif %}
<div class="blog-header">
<div>
<a class="mr-2" href="/blog">{{ _('Blog') }}</a>
<span class="text-muted">/</span>
<a class="ml-2" href="/blog/{{ category.title }}">{{ category.title }}</a>
</div>
<h1 itemprop="headline" class="blog-title">{{ title }}</h1>
<p class="blog-intro">
{{ blog_intro }}
</p>
<div class="text-muted">
<time datetime="{{ published_on }}">{{ frappe.format_date(published_on) }}</time>
{%- if read_time -%}
&nbsp;&middot;
<span>{{ read_time }} min read</span>
{%- endif -%}
</div>
</div>
<div itemprop="articleBody" class="longform blog-text mt-5">
{{ content }}
<hr class="my-5">
<div itemprop="articleBody" class="from-markdown">
{{ content }}
</div>
<!-- end blog content -->
</article>
{%- 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 -%}
<div class="blog-footer">
<div>
{{ _('Published on') }} <time datetime="{{ published_on }}">{{ frappe.format_date(published_on) }}</time>
</div>
<div>
{% if social_links %}
{% for link in social_links %}
<a href="{{ link.link }}" class="text-muted ml-2 fa fa-{{ link.icon }}" target="_blank"></a>
{% endfor %}
{% endif %}
</div>
</div>
{% if blogger_info %}
<hr class="my-5">
@ -45,7 +64,7 @@
{% endif %}
{% if not disable_comments %}
<div class="blog-comments my-5">
<div class="my-5 blog-comments">
{% include 'templates/includes/comments/comments.html' %}
</div>
{% endif %}
@ -55,30 +74,3 @@
frappe.ready(() => frappe.set_search_path("/blog"))
</script>
{% endblock %}
{% block style %}
<style>
.blog-container {
max-width: 720px;
margin: 0 auto;
font-size: 1.2rem;
}
.meta-info {
display: flex;
justify-content: space-between;
align-items: flex-end;
flex-wrap: wrap;
}
.social-links {
margin-right: 0px;
margin-top: 1rem;
}
.social-links a {
font-size: 1.25rem;
margin: 0 5px 0 0;
padding: 5px 0;
width: 2.5rem;
text-align: center;
}
</style>
{% endblock %}

View file

@ -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"
}
])
}}
<div class="blog-list-content">
<div class="website-list" data-doctype="{{ doctype }}" data-txt="{{ txt or '[notxt]' | e }}">
<div id="blog-list" class="blog-list result row">
{% if not result -%}
<div class="text-muted" style="min-height: 300px;">
{{ no_result_message or _("Nothing to show") }}
</div>
{% else %}
{% for item in result %}
{{ item }}
{% endfor %}
{% endif %}
</div>
<button class="btn btn-light btn-more btn {% if not show_more -%} hidden {%- endif %}">{{ _("Load More") }}</button>
</div>
</div>
{% endblock %}
{% block script %}
<script>{% include "templates/includes/list/list.js" %}</script>
{% endblock %}

View file

@ -1,38 +1,41 @@
{%- set post = doc -%}
<div class="web-list-item blog-list-item my-5">
<div class="row">
<div class="col-12 col-md-8">
<div class="row">
<div class="col-9 d-flex flex-column justify-content-between">
<div>
<div class="text-muted small text-uppercase">{{ post.category.title }}</div>
<h4><a href="/{{ post.route }}" class="text-dark">{{ post.title }}</a></h4>
<p class="post-description text-muted">{{ post.intro }}</p>
</div>
<div class="text-muted small">
<a class="text-muted" href="/blog?blogger={{ post.blogger }}">{{ post.full_name }}</a>
&middot;
{{ frappe.format_date(post.published_on) }}
{% if post.comments %}
&middot;
{% if post.comments == 1 %}
{{ _('1 comment') }}
{% else %}
{{ _('{0} comments').format(post.comments) }}
{% endif %}
{% endif %}
{% if post.read_time %}
&middot;
{{ _('{0} min read').format(post.read_time) }}
{% endif %}
</div>
<div class="blog-card col-sm-12 {{ 'col-md-8' if post.featured else 'col-md-4' }}">
<div class="card h-100">
<div class="card-img-top">
{% if post.cover_image %}
<img src="{{ post.cover_image }}" alt="{{post.title}} - Cover Image">
{% else %}
<div class="default-cover">
<span>{{ post.title }}</span>
</div>
<div class="col-3">
{% if post.cover_image %}
<img class="website-image-medium object-fit-cover" src="{{ post.cover_image }}" alt="{{post.title}} - Cover Image">
{% endif %}
{% endif %}
</div>
<div class="card-body">
<div>
<div class="text-muted small text-uppercase">
{%- if post.featured -%}
<span class="text-body">{{ _('Featured') }} &middot; </span>
{%- endif -%}
<span>{{ post.category.title }}</span>
</div>
{%- if post.featured -%}
<h5 class="mt-1"><span class="text-dark">{{ post.title }}</span></h5>
{%- else -%}
<h5 class="mt-1"><span class="text-dark">{{ post.title }}</span></h3>
{%- endif -%}
<p class="post-description text-muted">{{ post.intro }}</p>
</div>
<div class="blog-card-footer">
<img class="avatar website-image-extra-small" src="{{ post.avatar }}">
<div class="text-muted">
<a href="/blog?blogger={{ post.blogger }}">{{ post.full_name }}</a>
<div class="small">
{{ frappe.format_date(post.published_on) }}
{% if post.read_time %} &middot; {{ post.read_time }} min read {% endif %}
</div>
</div>
</div>
</div>
<a class="stretched-link" href="/{{ post.route }}"></a>
</div>
</div>
</div>

View file

@ -19,7 +19,7 @@ class TestBlogPost(unittest.TestCase):
self.assertTrue(response.status_code, 200)
html = response.get_data().decode()
self.assertTrue('<article class="blog-content mb-3" itemscope itemtype="http://schema.org/BlogPosting">' in html)
self.assertTrue('<article class="blog-content" itemscope itemtype="http://schema.org/BlogPosting">' in html)
def test_generator_not_found(self):
pages = frappe.get_all('Blog Post', fields=['name', 'route'],

View file

@ -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",

View file

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

View file

@ -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",

View file

@ -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 = $('<a class="no-underline">')
.prop('href', '#' + id)
.attr('aria-hidden', 'true')
.html(`
<svg xmlns="http://www.w3.org/2000/svg" style="width: 0.8em; height: 0.8em;" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
`);
$($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) {

View file

@ -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]

View file

@ -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 = '<div class="from-markdown">' + source + '</div>'
# 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 <body> tags,
or comment tags like <!-- base_template: [path] -->
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 (('</body>' not in source) and ('{% block' not in source)
and ('<!-- base_template:' not in source)) and 'base_template' not in page_info:
page_info.only_content = True
source = '''{% extends "templates/web.html" %}
{% block page_content %}\n''' + source + '\n{% endblock %}'
return source
base_template_map = frappe.get_hooks("base_template_map") or {}
patterns = list(base_template_map.keys())
patterns_desc = sorted(patterns, key=lambda x: len(x), reverse=True)
for pattern in patterns_desc:
if re.match(pattern, path):
templates = base_template_map[pattern]
base_template = templates[-1]
return base_template
def setup_index(page_info):
'''Build page sequence from index.txt'''
@ -335,7 +341,10 @@ def load_properties_from_source(page_info):
if base_template:
page_info.base_template = base_template
if page_info.base_template:
if (page_info.base_template
and "{%- extends" not in page_info.source
and "{% extends" not in page_info.source
and "</body>" not in page_info.source):
page_info.source = '''{{% extends "{0}" %}}
{{% block page_content %}}{1}{{% endblock %}}'''.format(page_info.base_template, page_info.source)
page_info.no_cache = 1

View file

@ -0,0 +1,18 @@
<div class="section-with-image-grid">
<h2 class="section-title">{{ title }}</h2>
<p class="section-description">{{ subtitle }}</p>
<div class="section-image-grid">
{%- for index in ['1', '2', '3', '4'] -%}
{%- set image = values['image_' + index ] -%}
{%- set class = "narrow" if index in ['1', '4'] else "wide" -%}
{%- if image -%}
<div class="image-container {{ class }}">
{{ frappe.render_template('templates/includes/image_with_blur.html', {
"src": image
}) }}
</div>
{%- endif -%}
{%- endfor -%}
</div>
</div>

View file

@ -0,0 +1,50 @@
{
"creation": "2020-06-04 14:43:39.753713",
"docstatus": 0,
"doctype": "Web Template",
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"reqd": 1
},
{
"fieldname": "subtitle",
"fieldtype": "Data",
"label": "Subtitle",
"reqd": 1
},
{
"fieldname": "image_1",
"fieldtype": "Attach Image",
"label": "Image 1",
"reqd": 1
},
{
"fieldname": "image_2",
"fieldtype": "Attach Image",
"label": "Image 2",
"reqd": 1
},
{
"fieldname": "image_3",
"fieldtype": "Attach Image",
"label": "Image 3",
"reqd": 0
},
{
"fieldname": "image_4",
"fieldtype": "Attach Image",
"label": "Image 4",
"reqd": 0
}
],
"idx": 0,
"modified": "2020-06-04 16:57:43.097550",
"modified_by": "Administrator",
"name": "Section with Image Grid",
"owner": "Administrator",
"standard": 1,
"template": ""
}

View file

@ -0,0 +1,11 @@
<div class="section-cta-container">
<div class="section-small-cta">
<div>
<h2 class="title">{{ title }}</h2>
<p class="subtitle">{{ subtitle }}</p>
</div>
<div>
<a href="{{ cta_url }}" class="btn btn-lg btn-primary">{{ cta_label }}</a>
</div>
</div>
</div>

View file

@ -0,0 +1,37 @@
{
"creation": "2020-06-01 15:56:38.002136",
"docstatus": 0,
"doctype": "Web Template",
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"reqd": 0
},
{
"fieldname": "subtitle",
"fieldtype": "Small Text",
"label": "Subtitle",
"reqd": 0
},
{
"fieldname": "cta_label",
"fieldtype": "Data",
"label": "CTA Label",
"reqd": 0
},
{
"fieldname": "cta_url",
"fieldtype": "Data",
"label": "CTA URL",
"reqd": 0
}
],
"idx": 0,
"modified": "2020-06-01 17:51:23.073342",
"modified_by": "Administrator",
"name": "Section with Small CTA",
"owner": "Administrator",
"standard": 1
}

View file

@ -21,7 +21,7 @@
{%- endif -%}
{%- endfor -%}
<ul class="nav nav-tabs flex-nowrap overflow-auto" role="tablist" aria-label="{{ title or '' }}">
<ul class="nav nav-tabs" role="tablist" aria-label="{{ title or '' }}">
{%- for tab in ns.tabs -%}
{%- set first_tab = true if loop.index0 == 0 else false -%}
<li class="nav-item">

View file

@ -7,9 +7,9 @@
"custom_overrides": "",
"docstatus": 0,
"doctype": "Website Theme",
"font_properties": "400,500,600,700,800",
"idx": 26,
"modified": "2020-05-20 14:47:12.938879",
"font_properties": "wght:400;500;600;700;800",
"idx": 28,
"modified": "2020-06-04 17:47:09.207101",
"modified_by": "Administrator",
"module": "Website",
"name": "Standard",

View file

@ -171,6 +171,9 @@ def get_list_context(context, doctype, web_form_name=None):
if not meta.custom and not list_context.row_template:
list_context.row_template = meta.get_row_template()
if not meta.custom and not list_context.list_template:
list_context.template = meta.get_list_template()
return list_context
def get_list(doctype, txt, filters, limit_start, limit_page_length=20, ignore_permissions=False,

View file

@ -66,3 +66,4 @@ watchdog==0.8.0
Werkzeug==0.16.1
xlrd==1.2.0
zxcvbn-python==4.4.24
Whoosh==2.7.4