Merge pull request #9958 from rmehta/web-view

feat(Web View): Create Web Views
This commit is contained in:
Rushabh Mehta 2020-04-16 11:07:03 +05:30 committed by GitHub
commit 141a7241b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 820 additions and 202 deletions

View file

@ -15,7 +15,8 @@ global_cache_keys = ("app_hooks", "installed_apps",
"app_modules", "module_app", "system_settings",
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version',
'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts')
'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts',
'sitemap_routes')
user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
"defaults", "user_permissions", "home_page", "linked_with",

View file

@ -206,7 +206,7 @@ class DocType(Document):
if d.fieldtype:
if (not getattr(d, "fieldname", None)):
if d.label:
d.fieldname = d.label.strip().lower().replace(' ','_')
d.fieldname = d.label.strip().lower().replace(' ','_').strip('?')
if d.fieldname in restricted:
d.fieldname = d.fieldname + '1'
if d.fieldtype=='Section Break':
@ -914,7 +914,7 @@ def validate_fields(meta):
if not d.permlevel: d.permlevel = 0
if d.fieldtype not in table_fields: d.allow_bulk_edit = 0
if not d.fieldname:
d.fieldname = d.fieldname.lower()
d.fieldname = d.fieldname.lower().strip('?')
check_illegal_characters(d.fieldname)
check_invalid_fieldnames(meta.get("name"), d.fieldname)

View file

@ -92,7 +92,6 @@ frappe.ui.GroupBy = class {
}
apply_settings(settings) {
if (!settings.group_by.startsWith('`tab')) {
settings.group_by = '`tab' + this.doctype + '`.`' + settings.group_by + '`';
}

View file

@ -2,13 +2,13 @@ import WebFormList from './web_form_list'
import WebForm from './web_form'
frappe.ready(function() {
let query_params = frappe.utils.get_query_params();
let wrapper = $(".web-form-wrapper");
let is_list = parseInt(wrapper.data('is-list'));
let is_list = parseInt(wrapper.data('is-list')) || query_params.is_list;
let webform_doctype = wrapper.data('web-form-doctype');
let webform_name = wrapper.data('web-form');
let login_required = parseInt(wrapper.data('login-required'));
let allow_delete = parseInt(wrapper.data('allow-delete'));
let query_params = frappe.utils.get_query_params();
let doc_name = query_params.name || '';
let is_new = query_params.new;
@ -38,7 +38,7 @@ frappe.ready(function() {
settings: {
allow_delete
}
})
});
}
function show_form() {

View file

@ -62,7 +62,11 @@
{%- endblock -%}
{%- block navbar -%}
{% include "templates/includes/navbar/navbar.html" %}
{%- if navbar_content -%}
{{ navbar_content }}
{%- else -%}
{% include "templates/includes/navbar/navbar.html" %}
{%- endif -%}
{%- endblock -%}
{% block content %}
@ -70,7 +74,11 @@
{% endblock %}
{%- block footer -%}
{% include "templates/includes/footer/footer.html" %}
{%- if footer_content -%}
{{ footer_content }}
{%- else -%}
{% include "templates/includes/footer/footer.html" %}
{%- endif -%}
{%- endblock -%}
{% block base_scripts %}

View file

@ -13,7 +13,7 @@
</div>
{% block page_container %}
<main class="container my-5">
<main class="{% if not theme.use_full_width %}container{% endif %} my-5">
<div class="d-flex justify-content-between align-items-center">
<div class="page-header">
{% block header %}{% endblock %}

View file

@ -5,6 +5,8 @@ from frappe.utils import get_html_for_route
class TestSitemap(unittest.TestCase):
def test_sitemap(self):
from frappe.test_runner import make_test_records
make_test_records('Blog Post')
blogs = frappe.db.get_all('Blog Post', {'published': 1}, ['route'], limit=1)
xml = get_html_for_route('sitemap.xml')
self.assertTrue('/about</loc>' in xml)

View file

@ -68,10 +68,7 @@ def render_template(template, context, is_path=None, safe_render=True):
if not template:
return ""
# if it ends with .html then its a freaking path, not html
if (is_path
or template.startswith("templates/")
or (template.endswith('.html') and '\n' not in template)):
if (is_path or guess_is_path(template)):
return get_jenv().get_template(template).render(context)
else:
if safe_render and ".__" in template:
@ -81,6 +78,16 @@ def render_template(template, context, is_path=None, safe_render=True):
except TemplateError:
throw(title="Jinja Template Error", msg="<pre>{template}</pre><pre>{tb}</pre>".format(template=template, tb=get_traceback()))
def guess_is_path(template):
# template can be passed as a path or content
# if its single line and ends with a html, then its probably a path
if not '\n' in template and '.' in template:
extn = template.rsplit('.')[-1]
if extn in ('html', 'css', 'scss', 'py'):
return True
return False
def get_jloader():
import frappe

View file

@ -0,0 +1,8 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('CSS Class', {
// refresh: function(frm) {
// }
});

View file

@ -0,0 +1,60 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "Prompt",
"creation": "2020-03-17 15:03:31.431344",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"is_global",
"is_dynamic",
"css"
],
"fields": [
{
"default": "0",
"fieldname": "is_global",
"fieldtype": "Check",
"label": "Is Global?"
},
{
"fieldname": "css",
"fieldtype": "Code",
"in_list_view": 1,
"label": "CSS",
"reqd": 1
},
{
"default": "0",
"description": "Website Theme elements are accessible as Jinja variables. Example: \"{{ primary_color }}\"",
"fieldname": "is_dynamic",
"fieldtype": "Check",
"label": "Is Dynamic?"
}
],
"links": [],
"modified": "2020-03-17 17:01:14.874631",
"modified_by": "Administrator",
"module": "Website",
"name": "CSS Class",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Website Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class CSSClass(Document):
pass

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 TestCSSClass(unittest.TestCase):
pass

