diff --git a/frappe/__init__.py b/frappe/__init__.py
index b302a749e7..4235605026 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -377,7 +377,7 @@ def get_module(modulename):
return importlib.import_module(modulename)
def scrub(txt):
- return txt.replace(' ','_').replace('-', '_').replace('/', '_').lower()
+ return txt.replace(' ','_').replace('-', '_').lower()
def unscrub(txt):
return txt.replace('_',' ').replace('-', ' ').title()
diff --git a/frappe/build.py b/frappe/build.py
index 3f49530dfa..fb057934b0 100644
--- a/frappe/build.py
+++ b/frappe/build.py
@@ -1,5 +1,5 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# MIT License. See license.txt
from __future__ import unicode_literals
from frappe.utils.minify import JavascriptMinify
@@ -16,7 +16,7 @@ def bundle(no_compress, make_copy=False):
# build js files
make_asset_dirs(make_copy=make_copy)
build(no_compress)
-
+
def watch(no_compress):
"""watch and rebuild if necessary"""
import time
@@ -25,18 +25,18 @@ def watch(no_compress):
while True:
if files_dirty():
build(no_compress=True)
-
+
time.sleep(3)
def make_asset_dirs(make_copy=False):
assets_path = os.path.join(frappe.local.sites_path, "assets")
for dir_path in [
- os.path.join(assets_path, 'js'),
+ os.path.join(assets_path, 'js'),
os.path.join(assets_path, 'css')]:
-
+
if not os.path.exists(dir_path):
os.makedirs(dir_path)
-
+
# symlink app/public > assets/app
for app_name in frappe.get_all_apps(True):
pymodule = frappe.get_module(app_name)
@@ -53,7 +53,7 @@ def build(no_compress=False):
assets_path = os.path.join(frappe.local.sites_path, "assets")
for target, sources in get_build_maps().iteritems():
- pack(os.path.join(assets_path, target), sources, no_compress)
+ pack(os.path.join(assets_path, target), sources, no_compress)
shutil.copy(os.path.join(os.path.dirname(os.path.abspath(frappe.__file__)), 'data', 'languages.txt'), frappe.local.sites_path)
# reset_app_html()
@@ -79,39 +79,46 @@ def get_build_maps():
else:
s = os.path.join(app_path, source)
source_paths.append(s)
-
+
build_maps[target] = source_paths
except Exception, e:
print path
raise
-
+
return build_maps
timestamps = {}
def pack(target, sources, no_compress):
from cStringIO import StringIO
-
+
outtype, outtxt = target.split(".")[-1], ''
jsm = JavascriptMinify()
-
+
for f in sources:
suffix = None
if ':' in f: f, suffix = f.split(':')
if not os.path.exists(f) or os.path.isdir(f): continue
timestamps[f] = os.path.getmtime(f)
try:
- with open(f, 'r') as sourcefile:
+ with open(f, 'r') as sourcefile:
data = unicode(sourcefile.read(), 'utf-8', errors='ignore')
-
- if outtype=="js" and (not no_compress) and suffix!="concat" and (".min." not in f):
+
+ extn = f.rsplit(".", 1)[1]
+
+ if outtype=="js" and extn=="js" and (not no_compress) and suffix!="concat" and (".min." not in f):
tmpin, tmpout = StringIO(data.encode('utf-8')), StringIO()
jsm.minify(tmpin, tmpout)
outtxt += unicode(tmpout.getvalue() or '', 'utf-8').strip('\n') + ';'
+ elif outtype=="js" and extn=="html":
+ # add to frappe.templates
+ content = data.replace("\n", " ").replace("'", "\'")
+ outtxt += """frappe.templates["{key}"] = '{content}';\n""".format(\
+ key=f.rsplit("/", 1)[1][:-5], content=content)
else:
outtxt += ('\n/*\n *\t%s\n */' % f)
outtxt += '\n' + data + '\n'
-
+
except Exception, e:
print "--Error in:" + f + "--"
print frappe.get_traceback()
@@ -119,10 +126,10 @@ def pack(target, sources, no_compress):
if not no_compress and outtype == 'css':
pass
#outtxt = cssmin(outtxt)
-
+
with open(target, 'w') as f:
f.write(outtxt.encode("utf-8"))
-
+
print "Wrote %s - %sk" % (target, str(int(os.path.getsize(target)/1024)))
def files_dirty():
@@ -135,4 +142,4 @@ def files_dirty():
return True
else:
return False
-
+
diff --git a/frappe/cli.py b/frappe/cli.py
index 8270d43498..0116bed406 100755
--- a/frappe/cli.py
+++ b/frappe/cli.py
@@ -230,7 +230,7 @@ def setup_utilities(parser):
# clear
parser.add_argument("--clear_web", default=False, action="store_true",
help="Clear website cache")
- parser.add_argument("--build_sitemap", default=False, action="store_true",
+ parser.add_argument("--build_website", default=False, action="store_true",
help="Build Website Route")
parser.add_argument("--sync_statics", default=False, action="store_true",
help="Sync files from templates/statics to Web Pages")
@@ -352,7 +352,7 @@ def add_to_installed_apps(*apps):
all_apps = frappe.get_all_apps(with_frappe=True)
for each in apps:
if each in all_apps:
- add_to_installed_apps(each, rebuild_sitemap=False)
+ add_to_installed_apps(each, rebuild_website=False)
frappe.destroy()
@cmd
@@ -402,13 +402,11 @@ def update(remote=None, branch=None, reload_gunicorn=False):
subprocess.check_output("killall -HUP gunicorn".split())
@cmd
-def latest(rebuild_website_config=True, quiet=False):
+def latest(rebuild_website=True, quiet=False):
import frappe.modules.patch_handler
import frappe.model.sync
- from frappe.website import rebuild_config
from frappe.utils.fixtures import sync_fixtures
import frappe.translate
- from frappe.website import statics
verbose = not quiet
@@ -419,17 +417,11 @@ def latest(rebuild_website_config=True, quiet=False):
frappe.modules.patch_handler.run_all()
# sync
frappe.model.sync.sync_all(verbose=verbose)
- sync_fixtures()
-
- sync = statics.sync()
- sync.start()
- sync.start(rebuild=True)
- # build website config if any changes in templates etc.
- if rebuild_website_config:
- rebuild_config()
-
frappe.translate.clear_cache()
+ sync_fixtures()
+ if rebuild_website:
+ build_website()
finally:
frappe.destroy()
@@ -561,10 +553,11 @@ def clear_all_sessions():
frappe.destroy()
@cmd
-def build_sitemap():
- from frappe.website import rebuild_config
+def build_website():
+ import frappe.website.sync
frappe.connect()
- rebuild_config()
+ frappe.website.sync.sync()
+ frappe.db.commit()
frappe.destroy()
@cmd
diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py
index d55d89cdea..dc7f150909 100644
--- a/frappe/core/doctype/communication/communication.py
+++ b/frappe/core/doctype/communication/communication.py
@@ -110,6 +110,7 @@ def get_customer_supplier(args=None):
def send_comm_email(d, name, sent_via=None, print_html=None, attachments='[]', send_me_a_copy=False):
footer = None
+
if sent_via:
if hasattr(sent_via, "get_sender"):
d.sender = sent_via.get_sender(d) or d.sender
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 26bdba1ab0..12333f289a 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -13,6 +13,10 @@ from frappe.model.document import Document
from frappe.model.db_schema import type_map
from frappe.core.doctype.property_setter.property_setter import make_property_setter
+form_grid_templates = {
+ "fields": "templates/form_grid/fields.html"
+}
+
class DocType(Document):
def validate(self):
if not frappe.conf.get("developer_mode"):
diff --git a/frappe/core/doctype/outgoing_email_settings/outgoing_email_settings.json b/frappe/core/doctype/outgoing_email_settings/outgoing_email_settings.json
index c4460d34c9..34fbd02313 100644
--- a/frappe/core/doctype/outgoing_email_settings/outgoing_email_settings.json
+++ b/frappe/core/doctype/outgoing_email_settings/outgoing_email_settings.json
@@ -1,88 +1,105 @@
{
- "allow_copy": 1,
- "creation": "2014-03-03 19:48:01.000000",
- "description": "Email Settings for Outgoing and Incoming Emails.",
- "docstatus": 0,
- "doctype": "DocType",
+ "allow_copy": 1,
+ "creation": "2014-03-03 19:48:01",
+ "description": "Email Settings for Outgoing and Incoming Emails.",
+ "docstatus": 0,
+ "doctype": "DocType",
"fields": [
{
- "description": "SMTP Server (e.g. smtp.gmail.com)",
- "fieldname": "mail_server",
- "fieldtype": "Data",
- "label": "Outgoing Mail Server",
+ "description": "SMTP Server (e.g. smtp.gmail.com)",
+ "fieldname": "mail_server",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Outgoing Mail Server",
"permlevel": 0
- },
+ },
{
- "description": "[?]",
- "fieldname": "use_ssl",
- "fieldtype": "Check",
- "label": "Use TLS",
+ "description": "[?]",
+ "fieldname": "use_ssl",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Use TLS",
"permlevel": 0
- },
+ },
{
- "description": "If non standard port (e.g. 587)",
- "fieldname": "mail_port",
- "fieldtype": "Int",
- "label": "Port",
+ "description": "If non standard port (e.g. 587)",
+ "fieldname": "mail_port",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Port",
"permlevel": 0
- },
+ },
{
- "fieldname": "cb0",
- "fieldtype": "Column Break",
+ "fieldname": "cb0",
+ "fieldtype": "Column Break",
"permlevel": 0
- },
+ },
{
- "description": "Set Login and Password if authentication is required.",
- "fieldname": "mail_login",
- "fieldtype": "Data",
- "label": "Login Id",
+ "description": "Set Login and Password if authentication is required.",
+ "fieldname": "mail_login",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Login Id",
"permlevel": 0
- },
+ },
{
- "description": "Check this if you want to send emails as this id only (in case of restriction by your email provider).",
- "fieldname": "always_use_login_id_as_sender",
- "fieldtype": "Check",
- "label": "Always use above Login Id as sender",
+ "description": "Check this if you want to send emails as this id only (in case of restriction by your email provider).",
+ "fieldname": "always_use_login_id_as_sender",
+ "fieldtype": "Check",
+ "label": "Always use above Login Id as sender",
"permlevel": 0
- },
+ },
{
- "fieldname": "mail_password",
- "fieldtype": "Password",
- "label": "Mail Password",
+ "fieldname": "mail_password",
+ "fieldtype": "Password",
+ "label": "Mail Password",
"permlevel": 0
- },
+ },
{
- "description": "System generated mails will be sent from this email id.",
- "fieldname": "auto_email_id",
- "fieldtype": "Data",
- "label": "Auto Email Id",
+ "description": "System generated mails will be sent from this email id.",
+ "fieldname": "auto_email_id",
+ "fieldtype": "Data",
+ "label": "Auto Email Id",
"permlevel": 0
- },
+ },
{
- "default": "1",
- "description": "If checked, an email with an attached HTML format will be added to part of the EMail body as well as attachment. To only send as attachment, uncheck this.",
- "fieldname": "send_print_in_body_and_attachment",
- "fieldtype": "Check",
- "label": "Send Print in Body and Attachment",
+ "default": "1",
+ "description": "If checked, an email with an attached HTML format will be added to part of the EMail body as well as attachment. To only send as attachment, uncheck this.",
+ "fieldname": "send_print_in_body_and_attachment",
+ "fieldtype": "Check",
+ "label": "Send Print in Body and Attachment",
"permlevel": 0
+ },
+ {
+ "fieldname": "section_break_10",
+ "fieldtype": "Section Break",
+ "permlevel": 0
+ },
+ {
+ "default": "
",
+ "fieldname": "footer",
+ "fieldtype": "Text Editor",
+ "label": "Email Footer",
+ "permlevel": 0,
+ "reqd": 0
}
- ],
- "icon": "icon-cog",
- "idx": 1,
- "in_create": 1,
- "issingle": 1,
- "modified": "2014-03-03 20:20:09.000000",
- "modified_by": "Administrator",
- "module": "Core",
- "name": "Outgoing Email Settings",
- "owner": "Administrator",
+ ],
+ "icon": "icon-cog",
+ "idx": 1,
+ "in_create": 1,
+ "issingle": 1,
+ "modified": "2014-06-26 02:15:27.070400",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Outgoing Email Settings",
+ "owner": "Administrator",
"permissions": [
{
- "create": 1,
- "permlevel": 0,
- "read": 1,
- "role": "System Manager",
+ "create": 1,
+ "permlevel": 0,
+ "read": 1,
+ "role": "System Manager",
"write": 1
}
]
-}
\ No newline at end of file
+}
diff --git a/frappe/core/doctype/outgoing_email_settings/outgoing_email_settings.py b/frappe/core/doctype/outgoing_email_settings/outgoing_email_settings.py
index 28e972cc9f..65838ee466 100644
--- a/frappe/core/doctype/outgoing_email_settings/outgoing_email_settings.py
+++ b/frappe/core/doctype/outgoing_email_settings/outgoing_email_settings.py
@@ -23,3 +23,6 @@ class OutgoingEmailSettings(Document):
# exceptions are handled in session connect
sess = smtpserver.sess
+
+def get_mail_footer():
+ return frappe.db.get_value("Outgoing Email Settings", "Outgoing Email Settings", "footer") or ""
diff --git a/frappe/core/page/desktop/desktop.css b/frappe/core/page/desktop/desktop.css
index 710882b873..084053fe8a 100644
--- a/frappe/core/page/desktop/desktop.css
+++ b/frappe/core/page/desktop/desktop.css
@@ -20,7 +20,7 @@
@media (max-width: 768px) {
.case-wrapper {
- margin: 12px;
+ margin: 9px;
width: 70px;
height: 80px;
}
diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js
index be34e0335b..f9cadd6889 100644
--- a/frappe/core/page/permission_manager/permission_manager.js
+++ b/frappe/core/page/permission_manager/permission_manager.js
@@ -35,6 +35,7 @@ frappe.PermissionEngine = Class.extend({
me.setup_appframe();
}
});
+
},
setup_appframe: function() {
var me = this;
@@ -192,8 +193,8 @@ frappe.PermissionEngine = Class.extend({
}
var checkbox = $("").appendTo(cell)
+ "
+ + (d.help || "") + "").appendTo(cell)
.attr("data-fieldname", fieldname)
.css("text-transform", "capitalize");
@@ -214,9 +215,12 @@ frappe.PermissionEngine = Class.extend({
me.set_show_users(role_cell, d.role);
if (d.permlevel===0) {
+ d.help = '';
add_check(role_cell, d, "apply_user_permissions")
.removeClass("col-md-4")
.css({"margin-top": "15px"});
+ d.help = "";
}
var cell = add_cell(row, d, "permlevel");
@@ -290,6 +294,12 @@ frappe.PermissionEngine = Class.extend({
},
add_check_events: function() {
var me = this;
+
+ this.body.on("click", ".show-user-permissions", function() {
+ frappe.route_options = { doctype: me.get_doctype() || "" };
+ frappe.set_route("user-permissions");
+ });
+
this.body.on("click", "input[type='checkbox']", function() {
var chk = $(this);
var args = {
diff --git a/frappe/core/page/user_permissions/user_permissions.js b/frappe/core/page/user_permissions/user_permissions.js
index 7d1392736b..c35c5574b5 100644
--- a/frappe/core/page/user_permissions/user_permissions.js
+++ b/frappe/core/page/user_permissions/user_permissions.js
@@ -2,7 +2,7 @@ frappe.pages['user-permissions'].onload = function(wrapper) {
frappe.ui.make_app_page({
parent: wrapper,
title: "User Permissions Manager",
- icon: "icon-user",
+ icon: "icon-shield",
single_column: true
});
$(wrapper).find(".layout-main").html("\
@@ -45,6 +45,10 @@ frappe.UserPermissions = Class.extend({
},
make: function() {
var me = this;
+ this.wrapper.appframe.add_primary_action("Role Permissions", function() {
+ frappe.route_options = { doctype: me.get_doctype() || "" };
+ frappe.set_route("permission-manager");
+ }, "icon-lock");
return frappe.call({
module:"frappe.core",
page:"user_permissions",
diff --git a/frappe/exceptions.py b/frappe/exceptions.py
index 805d68315b..fbd3fc03be 100644
--- a/frappe/exceptions.py
+++ b/frappe/exceptions.py
@@ -44,3 +44,4 @@ class DocstatusTransitionError(ValidationError): pass
class TimestampMismatchError(ValidationError): pass
class EmptyTableError(ValidationError): pass
class LinkExistsError(ValidationError): pass
+class InvalidEmailAddressError(ValidationError): pass
diff --git a/frappe/hooks.py b/frappe/hooks.py
index 6866b4ad17..fd1377c7c0 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -26,7 +26,7 @@ web_include_css = [
"style_settings.css"
]
-website_clear_cache = "frappe.templates.generators.website_group.clear_cache"
+website_clear_cache = "frappe.website.doctype.website_group.website_group.clear_cache"
write_file_keys = ["file_url", "file_name"]
@@ -34,34 +34,33 @@ notification_config = "frappe.core.notifications.get_notification_config"
before_tests = "frappe.utils.install.before_tests"
+website_generators = ["Web Page", "Blog Post", "Website Group", "Blog Category"]
+
# permissions
permission_query_conditions = {
- "Event": "frappe.core.doctype.event.event.get_permission_query_conditions",
- "ToDo": "frappe.core.doctype.todo.todo.get_permission_query_conditions"
- }
+ "Event": "frappe.core.doctype.event.event.get_permission_query_conditions",
+ "ToDo": "frappe.core.doctype.todo.todo.get_permission_query_conditions"
+}
has_permission = {
- "Event": "frappe.core.doctype.event.event.has_permission",
- "ToDo": "frappe.core.doctype.todo.todo.has_permission"
- }
-
-# bean
-
+ "Event": "frappe.core.doctype.event.event.has_permission",
+ "ToDo": "frappe.core.doctype.todo.todo.has_permission"
+}
doc_events = {
- "*": {
- "on_update": "frappe.core.doctype.notification_count.notification_count.clear_doctype_notifications",
- "on_cancel": "frappe.core.doctype.notification_count.notification_count.clear_doctype_notifications",
- "on_trash": "frappe.core.doctype.notification_count.notification_count.clear_doctype_notifications"
- },
- "User Vote": {
- "after_insert": "frappe.templates.generators.website_group.clear_cache_on_doc_event"
- },
- "Website Route Permission": {
- "on_update": "frappe.templates.generators.website_group.clear_cache_on_doc_event"
- }
+ "*": {
+ "on_update": "frappe.core.doctype.notification_count.notification_count.clear_doctype_notifications",
+ "on_cancel": "frappe.core.doctype.notification_count.notification_count.clear_doctype_notifications",
+ "on_trash": "frappe.core.doctype.notification_count.notification_count.clear_doctype_notifications"
+ },
+ "User Vote": {
+ "after_insert": "frappe.website.doctype.website_group.website_group.clear_cache_on_doc_event"
+ },
+ "Website Route Permission": {
+ "on_update": "frappe.website.doctype.website_group.website_group.clear_cache_on_doc_event"
}
+}
scheduler_events = {
"all": ["frappe.utils.email_lib.bulk.flush"],
@@ -72,6 +71,8 @@ scheduler_events = {
"frappe.sessions.clear_expired_sessions",
],
"hourly": [
- "frappe.templates.generators.website_group.clear_event_cache"
+ "frappe.website.doctype.website_group.website_group.clear_event_cache"
]
}
+
+mail_footer = "frappe.core.doctype.outgoing_email_settings.outgoing_email_settings.get_mail_footer"
diff --git a/frappe/installer.py b/frappe/installer.py
index 7e94db953e..52dd8d482d 100755
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -10,8 +10,8 @@ import os, json
import frappe
import frappe.database
import getpass
-from frappe import _
from frappe.model.db_schema import DbManager
+import frappe.website.sync
from frappe.model.sync import sync_for
from frappe.utils.fixtures import sync_fixtures
@@ -116,20 +116,22 @@ def install_app(name, verbose=False, set_as_patched=True):
for after_install in app_hooks.after_install or []:
frappe.get_attr(after_install)()
- sync_fixtures()
+ print "Installing Fixtures..."
+ sync_fixtures(name)
frappe.flags.in_install_app = False
-def add_to_installed_apps(app_name, rebuild_sitemap=True):
+def add_to_installed_apps(app_name, rebuild_website=True):
installed_apps = frappe.get_installed_apps()
if not app_name in installed_apps:
installed_apps.append(app_name)
frappe.db.set_global("installed_apps", json.dumps(installed_apps))
frappe.db.commit()
- if rebuild_sitemap:
- from frappe.website.doctype.website_template.website_template import rebuild_website_template
- rebuild_website_template()
+ if rebuild_website:
+ frappe.website.sync.sync()
+
+ frappe.db.commit()
frappe.clear_cache()
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index fd3d66e001..1095edf4bb 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -292,7 +292,7 @@ class BaseDocument(object):
return
for df in self.meta.get_select_fields():
- if not (self.get(df.fieldname) and df.options):
+ if df.fieldname=="naming_series" or not (self.get(df.fieldname) and df.options):
continue
options = (df.options or "").split("\n")
diff --git a/frappe/model/mapper.py b/frappe/model/mapper.py
index cdf65cad0a..485a0c0c23 100644
--- a/frappe/model/mapper.py
+++ b/frappe/model/mapper.py
@@ -22,7 +22,7 @@ def get_mapped_doc(from_doctype, from_docname, table_maps, target_doc=None,
elif isinstance(target_doc, basestring):
target_doc = frappe.get_doc(json.loads(target_doc))
- if not target_doc.has_permission("create"):
+ if not ignore_permissions and not target_doc.has_permission("create"):
target_doc.raise_no_permission_to("create")
map_doc(source_doc, target_doc, table_maps[source_doc.doctype])
diff --git a/frappe/model/sync.py b/frappe/model/sync.py
index b7ad94aa03..9ede198c1b 100644
--- a/frappe/model/sync.py
+++ b/frappe/model/sync.py
@@ -9,7 +9,6 @@ from __future__ import unicode_literals
import frappe
import os, sys
from frappe.modules.import_file import import_file_by_path
-from frappe.utils import get_path, cstr
from frappe.modules.patch_handler import block_user
def sync_all(force=0, verbose=False):
@@ -23,40 +22,41 @@ def sync_all(force=0, verbose=False):
frappe.clear_cache()
def sync_for(app_name, force=0, sync_everything = False, verbose=False):
+ files = []
for module_name in frappe.local.app_modules.get(app_name) or []:
folder = os.path.dirname(frappe.get_module(app_name + "." + module_name).__file__)
- walk_and_sync(folder, force, sync_everything, verbose=verbose)
+ files += get_doc_files(folder, force, sync_everything, verbose=verbose)
-def walk_and_sync(start_path, force=0, sync_everything = False, verbose=False):
- """walk and sync all doctypes and pages"""
-
- modules = []
-
- document_type = ['doctype', 'page', 'report', 'print_format']
+ l = len(files)
+ if l:
+ for i, doc_path in enumerate(files):
+ if import_file_by_path(doc_path, force=force) and verbose:
+ complete = int(float(i+1) / l * 40)
+ sys.stdout.write("\rSyncing {0}: [{1}{2}]".format(app_name, "="*complete, " "*(40-complete)))
+ sys.stdout.flush()
+ #print module_name + ' | ' + doctype + ' | ' + name
- for path, folders, files in os.walk(start_path):
- # sort folders so that doctypes are synced before pages or reports
+ frappe.db.commit()
- for dontwalk in (".git", "locale", "public"):
- if dontwalk in folders:
- folders.remove(dontwalk)
+ print ""
- folders.sort()
- if sync_everything or (os.path.basename(os.path.dirname(path)) in document_type):
- for f in files:
- f = cstr(f)
- if f.endswith(".json"):
- doc_name = f.split(".json")[0]
- if doc_name == os.path.basename(path):
+def get_doc_files(start_path, force=0, sync_everything = False, verbose=False):
+ """walk and sync all doctypes and pages"""
- module_name = path.split(os.sep)[-3]
- doctype = path.split(os.sep)[-2]
- name = path.split(os.sep)[-1]
+ out = []
+ document_type = ['doctype', 'page', 'report', 'print_format']
+ for doctype in document_type:
+ doctype_path = os.path.join(start_path, doctype)
+ if os.path.exists(doctype_path):
- if import_file_by_path(os.path.join(path, f), force=force) and verbose:
- print module_name + ' | ' + doctype + ' | ' + name
+ # Note: sorted is a hack because custom* and doc* need
+ # be synced first
- frappe.db.commit()
+ for docname in sorted(os.listdir(doctype_path)):
+ if os.path.isdir(os.path.join(doctype_path, docname)):
+ doc_path = os.path.join(doctype_path, docname, docname) + ".json"
+ if os.path.exists(doc_path):
+ out.append(doc_path)
- return modules
+ return out
diff --git a/frappe/modules/__init__.py b/frappe/modules/__init__.py
index 9f8890c482..3735d5f2ec 100644
--- a/frappe/modules/__init__.py
+++ b/frappe/modules/__init__.py
@@ -44,13 +44,26 @@ def export_doc(doctype, name, module=None):
def get_doctype_module(doctype):
return frappe.db.get_value('DocType', doctype, 'module') or "core"
+doctype_python_modules = {}
def load_doctype_module(doctype, module=None, prefix=""):
if not module:
module = get_doctype_module(doctype)
- return frappe.get_module(get_module_name(doctype, module, prefix))
-def get_module_name(doctype, module, prefix=""):
- from frappe.modules import scrub
+ app = get_module_app(module)
+
+ key = (app, doctype, prefix)
+
+ if key not in doctype_python_modules:
+ doctype_python_modules[key] = frappe.get_module(get_module_name(doctype, module, prefix))
+
+ return doctype_python_modules[key]
+
+def get_module_name(doctype, module, prefix="", app=None):
return '{app}.{module}.doctype.{doctype}.{prefix}{doctype}'.format(\
- app = scrub(frappe.local.module_app[scrub(module)]),
- module = scrub(module), doctype = scrub(doctype), prefix=prefix)
+ app = scrub(app or get_module_app(module)),
+ module = scrub(module),
+ doctype = scrub(doctype),
+ prefix=prefix)
+
+def get_module_app(module):
+ return frappe.local.module_app[scrub(module)]
diff --git a/frappe/patches.txt b/frappe/patches.txt
index 8962d1404b..14a584c4e6 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -41,4 +41,7 @@ frappe.patches.v4_0.fix_attach_field_file_url
execute:frappe.reset_perms("User") #2014-06-13
execute:frappe.db.sql("""delete from `tabUserRole` where ifnull(parentfield, '')=''""") #2014-06-17
frappe.patches.v4_0.remove_user_owner_custom_field
+execute:frappe.delete_doc("DocType", "Website Template")
+execute:frappe.reload_doc('website', 'doctype', 'website_route') #2014-06-17
execute:frappe.db.sql("""update `tabProperty Setter` set property_type='Text' where property in ('options', 'default')""") #2014-06-20
+
diff --git a/frappe/patches/v4_0/rename_sitemap_to_route.py b/frappe/patches/v4_0/rename_sitemap_to_route.py
index 2ef8de6486..a0922874ee 100644
--- a/frappe/patches/v4_0/rename_sitemap_to_route.py
+++ b/frappe/patches/v4_0/rename_sitemap_to_route.py
@@ -17,11 +17,11 @@ def execute():
frappe.reload_doc("website", "doctype", frappe.scrub(d))
rename_field_if_exists(d, "parent_website_sitemap", "parent_website_route")
- frappe.reload_doc("website", "doctype", "website_template")
+ #frappe.reload_doc("website", "doctype", "website_template")
frappe.reload_doc("website", "doctype", "website_route")
frappe.reload_doc("website", "doctype", "website_route_permission")
- rename_field_if_exists("Website Route", "website_sitemap_config", "website_template")
+ #rename_field_if_exists("Website Route", "website_sitemap_config", "website_template")
rename_field_if_exists("Website Route Permission", "website_sitemap", "website_route")
for d in ("blog_category", "blog_post", "web_page", "website_route", "website_group", "post", "user_vote"):
diff --git a/frappe/patches/v4_0/set_website_route_idx.py b/frappe/patches/v4_0/set_website_route_idx.py
index eb5fc1ebd6..82f64756a4 100644
--- a/frappe/patches/v4_0/set_website_route_idx.py
+++ b/frappe/patches/v4_0/set_website_route_idx.py
@@ -1,23 +1,24 @@
import frappe
def execute():
- from frappe.website.doctype.website_template.website_template import \
- get_pages_and_generators, get_template_controller
-
- frappe.reload_doc("website", "doctype", "website_template")
- frappe.reload_doc("website", "doctype", "website_route")
-
- for app in frappe.get_installed_apps():
- pages, generators = get_pages_and_generators(app)
- for g in generators:
- doctype = frappe.get_attr(get_template_controller(app, g["path"], g["fname"]) + ".doctype")
- module = frappe.db.get_value("DocType", doctype, "module")
- frappe.reload_doc(frappe.scrub(module), "doctype", frappe.scrub(doctype))
-
- frappe.db.sql("""update `tabBlog Category` set `title`=`name` where ifnull(`title`, '')=''""")
- frappe.db.sql("""update `tabWebsite Route` set idx=null""")
- for doctype in ["Blog Category", "Blog Post", "Web Page", "Website Group"]:
- frappe.db.sql("""update `tab{}` set idx=null""".format(doctype))
-
- from frappe.website.doctype.website_template.website_template import rebuild_website_template
- rebuild_website_template()
\ No newline at end of file
+ pass
+ # from frappe.website.doctype.website_template.website_template import \
+ # get_pages_and_generators, get_template_controller
+ #
+ # frappe.reload_doc("website", "doctype", "website_template")
+ # frappe.reload_doc("website", "doctype", "website_route")
+ #
+ # for app in frappe.get_installed_apps():
+ # pages, generators = get_pages_and_generators(app)
+ # for g in generators:
+ # doctype = frappe.get_attr(get_template_controller(app, g["path"], g["fname"]) + ".doctype")
+ # module = frappe.db.get_value("DocType", doctype, "module")
+ # frappe.reload_doc(frappe.scrub(module), "doctype", frappe.scrub(doctype))
+ #
+ # frappe.db.sql("""update `tabBlog Category` set `title`=`name` where ifnull(`title`, '')=''""")
+ # frappe.db.sql("""update `tabWebsite Route` set idx=null""")
+ # for doctype in ["Blog Category", "Blog Post", "Web Page", "Website Group"]:
+ # frappe.db.sql("""update `tab{}` set idx=null""".format(doctype))
+ #
+ # from frappe.website.doctype.website_template.website_template import rebuild_website_template
+ # rebuild_website_template()
diff --git a/frappe/patches/v4_0/website_sitemap_hierarchy.py b/frappe/patches/v4_0/website_sitemap_hierarchy.py
index 800a14f4e7..2ac591e415 100644
--- a/frappe/patches/v4_0/website_sitemap_hierarchy.py
+++ b/frappe/patches/v4_0/website_sitemap_hierarchy.py
@@ -6,9 +6,9 @@ from __future__ import unicode_literals
import frappe
def execute():
- frappe.db.sql("""update `tabWebsite Route` ws set ref_doctype=(select wsc.ref_doctype
- from `tabWebsite Template` wsc where wsc.name=ws.website_template)
- where ifnull(page_or_generator, '')!='Page'""")
+ # frappe.db.sql("""update `tabWebsite Route` ws set ref_doctype=(select wsc.ref_doctype
+ # from `tabWebsite Template` wsc where wsc.name=ws.website_template)
+ # where ifnull(page_or_generator, '')!='Page'""")
frappe.reload_doc("website", "doctype", "website_settings")
diff --git a/frappe/public/build.json b/frappe/public/build.json
index 53fb50072b..a033d26f14 100644
--- a/frappe/public/build.json
+++ b/frappe/public/build.json
@@ -32,7 +32,6 @@
"public/css/tag-it.css",
"public/css/bootstrap.css",
- "public/css/bootstrap-responsive.css",
"public/css/font-awesome.css",
"public/css/desk.css",
"public/css/appframe.css",
@@ -67,6 +66,9 @@
"public/js/frappe/router.js",
"public/js/frappe/desk.js",
"public/js/frappe/defaults.js",
+ "public/js/lib/microtemplate.js",
+
+ "public/html/print_template.html",
"public/js/legacy/globals.js",
"public/js/legacy/datatype.js",
diff --git a/frappe/public/html/print_template.html b/frappe/public/html/print_template.html
new file mode 100644
index 0000000000..aba7ff57dc
--- /dev/null
+++ b/frappe/public/html/print_template.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+ {%= title %}
+
+
+
+
+ {%= content %}
+
+
+
diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js
index daefe582ac..163719b174 100644
--- a/frappe/public/js/frappe/form/grid.js
+++ b/frappe/public/js/frappe/form/grid.js
@@ -6,6 +6,11 @@ frappe.ui.form.Grid = Class.extend({
$.extend(this, opts);
this.fieldinfo = {};
this.doctype = this.df.options;
+ this.template = null;
+ if(this.frm.meta.__form_grid_templates
+ && this.frm.meta.__form_grid_templates[this.df.fieldname]) {
+ this.template = this.frm.meta.__form_grid_templates[this.df.fieldname];
+ }
this.is_grid = true;
},
make: function() {
@@ -61,7 +66,7 @@ frappe.ui.form.Grid = Class.extend({
if(!force && this.data_rows_are_same(data)) {
// soft refresh
- this.header_row.refresh();
+ this.header_row && this.header_row.refresh();
for(var i in this.grid_rows) {
this.grid_rows[i].refresh();
}
@@ -261,38 +266,27 @@ frappe.ui.form.GridRow = Class.extend({
},
make_static_display: function() {
var me = this;
- this.make_static_display_template();
this.row.empty();
- $('' + (this.doc ? this.doc.idx : "#")+ '
')
+ $('' + (this.doc ? this.doc.idx : "#")+ '
')
.appendTo(this.row);
- for(var ci in this.static_display_template) {
- var df = this.static_display_template[ci][0];
- var colsize = this.static_display_template[ci][1];
- var txt = this.doc ?
- frappe.format(this.doc[df.fieldname], df, null, this.doc) :
- __(df.label);
- if(this.doc && df.fieldtype === "Select") {
- txt = __(txt);
- }
- var add_class = (["Text", "Small Text"].indexOf(df.fieldtype)===-1) ?
- " grid-overflow-ellipsis" : " grid-overflow-no-ellipsis";
- add_class += (["Int", "Currency", "Float"].indexOf(df.fieldtype)!==-1) ?
- " text-right": "";
-
- $col = $('')
- .html(txt)
- .attr("data-fieldname", df.fieldname)
- .data("df", df)
- .appendTo(this.row)
- if(!this.doc) $col.css({"font-weight":"bold"})
+ if(this.grid.template) {
+ $('').appendTo(this.row)
+ .html(frappe.render(this.grid.template, {doc:this.doc, frm:this.frm, grid_row: this}));
+ } else {
+ this.add_visible_columns();
}
- // TODO find a better solution
- // append button column
+ this.add_buttons();
+
+ $(this.frm.wrapper).trigger("grid-row-render", [this]);
+ },
+
+ add_buttons: function() {
+ var me = this;
if(this.doc && this.grid.is_editable()) {
if(!this.grid.$row_actions) {
- this.grid.$row_actions = $('
\
\
\
@@ -311,8 +305,34 @@ frappe.ui.form.GridRow = Class.extend({
}
}
- $(this.frm.wrapper).trigger("grid-row-render", [this]);
},
+
+ add_visible_columns: function() {
+ this.make_static_display_template();
+ for(var ci in this.static_display_template) {
+ var df = this.static_display_template[ci][0];
+ var colsize = this.static_display_template[ci][1];
+ var txt = this.doc ?
+ frappe.format(this.doc[df.fieldname], df, null, this.doc) :
+ __(df.label);
+ if(this.doc && df.fieldtype === "Select") {
+ txt = __(txt);
+ }
+ var add_class = (["Text", "Small Text"].indexOf(df.fieldtype)===-1) ?
+ " grid-overflow-ellipsis" : " grid-overflow-no-ellipsis";
+ add_class += (["Int", "Currency", "Float"].indexOf(df.fieldtype)!==-1) ?
+ " text-right": "";
+
+ $col = $('
')
+ .html(txt)
+ .attr("data-fieldname", df.fieldname)
+ .data("df", df)
+ .appendTo(this.row)
+ if(!this.doc) $col.css({"font-weight":"bold"})
+ }
+
+ },
+
make_static_display_template: function() {
if(this.static_display_template) return;
@@ -443,55 +463,6 @@ frappe.ui.form.GridRow = Class.extend({
$.extend(me.fields_dict[fieldname], fi);
})
- // var me = this,
- // make_row = function(label) {
- // if(label)
- // $('
'+ label +'
\
- //
')
- // .appendTo(me.form_area);
- //
- // var row = $('
')
- // .appendTo(me.form_area);
- //
- // var col_spans = 6;
- // if(row.parents(".form-column:first").hasClass("col-md-6"))
- // col_spans = 12;
- //
- // var col1 = $('
').appendTo(row),
- // col2 = $('
').appendTo(row);
- //
- // return [col1, col2];
- // },
- // cols = make_row(),
- // cnt = 0;
- //
- // $.each(me.docfields, function(ci, df) {
- // if(!df.hidden) {
- // if(df.fieldtype=="Section Break") {
- // cols = make_row(df.label);
- // cnt = 0;
- // return;
- // }
- // var fieldwrapper = $('
')
- // .appendTo(cols[cnt % 2])
- // var fieldobj = make_field(df, me.parent_df.options,
- // fieldwrapper.get(0), me.frm);
- // fieldobj.docname = me.doc.name;
- // fieldobj.refresh();
- // fieldobj.input &&
- // $(fieldobj.input).css({"max-height": "100px"});
- //
- // // set field properties
- // // used for setting custom get queries in links
- // if(me.grid.fieldinfo[df.fieldname])
- // $.extend(fieldobj, me.grid.fieldinfo[df.fieldname]);
- //
- // me.fields.push(fieldobj);
- // me.fields_dict[df.fieldname] = fieldobj;
- // cnt++;
- // }
- // });
-
this.toggle_add_delete_button_display(this.wrapper.find(".panel:first"));
this.grid.open_grid_row = this;
diff --git a/frappe/public/js/frappe/misc/user.js b/frappe/public/js/frappe/misc/user.js
index cfe614af37..6dcd55c959 100644
--- a/frappe/public/js/frappe/misc/user.js
+++ b/frappe/public/js/frappe/misc/user.js
@@ -41,7 +41,6 @@ frappe.ui.set_user_background = function(src, selector, style) {
frappe.dom.set_style(repl('%(selector)s { \
background: url("%(src)s") center center;\
background-attachment: fixed; \
- background-size: 100%; \
%(style)s \
}', {src:src, selector:selector, style: style==="Fill Screen" ? "background-size: cover;" : ""}));
}
diff --git a/frappe/public/js/frappe/misc/utils.js b/frappe/public/js/frappe/misc/utils.js
index 5096536c80..d4b418a7d2 100644
--- a/frappe/public/js/frappe/misc/utils.js
+++ b/frappe/public/js/frappe/misc/utils.js
@@ -247,5 +247,5 @@ frappe.utils = {
var dataURL = canvas.toDataURL("image/jpeg");
setTimeout(function() { callback(dataURL); }, 10 );
}
- }
+ },
};
diff --git a/frappe/public/js/frappe/provide.js b/frappe/public/js/frappe/provide.js
index a6091ec58b..6438b46839 100644
--- a/frappe/public/js/frappe/provide.js
+++ b/frappe/public/js/frappe/provide.js
@@ -1,8 +1,8 @@
// Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
-// MIT License. See license.txt
+// MIT License. See license.txt
// provide a namespace
-if(!window.frappe)
+if(!window.frappe)
window.frappe = {};
frappe.provide = function(namespace) {
// docs: create a namespace //
@@ -22,4 +22,5 @@ frappe.provide("locals");
frappe.provide("frappe.settings");
frappe.provide("frappe.utils");
frappe.provide("frappe.ui");
-frappe.provide("frappe.modules");
\ No newline at end of file
+frappe.provide("frappe.modules");
+frappe.provide("frappe.templates");
diff --git a/frappe/public/js/frappe/request.js b/frappe/public/js/frappe/request.js
index 2117ad8eb9..126a09a6fb 100644
--- a/frappe/public/js/frappe/request.js
+++ b/frappe/public/js/frappe/request.js
@@ -71,6 +71,7 @@ frappe.request.call = function(opts) {
500: function() {
msgprint(__("Server Error: Please check your server logs or contact tech support."))
opts.error && opts.error();
+
}
},
async: opts.async
@@ -144,7 +145,6 @@ frappe.request.prepare = function(opts) {
}
frappe.request.cleanup = function(opts, r) {
-
// stop button indicator
if(opts.btn) $(opts.btn).done_working();
diff --git a/frappe/public/js/frappe/views/doclistview.js b/frappe/public/js/frappe/views/doclistview.js
index 7738606fda..28cc7ad794 100644
--- a/frappe/public/js/frappe/views/doclistview.js
+++ b/frappe/public/js/frappe/views/doclistview.js
@@ -259,12 +259,19 @@ frappe.views.DocListView = frappe.ui.Listing.extend({
this.appframe.add_icon_btn("2", "icon-shield",
__("User Permissions Manager"), function() {
frappe.route_options = {
- property: me.doctype
+ doctype: me.doctype
};
frappe.set_route("user-permissions");
});
}
if(in_list(user_roles, "System Manager")) {
+ this.appframe.add_icon_btn("2", "icon-lock",
+ __("Role Permissions Manager"), function() {
+ frappe.route_options = {
+ doctype: me.doctype
+ };
+ frappe.set_route("permission-manager");
+ });
this.appframe.add_icon_btn("2", "icon-glass", __("Customize"), function() {
frappe.set_route("Form", "Customize Form", {
doctype: me.doctype
diff --git a/frappe/public/js/frappe/views/query_report.js b/frappe/public/js/frappe/views/query_report.js
index cdb3f67d19..c6f0b8feee 100644
--- a/frappe/public/js/frappe/views/query_report.js
+++ b/frappe/public/js/frappe/views/query_report.js
@@ -107,8 +107,9 @@ frappe.views.QueryReport = Class.extend({
},
callback: function(r) {
me.appframe.set_title(__("Query Report")+": " + __(me.report_name));
- frappe.dom.eval(r.message || "");
+ frappe.dom.eval(r.message.script || "");
me.setup_filters();
+ me.setup_html_format(r.message.html_format);
me.refresh();
}
});
@@ -124,6 +125,37 @@ frappe.views.QueryReport = Class.extend({
this.wrapper.find(".no-report-area").html(msg).toggle(true);
}
},
+ setup_html_format: function(html_format) {
+ var me = this;
+ if(html_format) {
+ this.appframe.add_primary_action(__('Print'), function() {
+ if(!me.data) {
+ msgprint(__("Run the report first"));
+ return;
+ }
+
+ var data = [];
+ $.each(me.data, function(i, d) {
+ var newd = {}; data.push(newd);
+ $.each(d, function(k, v) {
+ newd[k.replace(/ /g, "_").toLowerCase()] = v; });
+ });
+
+ var content = frappe.render(html_format,
+ {data: data, filters:me.get_values(), report:me});
+
+ var html = frappe.render(frappe.templates.print_template, {
+ title: __(me.report_name), content: content
+ });
+
+ var w = window.open();
+ w.document.write(html);
+ w.document.close();
+
+ }, "icon-print");
+
+ }
+ },
setup_filters: function() {
this.clear_filters();
var me = this;
diff --git a/frappe/public/js/legacy/datatype.js b/frappe/public/js/legacy/datatype.js
index 8bd282bd4f..ad3383a696 100644
--- a/frappe/public/js/legacy/datatype.js
+++ b/frappe/public/js/legacy/datatype.js
@@ -6,6 +6,8 @@ frappe.utils.full_name = function(fn, ln) {
}
function fmt_money(v, format){
+ // deprecated!
+ // for backward compatibility
return format_number(v, format);
}
diff --git a/frappe/public/js/lib/microtemplate.js b/frappe/public/js/lib/microtemplate.js
new file mode 100644
index 0000000000..3da5dad3ce
--- /dev/null
+++ b/frappe/public/js/lib/microtemplate.js
@@ -0,0 +1,34 @@
+// Simple JavaScript Templating
+// Adapted from John Resig - http://ejohn.org/ - MIT Licensed
+
+frappe.template = {compiled: {}, debug:{}};
+frappe.template.compile = function(str) {
+ if(str.indexOf("'")!==-1) {
+ console.log("Warning: Single quotes (') may not work in templates");
+ }
+ if(!frappe.template.compiled[str]) {
+ fn_str = "var p=[],print=function(){p.push.apply(p,arguments)};" +
+
+ // Introduce the data as local variables using with(){}
+ "with(obj){p.push('" +
+
+ // Convert the template into pure JavaScript
+ str
+ .replace(/[\r\t\n]/g, " ")
+ .split("{%").join("\t")
+ .replace(/((^|%})[^\t]*)'/g, "$1\r")
+ .replace(/\t=(.*?)%}/g, "',$1,'")
+ .split("\t").join("');")
+ .split("%}").join("p.push('")
+ .split("\r").join("\\'")
+ + "');}return p.join('');";
+
+ frappe.template.debug[str] = fn_str;
+ frappe.template.compiled[str] = new Function("obj", fn_str);
+ }
+
+ return frappe.template.compiled[str];
+};
+frappe.render = function(str, data) {
+ return frappe.template.compile(str)(data);
+};
diff --git a/frappe/templates/base.html b/frappe/templates/base.html
index 6af5cae355..5853a6965d 100644
--- a/frappe/templates/base.html
+++ b/frappe/templates/base.html
@@ -14,12 +14,9 @@ Built on Frappe.io. Free and Open Source Framework for the Web. https://frappe.i
{%- block head_include %}{% endblock -%}
{%- block head -%}
-
- {%- if metatags -%}
- {%- for name in metatags %}
-
- {%- endfor -%}
- {%- endif -%}
+ {% if meta_block is defined %}
+ {{ meta_block }}
+ {% endif %}
{%- for link in web_include_css %}
@@ -50,15 +47,15 @@ Built on Frappe.io. Free and Open Source Framework for the Web. https://frappe.i
diff --git a/frappe/templates/emails/new_message.html b/frappe/templates/emails/new_message.html
index b51e7129f4..a4a2ea933d 100644
--- a/frappe/templates/emails/new_message.html
+++ b/frappe/templates/emails/new_message.html
@@ -2,4 +2,4 @@
You have a new message from: {{ from }}
{{ message }}
-
Login and view in Browser
\ No newline at end of file
+
Login and view in Browser
diff --git a/frappe/templates/emails/new_user.html b/frappe/templates/emails/new_user.html
index 8c6b099c29..ef46f02dc3 100644
--- a/frappe/templates/emails/new_user.html
+++ b/frappe/templates/emails/new_user.html
@@ -2,9 +2,9 @@
Dear {{ first_name }}{% if last_name %} {{ last_name}}{% endif %},
A new account has been created for you.
Your login id is: {{ user }}
-
Click on the button below to complete your registration and set a new password.
-
Complete Registration
+
Click on the link below to complete your registration and set a new password.
+
Complete Registration
You can also copy-paste this link in your browser {{ link }}
Thank you,
-{{ user_fullname }}
\ No newline at end of file
+{{ user_fullname }}
diff --git a/frappe/templates/emails/standard.html b/frappe/templates/emails/standard.html
index a8f8ffd4de..d3a719a9b9 100644
--- a/frappe/templates/emails/standard.html
+++ b/frappe/templates/emails/standard.html
@@ -4,273 +4,16 @@
{{ subject or "" }}
-
-
-
-
-
-
-
- |
-
-
-
-
-
-
-
- {{ content }}
- |
-
-
-
-
-
- |
- |
-
-
-
+
{{ content }}
-
-
-
-
{{ print_html or "" }}
-