View file

@ -156,6 +156,9 @@ def get_context(context):
# only a single doc allowed and no existing doc, hence new
frappe.form_dict.new = 1
if frappe.form_dict.is_list:
context.is_list = True
# always render new form if login is not required or doesn't allow editing existing ones
if not self.login_required or not self.allow_edit:
frappe.form_dict.new = 1

View file

@ -148,6 +148,7 @@ class WebPage(WebsiteGenerator):
def check_publish_status():
# called via daily scheduler
web_pages = frappe.get_all("Web Page", fields=["name", "published", "start_date", "end_date"])
now_date = get_datetime(now())

View file

@ -0,0 +1,7 @@
{% extends "templates/web.html" %}
{% block page_content %}
{% include "frappe/website/doctype/web_view/templates/web_view_content.html" %}
{% endblock %}
<!-- this is a sample default web page template -->

View file

@ -0,0 +1,77 @@
{%- if css_rules or css -%}
<style>
{%- for css_rule in css_rules -%}
{{ css_rule }}
{%- endfor -%}
{{ css or "" }}
</style>
{%- endif -%}
{%- macro render_element(element) -%}
{%- if element.element_type=='Content' -%}
<div class="web-content {{ element_class(element) }}" {{ element_style(element) }}>
{{ element.web_content_html }}
</div>
{%- elif element.element_type=='Image' -%}
<img src='{{ element.image_url }}'
{%- if element.element_class -%}class='{{ element.element_class }}'{%- endif -%}
{{ element_style(element) }}>
{%- endif -%}
{%- endmacro -%}
{%- macro element_class(element) -%}
{{ element.element_class or "" }}
{%- endmacro -%}
{%- macro element_style(element) -%}
{%- if element.element_style -%}
style = "{{ element.element_style }}"
{%- endif -%}
{%- endmacro -%}
{%- for section in sections -%}
<section class='section {{ section.element_class or "" }} {{ section.hide and "hidden" or "" }}'>
<div class='section-body container'>
{%- if section.section_intro -%}
<div class='section-intro'>{{ section.section_intro }}</div>
{%- endif -%}
{%- if section.section_type == 'List' -%}
{%- for element in section.elements -%}
{{ render_element(element) }}
{%- endfor -%}
{%- elif section.section_type == 'Grid' -%}
<div class='row'>
{%- for element in section.elements -%}
<div class='col-md-{{ element.columns or 6 }}'>
{{ render_element(element) }}
</div>
{%- endfor -%}
</div>
{%- elif section.section_type == 'Tabbed' -%}
<ul class="nav" role="tablist">
{%- for element in section.elements -%}
<li class="nav-item">
<a class="nav-link {{ loop.index == 1 and 'active' or ''}}" href="#{{ element.element_id }}" role="tab" data-toggle="tab">
{{ element.title }}
</a>
</li>
{%- endfor -%}
</ul>
<div class="tab-content">
{%- for element in section.elements -%}
<div class="tab-pane {{ loop.index == 1 and 'show active' or ''}}" role="tabpanel" id="{{ element.element_id }}">
{{ render_element(element) }}
</div>
{%- endfor -%}
</div>
{%- endif -%}
</div>
</section>
{%- endfor -%}

View file

@ -0,0 +1,4 @@
<div>
<a href={{ route }}>{{ title }}</a>
</div>
<!-- this is a sample default list template -->

View file

@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from frappe.website.doctype.web_page.test_web_page import get_page_content
test_dependencies = ['Web Page'] # for test
class TestWebView(unittest.TestCase):
@classmethod
def setUpClass(cls):
frappe.delete_doc_if_exists('Web View', 'test-web-view')
frappe.delete_doc_if_exists('CSS Class', 'test-css-class')
frappe.get_doc(dict(
doctype = 'CSS Class',
name = 'test-css-class',
css = '.test-class { color: red; }'
)).insert()
frappe.get_doc(dict(
doctype = 'Web View',
title = 'Test Web View',
route = 'test-web-view',
published = 1,
items = [
dict(
element_type = 'Section',
section_type = 'List'
),
dict(
element_type = 'Content',
web_content_type = 'Markdown',
web_content_markdown = '## Heading\n\nBody'
),
dict(
element_type = 'Content',
web_content_type = 'HTML',
web_content_html = '<div>Here is some HTML</div>'
),
dict(
element_type = 'Section',
section_type = 'Grid'
),
dict(
element_type = 'Content',
element_class = 'test-css-class',
web_content_type = 'Markdown',
web_content_markdown = 'Column 1'
),
dict(
element_type = 'Content',
web_content_type = 'Markdown',
web_content_markdown = 'Column 2'
),
]
)).insert()
def test_web_view(self):
html = get_page_content('test-web-view')
#print(html)
self.assert_web_view_in_html(html)
def assert_web_view_in_html(self, html):
self.assertTrue('<h2 id="heading">Heading</h2>' in html)
self.assertTrue('<div>Here is some HTML</div>' in html)
self.assertTrue('Column 1' in html)
self.assertTrue('Column 2' in html)
self.assertTrue('.test-class { color: red; }' in html)
def test_web_view_in_footer(self):
website_settings = frappe.get_single("Website Settings")
website_settings.footer_type = 'Web View'
website_settings.footer_web_view = 'test-web-view'
website_settings.save()
html = get_page_content('test-web-page-1')
website_settings.footer_type = 'Standard'
website_settings.footer_web_view = ''
website_settings.save()
# web view should still come as footer
self.assert_web_view_in_html(html)
html_without_footer = get_page_content('test-web-page-1')
# no more footer
self.assertFalse('Column 1' in html_without_footer)

View file

@ -0,0 +1,8 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Web View', {
// refresh: function(frm) {
// }
});

View file

@ -0,0 +1,92 @@
{
"actions": [],
"allow_guest_to_view": 1,
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:title",
"beta": 1,
"creation": "2020-03-16 15:28:03.828741",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"route",
"published",
"items",
"css"
],
"fields": [
{
"fieldname": "items",
"fieldtype": "Table",
"label": "Items",
"options": "Web View Item",
"reqd": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1,
"unique": 1
},
{
"fieldname": "route",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Route",
"reqd": 1
},
{
"default": "0",
"fieldname": "published",
"fieldtype": "Check",
"label": "Published"
},
{
"fieldname": "css",
"fieldtype": "Code",
"label": "CSS"
}
],
"has_web_view": 1,
"is_published_field": "published",
"links": [],
"modified": "2020-04-15 23:58:12.208049",
"modified_by": "Administrator",
"module": "Website",
"name": "Web View",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Website Manager",
"share": 1,
"write": 1
}
],
"route": "route",
"sort_field": "creation",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.website.website_generator import WebsiteGenerator
from frappe.utils import markdown
import frappe
class WebView(WebsiteGenerator):
def get_context(self, context):
# group items into sections
context.sections = []
context.css_rules = []
for item in self.items:
if not context.sections and item.element_type!='Section':
self.add_default_section(context)
if item.element_type=='Section':
self.add_section(context, item)
else:
self.add_item(context, item)
self.add_css_class(context, item)
return context
def add_section(self, context, item):
item.elements = []
context.sections.append(item)
if item.section_intro:
item.section_intro = markdown(item.section_intro)
def add_item(self, context, item):
if item.hide:
return
if item.web_content_type == 'Markdown':
item.web_content_html = markdown(item.web_content_markdown)
if item.title:
item.element_id = frappe.scrub(item.title)
context.sections[-1].elements.append(item)
def add_css_class(self, context, item):
# add css class definitions selected by the user
if item.element_class and not item.hide:
css, is_dynamic = frappe.db.get_value('CSS Class', item.element_class, ['css', 'is_dynamic'])
if is_dynamic:
css = frappe.render_template(css, self.get_theme())
context.css_rules.append(css)
def render_content(self):
# webview can be rendered as an object (see footer)
return frappe.render_template("frappe/website/doctype/web_view/templates/web_view_content.html", self.get_context(self.as_dict()))
def get_theme(self):
# get theme properties
if not hasattr(self, '_theme'):
default_theme = frappe.db.get_value("Website Settings", "Website Settings", "website_theme")
self._theme = frappe.get_value('Website Theme', default_theme, '*')
return self._theme
def add_default_section(self, context):
# add a default section if not added
context.sections.append(frappe._dict(
element_type='Section',
section_type='List',
title='Default Section',
elements=[]
))

View file

@ -0,0 +1,121 @@
{
"actions": [],
"creation": "2020-03-16 15:25:17.530296",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"element_type",
"title",
"hide",
"column_break_3",
"columns",
"element_class",
"element_style",
"section_break_5",
"section_type",
"web_content_type",
"web_content_html",
"web_content_markdown",
"image_url",
"section_intro"
],
"fields": [
{
"fieldname": "element_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Element Type",
"options": "Section\nContent\nParagraph\nWeb List\nWeb Form",
"reqd": 1
},
{
"depends_on": "eval:doc.element_type==='Section'",
"fieldname": "section_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Section Type",
"options": "\nList\nTabbed\nGrid"
},
{
"depends_on": "eval:doc.element_type==='Content'",
"fieldname": "web_content_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Web Content Type",
"options": "\nHTML\nMarkdown"
},
{
"depends_on": "eval:doc.web_content_type==='HTML'",
"fieldname": "web_content_html",
"fieldtype": "HTML Editor",
"label": "Web Content HTML"
},
{
"depends_on": "eval:doc.web_content_type==='Markdown'",
"fieldname": "web_content_markdown",
"fieldtype": "Markdown Editor",
"label": "Web Content Markdown"
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title"
},
{
"fieldname": "columns",
"fieldtype": "Int",
"label": "Columns"
},
{
"fieldname": "element_class",
"fieldtype": "Link",
"label": "Element Class",
"options": "CSS Class"
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.element_type==='Image'",
"fieldname": "image_url",
"fieldtype": "Small Text",
"label": "Image URL"
},
{
"depends_on": "eval:doc.element_type==='Section'",
"fieldname": "section_intro",
"fieldtype": "Markdown Editor",
"label": "Section Intro"
},
{
"default": "0",
"fieldname": "hide",
"fieldtype": "Check",
"label": "Hide"
},
{
"fieldname": "element_style",
"fieldtype": "Small Text",
"label": "Element Style"
}
],
"istable": 1,
"links": [],
"modified": "2020-03-28 14:21:50.014823",
"modified_by": "Administrator",
"module": "Website",
"name": "Web View Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class WebViewItem(Document):
pass

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 TestWebsiteSettings(unittest.TestCase):
pass

View file

@ -19,11 +19,15 @@
"set_banner_from_image",
"favicon",
"top_bar",
"top_bar_type",
"top_bar_web_view",
"navbar_search",
"top_bar_items",
"banner",
"banner_html",
"footer",
"footer_type",
"footer_web_view",
"copyright",
"address",
"footer_items",
@ -130,11 +134,13 @@
},
{
"default": "0",
"depends_on": "eval:doc.top_bar_type==='Standard'",
"fieldname": "navbar_search",
"fieldtype": "Check",
"label": "Include Search in Top Bar"
},
{
"depends_on": "eval:doc.top_bar_type==='Standard'",
"fieldname": "top_bar_items",
"fieldtype": "Table",
"label": "Top Bar Items",
@ -160,17 +166,20 @@
"label": "Footer"
},
{
"depends_on": "eval:doc.footer_type==='Standard'",
"fieldname": "copyright",
"fieldtype": "Data",
"label": "Copyright"
},
{
"depends_on": "eval:doc.footer_type==='Standard'",
"description": "Address and other legal information you may want to put in the footer.",
"fieldname": "address",
"fieldtype": "Text Editor",
"label": "Address"
},
{
"depends_on": "eval:doc.footer_type==='Standard'",
"fieldname": "footer_items",
"fieldtype": "Table",
"label": "Footer Items",
@ -178,6 +187,7 @@
},
{
"default": "0",
"depends_on": "eval:doc.footer_type==='Standard'",
"fieldname": "hide_footer_signup",
"fieldtype": "Check",
"label": "Hide Footer Signup"
@ -319,6 +329,34 @@
"fieldname": "authorize_api_indexing_access",
"fieldtype": "Button",
"label": "Authorize API Indexing Access"
},
{
"default": "Standard",
"fieldname": "footer_type",
"fieldtype": "Select",
"label": "Footer Type",
"options": "Standard\nWeb View"
},
{
"depends_on": "eval:doc.footer_type==='Web View'",
"fieldname": "footer_web_view",
"fieldtype": "Link",
"label": "Footer Web View",
"options": "Web View"
},
{
"default": "Standard",
"fieldname": "top_bar_type",
"fieldtype": "Select",
"label": "Top Bar Type",
"options": "Standard\nWeb View"
},
{
"depends_on": "eval:doc.top_bar_type==='Web View'",
"fieldname": "top_bar_web_view",
"fieldtype": "Link",
"label": "Top Bar Web View",
"options": "Web View"
}
],
"icon": "fa fa-cog",
@ -326,7 +364,7 @@
"issingle": 1,
"links": [],
"max_attachments": 10,
"modified": "2020-02-21 16:46:59.947403",
"modified": "2020-04-21 16:46:59.947403",
"modified_by": "Administrator",
"module": "Website",
"name": "Website Settings",

View file

@ -149,6 +149,7 @@ def get_website_settings():
context[key] = context[key][-1]
add_website_theme(context)
add_webviews(context, settings)
if not context.get("favicon"):
context["favicon"] = "/assets/frappe/images/favicon.png"
@ -158,6 +159,17 @@ def get_website_settings():
return context
def add_webviews(context, settings):
# render footer as webview, not standard view
# see base.html for how this is handled
if settings.footer_type=='Web View' and settings.footer_web_view:
context.footer_content = frappe.get_doc('Web View',
settings.footer_web_view).render_content()
if settings.top_bar_type=='Web View' and settings.top_bar_web_view:
context.navbar_content = frappe.get_doc('Web View',
settings.top_bar_web_view).render_content()
def get_items(parentfield):
all_top_items = frappe.db.sql("""\
select * from `tabTop Bar Item`

View file

@ -2,10 +2,30 @@
# See license.txt
from __future__ import unicode_literals
import os
import frappe
import unittest
test_records = frappe.get_test_records('Website Theme')
class TestWebsiteTheme(unittest.TestCase):
pass
def test_website_theme(self):
if os.environ.get('CI'):
# no node-sass on travis (?)
return
frappe.delete_doc_if_exists('Website Theme', 'test-theme')
theme = frappe.get_doc(dict(
doctype = 'Website Theme',
theme = 'test-theme',
google_font = 'Inter',
custom_scss = 'body { font-size: 16.5px; }'
)).insert()
with open(theme.theme_url[1:]) as f:
css = f.read()
self.assertTrue(theme.custom_scss in css)
self.assertTrue('fonts.googleapis.com' in css)

View file

@ -6,7 +6,6 @@ frappe.ui.form.on('Website Theme', {
frm.clear_custom_buttons();
frm.toggle_display(["module", "custom"], !frappe.boot.developer_mode);
frm.trigger('setup_configure_theme');
frm.trigger('set_default_theme_button_and_indicator');
if (!frm.doc.custom && !frappe.boot.developer_mode) {
@ -17,96 +16,6 @@ frappe.ui.form.on('Website Theme', {
}
},
setup_configure_theme(frm) {
frm.add_custom_button(__('Configure Theme'), () => {
const d = new frappe.ui.Dialog({
title: __('Configure Theme'),
fields: [
{
label: __('Font Styles'),
fieldtype: 'Section Break'
},
{
label: __('Google Font'),
fieldtype: 'Data',
fieldname: 'google_font',
description: __('Add the name of a "Google Web Font" e.g. "Open Sans"')
},
{
label: __('Font Size (px)'),
fieldtype: 'Int',
fieldname: 'font_size',
default: 16
},
{
label: __('Theme Colors'),
fieldtype: 'Section Break',
},
{
label: __('Primary Color'),
fieldtype: 'Color',
fieldname: 'primary_color'
},
{
label: __('Dark Color'),
fieldtype: 'Color',
fieldname: 'dark_color'
},
{
label: __('Text Color'),
fieldtype: 'Color',
fieldname: 'text_color'
},
{
label: __('Background Color'),
fieldtype: 'Color',
fieldname: 'background_color'
},
{
label: __('Misc'),
fieldtype: 'Section Break',
},
{
label: __('Navbar Style'),
fieldtype: 'Select',
fieldname: 'navbar_style',
options: [
'Light',
'Dark'
],
default: 'Light'
},
{
label: __('Enable Shadows'),
fieldtype: 'Check',
fieldname: 'enable_shadows'
},
{
label: __('Enable Gradients'),
fieldtype: 'Check',
fieldname: 'enable_gradients'
},
{
label: __('Rounded Corners'),
fieldtype: 'Check',
fieldname: 'enable_rounded',
default: 1
},
],
primary_action: (values) => {
frm.set_value('theme_json', JSON.stringify(values));
frm.events.set_theme_from_config(frm, values);
d.hide();
}
});
if (frm.doc.theme_json) {
d.set_values(JSON.parse(frm.doc.theme_json));
}
d.show();
});
},
set_default_theme_button_and_indicator(frm) {
frappe.db.get_single_value('Website Settings', 'website_theme')
.then(value => {
@ -122,92 +31,5 @@ frappe.ui.form.on('Website Theme', {
}
}
});
},
set_theme_from_config(frm, config) {
const {
google_font,
font_size,
primary_color,
dark_color,
text_color,
background_color,
navbar_style,
enable_shadows,
enable_gradients,
enable_rounded
} = config;
let scss_lines = [];
let js_lines = [];
if (google_font) {
const google_font_slug = google_font.split(' ').join('+');
const font_family_default = `'-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif'`;
scss_lines.push(
`@import url('https://fonts.googleapis.com/css?family=${google_font_slug}:400,300,400italic,700&subset=latin,latin-ext');`,
`$font-family-sans-serif: "${google_font}", ${font_family_default};`
);
}
if (primary_color) {
scss_lines.push(
`$primary: ${primary_color};`
);
}
if (dark_color) {
scss_lines.push(
`$dark: ${dark_color};`
);
}
if (text_color) {
scss_lines.push(
`$body-color: ${text_color};`
);
}
if (background_color) {
scss_lines.push(
`$body-bg: ${background_color};`
);
}
scss_lines.push(
`$enable-shadows: ${Boolean(enable_shadows)};`
);
scss_lines.push(
`$enable-gradients: ${Boolean(enable_gradients)};`
);
scss_lines.push(
`$enable-rounded: ${Boolean(enable_rounded)};`
);
if (font_size) {
scss_lines.push(
'\n',
`body {\n\tfont-size: ${font_size}px;\n}`
);
}
if (navbar_style === 'Dark') {
if (!(frm.doc.js || '').includes(`.addClass('navbar-dark bg-dark')`)) {
js_lines.push(
`frappe.ready(() => {`,
`\t$('.navbar').removeClass('navbar-light bg-white').addClass('navbar-dark bg-dark')`,
`})`
);
}
}
scss_lines.push(
`@import "frappe/public/scss/website";`,
'\n'
);
// set scss
frm.set_value('theme_scss', scss_lines.join('\n'));
// set js
const js = frm.doc.js || '';
frm.set_value('js', js_lines.join('\n') + js);
}
});

View file

@ -1,4 +1,5 @@
{
"actions": [],
"allow_import": 1,
"autoname": "field:theme",
"creation": "2015-02-18 12:46:38.168929",
@ -9,7 +10,20 @@
"theme",
"module",
"custom",
"configuration_section",
"google_font",
"font_size",
"font_properties",
"use_full_width",
"column_break_7",
"primary_color",
"text_color",
"light_color",
"dark_color",
"background_color",
"stylesheet_section",
"theme_scss",
"custom_scss",
"theme_json",
"theme_url",
"custom_js_section",
@ -43,7 +57,8 @@
"fieldname": "theme_scss",
"fieldtype": "Code",
"label": "Theme",
"options": "SCSS"
"options": "SCSS",
"read_only": 1
},
{
"fieldname": "theme_url",
@ -68,9 +83,76 @@
"hidden": 1,
"label": "Theme JSON",
"options": "JSON"
},
{
"fieldname": "configuration_section",
"fieldtype": "Section Break",
"label": "Configuration"
},
{
"fieldname": "google_font",
"fieldtype": "Data",
"label": "Google Font"
},
{
"fieldname": "font_size",
"fieldtype": "Data",
"label": "Font Size"
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
},
{
"fieldname": "primary_color",
"fieldtype": "Color",
"label": "Primary Color"
},
{
"fieldname": "text_color",
"fieldtype": "Color",
"label": "Text Color"
},
{
"fieldname": "dark_color",
"fieldtype": "Color",
"label": "Dark Color"
},
{
"fieldname": "background_color",
"fieldtype": "Color",
"label": "Background Color"
},
{
"fieldname": "stylesheet_section",
"fieldtype": "Section Break",
"label": "Stylesheet"
},
{
"fieldname": "custom_scss",
"fieldtype": "Code",
"label": "Custom SCSS"
},
{
"fieldname": "light_color",
"fieldtype": "Color",
"label": "Light Color"
},
{
"default": "300,600",
"fieldname": "font_properties",
"fieldtype": "Data",
"label": "Font Properties"
},
{
"description": "Content will not be inside a \"container\" class, you will have to add your own containers for different sections.",
"fieldname": "use_full_width",
"fieldtype": "Data",
"label": "Use Full Width"
}
],
"modified": "2019-06-14 18:36:21.283390",
"links": [],
"modified": "2020-03-19 09:46:48.750150",
"modified_by": "Administrator",
"module": "Website",
"name": "Website Theme",

View file

@ -10,6 +10,7 @@ from os.path import join as join_path, exists as path_exists
class WebsiteTheme(Document):
def validate(self):
self.validate_if_customizable()
self.render_theme()
self.validate_theme()
def on_update(self):
@ -35,12 +36,14 @@ class WebsiteTheme(Document):
if self.is_standard_and_not_valid_user():
frappe.throw(_("Please Duplicate this Website Theme to customize."))
def render_theme(self):
self.theme_scss = frappe.render_template('frappe/website/doctype/website_theme/website_theme_template.scss', self.as_dict())
def validate_theme(self):
'''Generate theme css if theme_scss has changed'''
if self.theme_scss:
doc_before_save = self.get_doc_before_save()
if doc_before_save is None or self.theme_scss != doc_before_save.theme_scss:
self.generate_bootstrap_theme()
doc_before_save = self.get_doc_before_save()
if doc_before_save is None or get_scss(self) != get_scss(doc_before_save):
self.generate_bootstrap_theme()
def export_doc(self):
"""Export to standard folder `[module]/website_theme/[name]/[name].json`."""
@ -57,9 +60,14 @@ class WebsiteTheme(Document):
def generate_bootstrap_theme(self):
from subprocess import Popen, PIPE
folder_path = join_path(frappe.utils.get_bench_path(), 'sites', 'assets', 'css')
self.delete_old_theme_files(folder_path)
# add a random suffix
file_name = frappe.scrub(self.name) + '_' + frappe.generate_hash('Website Theme', 8) + '.css'
output_path = join_path(frappe.utils.get_bench_path(), 'sites', 'assets', 'css', file_name)
content = self.theme_scss
output_path = join_path(folder_path, file_name)
content = get_scss(self)
content = content.replace('\n', '\\n')
command = ['node', 'generate_bootstrap_theme.js', output_path, content]
@ -76,6 +84,12 @@ class WebsiteTheme(Document):
frappe.msgprint(_('Compiled Successfully'), alert=True)
def delete_old_theme_files(self, folder_path):
import os
for fname in os.listdir(folder_path):
if fname.startswith(frappe.scrub(self.name) + '_') and fname.endswith('.css'):
os.remove(os.path.join(folder_path, fname))
def generate_theme_if_not_exist(self):
bench_path = frappe.utils.get_bench_path()
if self.theme_url:
@ -116,3 +130,7 @@ def generate_theme_files_if_not_exist():
doc.save()
except Exception:
frappe.log_error(frappe.get_traceback(), "Theme File Generation Failed")
def get_scss(doc):
return (doc.theme_scss or '') + '\n' + (doc.custom_scss or '')

View file

@ -0,0 +1,21 @@
{% if google_font %}
@import url('https://fonts.googleapis.com/css?family={{ google_font.replace(' ', '+') }}:{{ font_properties }}&display=swap');
$font-family-sans-serif: "{{ google_font }}", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
{% endif %}
{% if primary_color %}$primary: {{ primary_color }};{% endif %}
{% if dark_color %}$dark: {{ dark_color }};{% endif %}
{% if text_color %}$body-color: {{ text_color }};{% endif %}
{% if background_color %}$body-bg: {{ background_color }};{% endif %}
$enable-shadows: {{ enable_shadows and "true" or "false" }};
$enable-gradients: {{ enable_gradients and "true" or "false" }};
$enable-rounded: {{ enable_rounded and "true" or "false" }};
@import "frappe/public/scss/website";
body {
{% if font_size %}
font-size: {{ font_size }};
{% endif %}
}