Merge branch 'develop' into email-domain-fixes

This commit is contained in:
Suraj Shetty 2020-04-18 15:57:23 +05:30 committed by GitHub
commit e48a26ced7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 1176 additions and 327 deletions

28
.github/frappe_linter/translation.py vendored Normal file
View file

@ -0,0 +1,28 @@
import re
import sys
errors_encounter = 0
pattern = re.compile(r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)")
start_pattern = re.compile(r"_{1,2}\([\"']{1,3}")
# skip first argument
files = sys.argv[1:]
for _file in files:
if not _file.endswith(('.py', '.js')):
continue
with open(_file, 'r') as f:
print(f'Checking: {_file}')
for num, line in enumerate(f, 1):
all_matches = start_pattern.finditer(line)
if all_matches:
for match in all_matches:
verify = pattern.search(line)
if not verify:
errors_encounter += 1
print(f'A syntax error has been discovered at line number: {num}')
print(f'Syntax error occurred with: {line}')
if errors_encounter > 0:
print('You can visit "https://frappe.io/docs/user/en/translations" to resolve this error.')
assert 1+1 == 3
else:
print('Good To Go!')

View file

@ -0,0 +1,22 @@
name: Frappe Linter
on:
pull_request:
branches:
- develop
- version-12-hotfix
- version-11-hotfix
jobs:
check_translation:
name: Translation Syntax Check
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- name: Setup python3
uses: actions/setup-python@v1
with:
python-version: 3.6
- name: Validating Translation Syntax
run: |
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
files=$(git diff --name-only $GITHUB_BASE_REF)
python $GITHUB_WORKSPACE/.github/frappe_linter/translation.py $files

View file

@ -1,7 +1,7 @@
pull_request_rules:
- name: Automatic merge on CI success and review
conditions:
- status-success=Codacy/PR Quality Review
- status-success=Sider
- status-success=Semantic Pull Request
- status-success=Travis CI - Pull Request
- status-success=security/snyk - package.json (frappe)
@ -14,7 +14,7 @@ pull_request_rules:
method: merge
- name: Automatic squash on CI success and review
conditions:
- status-success=Codacy/PR Quality Review
- status-success=Sider
- status-success=Semantic Pull Request
- status-success=Travis CI - Pull Request
- status-success=security/snyk - package.json (frappe)

View file

@ -1,5 +1,7 @@
{
"baseUrl": "http://test_site_ui:8000",
"projectId": "92odwv",
"adminPassword": "admin"
"adminPassword": "admin",
"defaultCommandTimeout": 10000,
"pageLoadTimeout": 15000
}

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

@ -1,13 +1,23 @@
from __future__ import unicode_literals, absolute_import, print_function
# imports - standard imports
import atexit
import compileall
import hashlib
import os
import re
import shutil
import sys
# imports - third party imports
import click
import hashlib, os, sys, compileall, re
# imports - module imports
import frappe
from frappe import _
from frappe.commands import pass_context, get_site
from frappe.commands import get_site, pass_context
from frappe.commands.scheduler import _is_scheduler_enabled
from frappe.installer import update_site_config
from frappe.utils import touch_file, get_site_path
from six import text_type
from frappe.utils import get_site_path, touch_file
@click.command('new-site')
@click.argument('site')
@ -68,32 +78,33 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
make_site_dirs()
installing = None
try:
installing = touch_file(get_site_path('locks', 'installing.lock'))
installing = touch_file(get_site_path('locks', 'installing.lock'))
atexit.register(_new_site_cleanup, site, mariadb_root_username, mariadb_root_password)
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password,
db_name=db_name, admin_password=admin_password, verbose=verbose,
source_sql=source_sql, force=force, reinstall=reinstall, db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port, no_mariadb_socket=no_mariadb_socket)
install_db(root_login=mariadb_root_username, root_password=mariadb_root_password, db_name=db_name,
admin_password=admin_password, verbose=verbose, source_sql=source_sql, force=force, reinstall=reinstall,
db_password=db_password, db_type=db_type, db_host=db_host, db_port=db_port, no_mariadb_socket=no_mariadb_socket)
apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
for app in apps_to_install:
_install_app(app, verbose=verbose, set_as_patched=not source_sql)
apps_to_install = ['frappe'] + (frappe.conf.get("install_apps") or []) + (list(install_apps) or [])
for app in apps_to_install:
_install_app(app, verbose=verbose, set_as_patched=not source_sql)
os.remove(installing)
frappe.utils.scheduler.toggle_scheduler(enable_scheduler)
frappe.db.commit()
frappe.utils.scheduler.toggle_scheduler(enable_scheduler)
frappe.db.commit()
scheduler_status = "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled"
print("*** Scheduler is", scheduler_status, "***")
scheduler_status = "disabled" if frappe.utils.scheduler.is_scheduler_disabled() else "enabled"
print("*** Scheduler is", scheduler_status, "***")
except frappe.exceptions.ImproperDBConfigurationError:
_drop_site(site, mariadb_root_username, mariadb_root_password, force=True)
def _new_site_cleanup(site, mariadb_root_username, mariadb_root_password):
installing = get_site_path('locks', 'installing.lock')
finally:
if installing and os.path.exists(installing):
os.remove(installing)
if installing and os.path.exists(installing):
if mariadb_root_password:
_drop_site(site, mariadb_root_username, mariadb_root_password, force=True)
shutil.rmtree(site)
frappe.destroy()
frappe.destroy()
@click.command('restore')
@click.argument('sql-file-path')
@ -317,10 +328,18 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non
"Backup"
from frappe.utils.backups import scheduled_backup
verbose = context.verbose
exit_code = 0
for site in context.sites:
frappe.init(site=site)
frappe.connect()
odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True)
try:
frappe.init(site=site)
frappe.connect()
odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True)
except Exception as e:
if verbose:
print("Backup failed for {0}. Database or site_config.json may be corrupted".format(site))
exit_code = 1
continue
if verbose:
from frappe.utils import now
print("database backup taken -", odb.backup_path_db, "- on", now())
@ -329,6 +348,7 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non
print("private files backup taken -", odb.backup_path_private_files, "- on", now())
frappe.destroy()
sys.exit(exit_code)
@click.command('remove-from-installed-apps')
@click.argument('app')

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

@ -608,8 +608,7 @@ def get_local_image(file_url):
try:
image = Image.open(file_path)
except IOError:
frappe.msgprint(_("Unable to read file format for {0}").format(file_url))
raise
frappe.msgprint(_("Unable to read file format for {0}").format(file_url), raise_exception=True)
content = None

View file

@ -1,4 +1,5 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:language_code",
"creation": "2014-08-22 16:12:17.249590",
@ -41,7 +42,9 @@
}
],
"icon": "fa fa-globe",
"modified": "2019-07-19 16:32:12.652550",
"in_create": 1,
"links": [],
"modified": "2020-04-16 22:11:33.066852",
"modified_by": "Administrator",
"module": "Core",
"name": "Language",

View file

@ -13,6 +13,7 @@
"field_order": [
"stopped",
"method",
"server_script",
"frequency",
"cron_format",
"last_execution",
@ -63,6 +64,14 @@
"options": "All\nHourly\nHourly Long\nDaily\nDaily Long\nWeekly\nWeekly Long\nMonthly\nMonthly Long\nCron\nYearly\nAnnual",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "server_script",
"fieldtype": "Link",
"label": "Server Script",
"options": "Server Script",
"read_only": 1,
"search_index": 1
}
],
"in_create": 1,
@ -72,7 +81,7 @@
"link_fieldname": "scheduled_job_type"
}
],
"modified": "2019-12-09 11:10:21.259929",
"modified": "2020-04-05 17:27:33.480562",
"modified_by": "Administrator",
"module": "Core",
"name": "Scheduled Job Type",

View file

@ -70,7 +70,12 @@ class ScheduledJobType(Document):
self.scheduler_log = None
try:
self.log_status('Start')
frappe.get_attr(self.method)()
if self.server_script:
script_name = frappe.db.get_value("Server Script", self.server_script)
if script_name:
frappe.get_doc('Server Script', script_name).execute_scheduled_method()
else:
frappe.get_attr(self.method)()
frappe.db.commit()
self.log_status('Complete')
except Exception:

View file

@ -2,7 +2,45 @@
// For license information, please see license.txt
frappe.ui.form.on('Server Script', {
// refresh: function(frm) {
refresh: function(frm) {
if(frm.doc.script_type === 'Scheduler Event' && !frm.doc.disabled){
frm.add_custom_button('Schedule Script', function() {
var d = new frappe.ui.Dialog({
title: "Schedule Script Execution",
fields: [
{
fieldname: "event_type",
label: __('Select Event Type'),
fieldtype: "Select",
options: "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long"
},
],
primary_action_label: __('Schedule Script'),
primary_action: () => {
d.get_primary_btn().attr('disabled', true);
var data = d.get_values();
d.hide();
if(data) {
frm.events.schedule_script(frm, data);
}
}
});
d.show();
});
}
},
schedule_script(frm, data){
frm.call({
method: "frappe.core.doctype.server_script.server_script.setup_scheduler_events",
args: {
'script_name': frm.doc.name,
'frequency': data.event_type
}
})
}
// }
});

View file

@ -22,7 +22,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Script Type",
"options": "DocType Event\nAPI",
"options": "DocType Event\nScheduler Event\nAPI",
"reqd": 1
},
{
@ -75,7 +75,7 @@
}
],
"links": [],
"modified": "2019-12-17 12:55:07.389775",
"modified": "2020-04-06 11:24:38.161555",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",

View file

@ -7,6 +7,8 @@ from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.utils.safe_exec import safe_exec
from frappe import _
class ServerScript(Document):
@staticmethod
@ -31,3 +33,39 @@ class ServerScript(Document):
# execute event
safe_exec(self.script, None, dict(doc = doc))
def execute_scheduled_method(self):
if self.script_type == 'Scheduler Event':
safe_exec(self.script)
else:
# wrong report type!
raise frappe.DoesNotExistError
@frappe.whitelist()
def setup_scheduler_events(script_name, frequency):
method = frappe.scrub(script_name) + '_' + frequency.lower()
scheduled_script = frappe.db.get_value('Scheduled Job Type',
dict(method=method))
if not scheduled_script:
doc = frappe.get_doc(dict(
doctype = 'Scheduled Job Type',
method = method,
frequency = frequency,
server_script = script_name
))
doc.insert()
frappe.msgprint(_('Enabled scheduled execution for script {0}').format(script_name))
else:
doc = frappe.get_doc('Scheduled Job Type', scheduled_script)
doc.update(dict(
doctype = 'Scheduled Job Type',
method = method,
frequency = frequency,
server_script = script_name
))
doc.save()
frappe.msgprint(_('Scheduled execution for script {0} has updated').format(script_name))

View file

@ -66,6 +66,7 @@ def get_server_script_map():
script_map.setdefault(script.reference_doctype, {}).setdefault(script.doctype_event, []).append(script.name)
else:
script_map.setdefault('_api', {})[script.api_method] = script.name
frappe.cache().set_value('server_script_map', script_map)
return script_map
return script_map

View file

@ -208,9 +208,11 @@ class CustomizeForm(Document):
self.validate_fieldtype_change(df, meta_df[0].get(property), df.get(property))
elif property == "allow_on_submit" and df.get(property):
frappe.msgprint(_("Row {0}: Not allowed to enable Allow on Submit for standard fields")\
.format(df.idx))
continue
if not frappe.db.get_value("DocField",
{"parent": self.doc_type, "fieldname": df.fieldname}, "allow_on_submit"):
frappe.msgprint(_("Row {0}: Not allowed to enable Allow on Submit for standard fields")\
.format(df.idx))
continue
elif property == "reqd" and \
((frappe.db.get_value("DocField",
@ -369,7 +371,12 @@ class CustomizeForm(Document):
for allowed_changes in allowed_fieldtype_change:
if (old_value in allowed_changes and new_value in allowed_changes):
allowed = True
if frappe.db.type_map.get(old_value)[1] > frappe.db.type_map.get(new_value)[1]:
old_value_length = cint(frappe.db.type_map.get(old_value)[1])
new_value_length = cint(frappe.db.type_map.get(new_value)[1])
# Ignore fieldtype check validation if new field type has unspecified maxlength
# Changes like DATA to TEXT, where new_value_lenth equals 0 will not be validated
if new_value_length and (old_value_length > new_value_length):
self.check_length_for_fieldtypes.append({'df': df, 'old_value': old_value})
self.validate_fieldtype_length()
else:
@ -381,7 +388,7 @@ class CustomizeForm(Document):
def validate_fieldtype_length(self):
for field in self.check_length_for_fieldtypes:
df = field.get('df')
max_length = frappe.db.type_map.get(df.fieldtype)[1]
max_length = cint(frappe.db.type_map.get(df.fieldtype)[1])
fieldname = df.fieldname
docs = frappe.db.sql('''
SELECT name, {fieldname}, LENGTH({fieldname}) AS len

View file

@ -506,9 +506,13 @@ class BaseDocument(object):
fetch_from_fieldname = _df.fetch_from.split('.')[-1]
value = values[fetch_from_fieldname]
if _df.fieldtype == 'Small Text' or _df.fieldtype == 'Text' or _df.fieldtype == 'Data':
fetch_from_df = frappe.get_meta(doctype).get_field(fetch_from_fieldname)
fetch_from_ft = fetch_from_df.get('fieldtype')
if fetch_from_fieldname in default_fields:
from frappe.model.meta import get_default_df
fetch_from_df = get_default_df(fetch_from_fieldname)
else:
fetch_from_df = frappe.get_meta(doctype).get_field(fetch_from_fieldname)
fetch_from_ft = fetch_from_df.get('fieldtype')
if fetch_from_ft == 'Text Editor' and value:
value = unescape_html(strip_html(value))
setattr(self, _df.fieldname, value)

View file

@ -210,7 +210,7 @@ def check_permission_and_not_submitted(doc):
# check if submitted
if doc.docstatus == 1:
frappe.msgprint(_("{0} {1}: Submitted Record cannot be deleted.").format(_(doc.doctype), doc.name),
frappe.msgprint(_("{0} {1}: Submitted Record cannot be deleted. You must {2} Cancel {3} it first.").format(_(doc.doctype), doc.name, "<a href='https://docs.erpnext.com//docs/user/manual/en/setting-up/articles/delete-submitted-document' target='_blank'>", "</a>"),
raise_exception=True)
def check_if_doc_is_linked(doc, method="Delete"):

View file

@ -272,3 +272,4 @@ execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Account')
execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Settings')
frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats
execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders()
frappe.patches.v13_0.website_theme_custom_scss

View file

View file

@ -0,0 +1,10 @@
import frappe
def execute():
frappe.reload_doctype('Website Theme')
for theme in frappe.get_all('Website Theme'):
doc = frappe.get_doc('Website Theme', theme.name)
if not doc.custom_scss and doc.theme_scss:
# move old theme to new theme
doc.custom_scss = doc.theme_scss
doc.save()

View file

@ -147,7 +147,7 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({
[{ 'color': [] }, { 'background': [] }],
['blockquote', 'code-block'],
['link', 'image'],
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
[{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'list': 'check' }],
[{ 'align': [] }],
[{ 'indent': '-1'}, { 'indent': '+1' }],
[{'table': [

View file

@ -125,8 +125,9 @@ frappe.ui.form.Dashboard = Class.extend({
},
format_percent: function(title, percent) {
var width = cint(percent) < 1 ? 1 : cint(percent);
var progress_class = "progress-bar-success";
const percentage = cint(percent);
const width = percentage < 0 ? 100 : percentage;
const progress_class = percentage < 0 ? "progress-bar-danger" : "progress-bar-success";
return [{
title: title,

View file

@ -21,7 +21,7 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
remove_empty_rows();
$(frm.wrapper).addClass('validated-form');
if (frm.is_dirty() && check_mandatory()) {
if ((action !== 'Save' || frm.is_dirty()) && check_mandatory()) {
_call({
method: "frappe.desk.form.save.savedocs",
args: { doc: frm.doc, action: action },

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

@ -1,64 +1,71 @@
// moment strings for translation
function prettyDate(date, mini) {
if (!date) return '';
function prettyDate(time, mini) {
if (!time) {
time = new Date();
}
if ('moment' in window) { // use frappe.ImportError ;)
let ret;
if (frappe.sys_defaults && frappe.sys_defaults.time_zone) {
ret = moment.tz(time, frappe.sys_defaults.time_zone).locale(frappe.boot.lang).fromNow(mini);
} else {
ret = moment(time).locale(frappe.boot.lang).fromNow(mini);
}
if (mini) {
if (ret === moment().locale(frappe.boot.lang).fromNow(mini)) {
ret = __("now");
} else {
var parts = ret.split(" ");
if (parts.length > 1) {
if (parts[0] === "a" || parts[0] === "an") {
parts[0] = 1;
}
if (parts[1].substr(0, 2) === "mo") {
ret = parts[0] + " M";
} else {
ret = parts[0] + " " + parts[1].substr(0, 1);
}
}
if (typeof (date) == "string")
date = new Date((date || "").replace(/-/g, "/").replace(/[TZ]/g, " ").replace(/\.[0-9]*/, ""));
let diff = (((new Date()).getTime() - date.getTime()) / 1000);
let day_diff = Math.floor(diff / 86400);
if (isNaN(day_diff) || day_diff < 0) return '';
if (mini) {
// Return short format of time difference
if (day_diff == 0) {
if (diff < 60) {
return __("Now");
} else if (diff < 3600) {
return __("{0} m", [Math.floor(diff / 60)]);
} else if (diff < 86400) {
return __("{0} h", [Math.floor(diff / 3600)]);
}
} else {
if (day_diff < 7) {
return __("{0} D", [day_diff]);
} else if (day_diff < 31) {
return __("{0} W", [Math.ceil(day_diff / 7)]);
} else if (day_diff < 365) {
return __("{0} M", [Math.ceil(day_diff / 30)]);
} else {
return __("{0} Y", [Math.ceil(day_diff / 365)]);
}
ret = ret.substr(0, 5);
}
return ret;
} else {
if (!time) return '';
var date = time;
if (typeof (time) == "string")
date = new Date((time || "").replace(/-/g, "/").replace(/[TZ]/g, " ").replace(/\.[0-9]*/, ""));
var diff = (((new Date()).getTime() - date.getTime()) / 1000),
day_diff = Math.floor(diff / 86400);
if (isNaN(day_diff) || day_diff < 0)
return '';
var when = day_diff == 0 && (
diff < 60 && __("just now") ||
diff < 120 && __("1 minute ago") ||
diff < 3600 && __("{0} minutes ago", [Math.floor(diff / 60)]) ||
diff < 7200 && __("1 hour ago") ||
diff < 86400 && ("{0} hours ago", [Math.floor(diff / 3600)])) ||
day_diff == 1 && __("Yesterday") ||
day_diff < 7 && __("{0} days ago", day_diff) ||
day_diff < 31 && __("{0} weeks ago", [Math.ceil(day_diff / 7)]) ||
day_diff < 365 && __("{0} months ago", [Math.ceil(day_diff / 30)]) ||
__("> {0} year(s) ago", [Math.floor(day_diff / 365)]);
return when;
// Return long format of time difference
if (day_diff == 0) {
if (diff < 60) {
return __("Just now");
} else if (diff < 120) {
return __("1 minute ago");
} else if (diff < 3600) {
return __("{0} minutes ago", [Math.floor(diff / 60)]);
} else if (diff < 7200) {
return __("1 hour ago");
} else if (diff < 86400) {
return __("{0} hours ago", [Math.floor(diff / 3600)]);
}
} else {
if (day_diff == 1) {
return __("Yesterday");
} else if (day_diff < 7) {
return __("{0} days ago", [day_diff]);
} else if (day_diff < 14) {
return __("1 week ago");
} else if (day_diff < 31) {
return __("{0} weeks ago", [Math.ceil(day_diff / 7)]);
} else if (day_diff < 62) {
return __("1 month ago");
} else if (day_diff < 365) {
return __("{0} months ago", [Math.ceil(day_diff / 30)]);
} else if (day_diff < 730) {
return __("1 year ago");
} else {
return __("{0} years ago", [Math.ceil(day_diff / 365)]);
}
}
}
}
frappe.provide("frappe.datetime");
window.comment_when = function(datetime, mini) {
var timestamp = frappe.datetime.str_to_user ?

View file

@ -675,7 +675,9 @@ Object.assign(frappe.utils, {
return __(frappe.utils.to_title_case(route[0], true));
},
report_column_total: function(values, column, type) {
if (values.length > 0) {
if (column.column.disable_total) {
return '';
} else if (values.length > 0) {
if (column.column.fieldtype == "Percent" || type === "mean") {
return values.reduce((a, b) => a + flt(b)) / values.length;
} else if (column.column.fieldtype == "Int") {

View file

@ -203,8 +203,8 @@ class DesktopPage {
this.allow_customization && this.make_customization_link();
let create_shortcuts_and_cards = () => {
this.data.shortcuts.items.length && this.make_shortcuts();
this.data.cards.items.length && this.make_cards();
this.make_shortcuts();
this.make_cards();
if (this.allow_customization) {
// Move the widget group up to align with labels if customization is allowed
@ -212,7 +212,7 @@ class DesktopPage {
}
};
if (!this.sections["onboarding"] && this.data.charts.items.length) {
if (!this.sections["onboarding"]) {
this.make_charts().then(() => {
create_shortcuts_and_cards();
});

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() {
@ -113,8 +113,9 @@ frappe.ready(function() {
df.only_select = true;
}
if (["Attach", "Attach Image"].includes(df.fieldtype)) {
if (!df.options)
if (typeof df.options !== "object") {
df.options = {};
}
df.options.disable_file_browser = true;
}
});

View file

@ -32,6 +32,10 @@ details > summary {
cursor: pointer;
}
details > summary:focus {
outline: none;
}
.text-color {
color: @text-color !important;
}

View file

@ -4,8 +4,23 @@
@import "multilevel-dropdown";
@import "website-image";
html {
height: 100%;
}
body {
min-height: 100%;
display: flex;
flex-direction: column;
font-size: 16px;
> div {
flex: 1 0 auto;
}
}
footer {
flex-shrink: 0;
}
.navbar.bg-dark {

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

@ -11,6 +11,7 @@ class TestDBUpdate(unittest.TestCase):
frappe.reload_doctype('User', force=True)
frappe.model.meta.trim_tables('User')
make_property_setter(doctype, 'bio', 'fieldtype', 'Text', 'Data')
make_property_setter(doctype, 'middle_name', 'fieldtype', 'Data', 'Text')
make_property_setter(doctype, 'enabled', 'default', '1', 'Int')
frappe.db.updatedb(doctype)

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

@ -63,7 +63,6 @@ apps/frappe/frappe/templates/emails/download_data.html,We have received a reques
DocType: System Settings,"If enabled, the password strength will be enforced based on the Minimum Password Score value. A value of 2 being medium strong and 4 being very strong.","Falls diese Option aktiviert ist, wird die Passwortstärke auf der Grundlage des Minimum Password Score Wertes erzwungen. Ein Wert von 2 ist mittelstark und 4 sehr stark."
DocType: About Us Settings,"""Team Members"" or ""Management""",„Teammitglieder“ oder „Management“
apps/frappe/frappe/core/doctype/doctype/doctype.py,Default for 'Check' type of field must be either '0' or '1',Standard für 'Prüfen'-Feldtyp muss entweder '0' oder '1' sein
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,Yesterday,Gestern
DocType: Contact,Designation,Bezeichnung
apps/frappe/frappe/email/doctype/email_account/email_account.py,Automatic Linking can be activated only for one Email Account.,Die automatische Verknüpfung kann nur für ein E-Mail-Konto aktiviert werden.
DocType: Test Runner,Test Runner,Tester
@ -120,7 +119,6 @@ DocType: Dashboard Chart,Timespan,Zeitspanne
apps/frappe/frappe/public/js/frappe/file_uploader/FileUploader.vue,Web Link,Weblink
DocType: Deleted Document,Restored,Restauriert
apps/frappe/frappe/public/js/frappe/form/print.js,"Error connecting to QZ Tray Application...<br><br> You need to have QZ Tray application installed and running, to use the Raw Print feature.<br><br><a href=""https://qz.io/download/"" target=""_blank"">Click here to Download and install QZ Tray</a>.<br> <a href=""https://erpnext.com/docs/user/manual/en/setting-up/print/raw-printing"" target=""_blank"">Click here to learn more about Raw Printing</a>.","Fehler beim Verbinden mit der QZ-Tray-Anwendung ... <br><br> Sie müssen die QZ Tray-Anwendung installiert haben und ausführen, um die Raw Print-Funktion verwenden zu können. <br><br> <a href=""https://qz.io/download/"" target=""_blank"">Klicken Sie hier, um QZ Tray herunterzuladen und zu installieren</a> . <br> <a href=""https://erpnext.com/docs/user/manual/en/setting-up/print/raw-printing"" target=""_blank"">Klicken Sie hier, um mehr über den Rohdruck zu erfahren</a> ."
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,1 minute ago,Vor 1 Minute
apps/frappe/frappe/core/page/permission_manager/permission_manager_help.html,"Apart from System Manager, roles with Set User Permissions right can set permissions for other users for that Document Type.",Zusätzlich zum System-Manager können Rollen mit der Erlaubnis Benutzer anzulegen Berechtigungen für andere Nutzer für diesen Dokumententyp setzen.
apps/frappe/frappe/website/doctype/website_theme/website_theme.js,Configure Theme,Thema konfigurieren
DocType: Company History,Company History,Unternehmensgeschichte
@ -971,7 +969,6 @@ apps/frappe/frappe/public/js/frappe/list/list_view.js,Share URL,URL teilen
DocType: System Settings,Allow Consecutive Login Attempts ,Erlaube aufeinanderfolgende Login-Versuche
apps/frappe/frappe/templates/pages/integrations/stripe_checkout.html,An error occured during the payment process. Please contact us.,Während des Bezahlvorgangs ist ein Fehler aufgetreten. Bitte kontaktieren Sie uns.
DocType: Onboarding Slide,If Slide Type is Create or Settings there should be a 'create_onboarding_docs' method in the {ref_doctype}.py file bound to be executed after the slide is completed.,"Wenn der Folientyp &quot;Erstellen&quot; oder &quot;Einstellungen&quot; ist, sollte die Methode &quot;create_onboarding_docs&quot; in der Datei &quot;{ref_doctype} .py&quot; enthalten sein, die nach Abschluss der Folie ausgeführt werden soll."
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} days ago,vor {0} Tag(en)
DocType: Email Account,Awaiting Password,Warte auf Passwort
DocType: Address,Address Line 1,Adresse Zeile 1
apps/frappe/frappe/public/js/frappe/ui/filters/filter.js,Not Descendants Of,Nicht Nachkommen von
@ -1360,7 +1357,6 @@ apps/frappe/frappe/social/doctype/energy_point_rule/energy_point_rule.py,Referen
DocType: PayPal Settings,PayPal Settings,PayPal-Einstellungen
apps/frappe/frappe/core/page/permission_manager/permission_manager.js,Select Document Type,Dokumenttyp auswählen
apps/frappe/frappe/utils/nestedset.py,Cannot delete {0} as it has child nodes,"{0} kann nicht gelöscht werden, da es Unterknoten gibt"
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} minutes ago,vor {0} Minute(n)
apps/frappe/frappe/automation/doctype/assignment_rule/assignment_rule.py,Assignment Day {0} has been repeated.,Der Zuordnungstag {0} wurde wiederholt.
DocType: Kanban Board Column,lightblue,hellblau
apps/frappe/frappe/integrations/doctype/webhook/webhook.py,Same Field is entered more than once,Gleiches Feld wird mehrmals eingegeben
@ -1787,7 +1783,6 @@ DocType: Notification Log,Assignment,Zuordnung
DocType: Notification,Slack Channel,Slack-Kanal
DocType: About Us Team Member,Image Link,Bildverknüpfung
DocType: Auto Email Report,Report Filters,Berichtsfilter
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,now,jetzt
DocType: Workflow State,step-backward,Schritt zurück
apps/frappe/frappe/utils/boilerplate.py,{app_title},{app_title}
apps/frappe/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py,Please set Dropbox access keys in your site config,Bitte Dropbox-Zugriffsdaten in den Einstellungen der Seite setzen
@ -2520,7 +2515,6 @@ apps/frappe/frappe/public/js/frappe/form/print.js,QZ Tray Connection Active!,QZ-
DocType: Contact Us Settings,Settings for Contact Us Page,Einstellungen Kontakt
DocType: Server Script,Script Type,Skripttyp
DocType: Print Settings,Enable Print Server,Aktivieren Sie den Druckserver
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} weeks ago,vor {0} Woche(n)
DocType: Email Account,Footer,Fußzeile
apps/frappe/frappe/config/integrations.py,Authentication,Authentifizierung
apps/frappe/frappe/utils/verified_command.py,Invalid Link,Ungültige Verknüpfung
@ -3444,7 +3438,6 @@ apps/frappe/frappe/integrations/doctype/ldap_settings/ldap_settings.py,Please In
apps/frappe/frappe/core/doctype/data_import/log_details.html,Row Status,Zeilenstatus
DocType: S3 Backup Settings,sa-east-1,sa-east-1
DocType: Workflow Transition,Workflow Transition,Workflow-Übergang
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} months ago,vor {0} Monate(n)
apps/frappe/frappe/custom/doctype/custom_field/custom_field.py,Custom Fields can only be added to a standard DocType.,Benutzerdefinierte Felder können nur zu einem Standard-DocType hinzugefügt werden.
apps/frappe/frappe/public/js/frappe/list/list_sidebar_group_by.js,Created By,Erstellt von
apps/frappe/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py,Dropbox Setup,Dropbox-Setup
@ -3500,7 +3493,6 @@ apps/frappe/frappe/website/doctype/website_theme/website_theme.js,Theme Colors,T
apps/frappe/frappe/printing/page/print_format_builder/print_format_builder.js,Select a DocType to make a new format,"DocType auswählen, um ein neues Format zu erstellen"
apps/frappe/frappe/desk/page/user_profile/user_profile.js,User does not exist,Benutzer existiert nicht
apps/frappe/frappe/automation/doctype/auto_repeat/auto_repeat.py,'Recipients' not specified,"Keine ""Empfänger"" angegeben"
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,just now,gerade eben
apps/frappe/frappe/public/js/frappe/ui/filters/edit_filter.html,Apply,Anwenden
DocType: Footer Item,Policy,Politik
apps/frappe/frappe/integrations/utils.py,{0} Settings not found,{0} Einstellungen nicht gefunden
@ -3592,7 +3584,6 @@ DocType: Auto Email Report,Period,Periode
apps/frappe/frappe/core/doctype/data_import_beta/data_import_beta.js,About {0} minute remaining,Noch ungefähr {0} Minuten
apps/frappe/frappe/www/login.py,Invalid Login Token,Invalid Login Token
apps/frappe/frappe/public/js/frappe/chat.js,Discard,Verwerfen
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,1 hour ago,vor 1 Stunde
DocType: Website Settings,Home Page,Startseite
DocType: Error Snapshot,Parent Error Snapshot,Momentaufnahme des übergeordneten Fehlers
apps/frappe/frappe/public/js/frappe/data_import/import_preview.js,Map columns from {0} to fields in {1},Ordnen Sie Spalten von {0} Feldern in {1} zu.
@ -3804,7 +3795,6 @@ DocType: Webhook,on_update_after_submit,on_update_after_submit
DocType: System Settings,Allow Login using User Name,Login mit Benutzernamen zulassen
apps/frappe/frappe/core/doctype/report/report.js,Enable Report,Bericht aktivieren
DocType: DocField,Display Depends On,Anzeige ist abhängig von
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,&gt; {0} year(s) ago,&gt; {0} Jahr (e) her
DocType: Social Login Key,API Endpoint,API-Endpunkt
DocType: Web Page,Insert Code,Code einfügen
DocType: Data Migration Run,Current Mapping Type,Aktueller Kartentyp
@ -4188,3 +4178,23 @@ DocType: DocField,Ignore User Permissions,Ignorieren von Benutzerberechtigungen
apps/frappe/frappe/public/js/frappe/web_form/web_form.js,Saved Successfully,Erfolgreich gespeichert
apps/frappe/frappe/core/doctype/user/user.py,Please ask your administrator to verify your sign-up,Bitte fragen Sie Ihren Administrator Ihre Anmeldung bis zum überprüfen
DocType: Domain Settings,Active Domains,Aktive Domains
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,Now,Jetzt
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} m,{0} m
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} h,{0} h
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} D,{0} T
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} W,{0} W
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} M,{0} M
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} Y,{0} J
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,Just now,Gerade eben
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,1 minute ago,Vor einer Minute
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} minutes ago,Vor {0} Minuten
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,1 hour ago,Vor einer Stunde
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} hours ago,Vor {0} Stunden
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,Yesterday,Gestern
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} days ago,Vor {0} Tagen
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,1 week ago,Vor einer Woche
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} weeks ago,Vor {0} Wochen
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,1 month ago,Vor einem Monat
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} months ago,Vor {0} Monaten
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,1 year ago,Vor einem Jahr
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js,{0} years ago,Vor {0} Jahren
1 apps/frappe/frappe/utils/change_log.py New {} releases for the following apps are available Neue {} Versionen für die folgenden Apps sind verfügbar
63 DocType: System Settings If enabled, the password strength will be enforced based on the Minimum Password Score value. A value of 2 being medium strong and 4 being very strong. Falls diese Option aktiviert ist, wird die Passwortstärke auf der Grundlage des Minimum Password Score Wertes erzwungen. Ein Wert von 2 ist mittelstark und 4 sehr stark.
64 DocType: About Us Settings "Team Members" or "Management" „Teammitglieder“ oder „Management“
65 apps/frappe/frappe/core/doctype/doctype/doctype.py Default for 'Check' type of field must be either '0' or '1' Standard für 'Prüfen'-Feldtyp muss entweder '0' oder '1' sein
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js Yesterday Gestern
66 DocType: Contact Designation Bezeichnung
67 apps/frappe/frappe/email/doctype/email_account/email_account.py Automatic Linking can be activated only for one Email Account. Die automatische Verknüpfung kann nur für ein E-Mail-Konto aktiviert werden.
68 DocType: Test Runner Test Runner Tester
119 apps/frappe/frappe/public/js/frappe/file_uploader/FileUploader.vue Web Link Weblink
120 DocType: Deleted Document Restored Restauriert
121 apps/frappe/frappe/public/js/frappe/form/print.js Error connecting to QZ Tray Application...<br><br> You need to have QZ Tray application installed and running, to use the Raw Print feature.<br><br><a href="https://qz.io/download/" target="_blank">Click here to Download and install QZ Tray</a>.<br> <a href="https://erpnext.com/docs/user/manual/en/setting-up/print/raw-printing" target="_blank">Click here to learn more about Raw Printing</a>. Fehler beim Verbinden mit der QZ-Tray-Anwendung ... <br><br> Sie müssen die QZ Tray-Anwendung installiert haben und ausführen, um die Raw Print-Funktion verwenden zu können. <br><br> <a href="https://qz.io/download/" target="_blank">Klicken Sie hier, um QZ Tray herunterzuladen und zu installieren</a> . <br> <a href="https://erpnext.com/docs/user/manual/en/setting-up/print/raw-printing" target="_blank">Klicken Sie hier, um mehr über den Rohdruck zu erfahren</a> .
apps/frappe/frappe/public/js/frappe/utils/pretty_date.js 1 minute ago Vor 1 Minute
122 apps/frappe/frappe/core/page/permission_manager/permission_manager_help.html Apart from System Manager, roles with Set User Permissions right can set permissions for other users for that Document Type. Zusätzlich zum System-Manager können Rollen mit der Erlaubnis Benutzer anzulegen Berechtigungen für andere Nutzer für diesen Dokumententyp setzen.
123 apps/frappe/frappe/website/doctype/website_theme/website_theme.js Configure Theme Thema konfigurieren
124 DocType: Company History Company History Unternehmensgeschichte
969 DocType: Address apps/frappe/frappe/public/js/frappe/ui/filters/filter.js Address Line 1 Not Descendants Of Adresse Zeile 1 Nicht Nachkommen von
970 apps/frappe/frappe/public/js/frappe/ui/filters/filter.js DocType: Contact Not Descendants Of Company Name Nicht Nachkommen von Firma
971 DocType: Contact DocType: Custom DocPerm Company Name Role Firma Rolle
DocType: Custom DocPerm Role Rolle
972 apps/frappe/frappe/templates/emails/delete_data_confirmation.html to your browser zu Ihrem Browser
973 apps/frappe/frappe/utils/data.py Cent Cent
974 Recorder Recorder
1357 apps/frappe/frappe/templates/includes/list/filters.html apps/frappe/frappe/core/page/background_jobs/background_jobs.html clear Finished bereinigen Fertig
1358 apps/frappe/frappe/core/page/background_jobs/background_jobs.html DocType: Prepared Report Finished Filter Values Fertig Werte filtern
1359 DocType: Prepared Report DocType: Communication Filter Values User Tags Werte filtern Schlagworte zum Benutzer
DocType: Communication User Tags Schlagworte zum Benutzer
1360 DocType: Data Migration Run Fail Fehlschlagen
1361 DocType: Workflow State download-alt download-alt
1362 DocType: Scheduled Job Type Last Execution Letzte Ausführung
1783 apps/frappe/frappe/core/doctype/data_export/exporter.py apps/frappe/frappe/desk/page/user_profile/user_profile.html Only mandatory fields are necessary for new records. You can delete non-mandatory columns if you wish. Show More Activity Für neue Datensätze sind nur Pflichtfelder zwingend erforderlich. Nicht zwingend erforderliche Spalten können gelöscht werden, falls gewünscht. Weitere Aktivitäten anzeigen
1784 apps/frappe/frappe/desk/page/user_profile/user_profile.html apps/frappe/frappe/templates/includes/login/login.js Show More Activity Enter Code displayed in OTP App. Weitere Aktivitäten anzeigen Geben Sie den in der OTP-App angezeigten Code ein.
1785 apps/frappe/frappe/templates/includes/login/login.js apps/frappe/frappe/public/js/frappe/views/calendar/calendar.js Enter Code displayed in OTP App. Unable to update event Geben Sie den in der OTP-App angezeigten Code ein. Ereignis kann nicht aktualisiert werden
apps/frappe/frappe/public/js/frappe/views/calendar/calendar.js Unable to update event Ereignis kann nicht aktualisiert werden
1786 apps/frappe/frappe/twofactor.py Verification code has been sent to your registered email address. Der Bestätigungscode wurde an Ihre registrierte E-Mail-Adresse gesendet.
1787 apps/frappe/frappe/core/doctype/user/user.py Throttled Gedrosselt
1788 apps/frappe/frappe/public/js/frappe/ui/notifications/notifications.js Your Target Dein Ziel
2515 apps/frappe/frappe/core/doctype/data_import/importer.py DocType: Deleted Document Not allowed to Import Deleted DocType Import nicht erlaubt Gelöschtes DocType
2516 DocType: Deleted Document apps/frappe/frappe/core/page/permission_manager/permission_manager_help.html Deleted DocType Permission Levels Gelöschtes DocType Berechtigungsebenen
2517 apps/frappe/frappe/core/page/permission_manager/permission_manager_help.html DocType: Workflow State Permission Levels Warning Berechtigungsebenen Warnung
DocType: Workflow State Warning Warnung
2518 apps/frappe/frappe/public/js/frappe/form/print.js This may get printed on multiple pages Dies kann auf mehreren Seiten ausgedruckt werden
2519 DocType: Data Migration Run Percent Complete Prozent abgeschlossen
2520 DocType: Google Calendar Pull from Google Calendar Aus Google Kalender ziehen
3438 apps/frappe/frappe/public/js/frappe/views/file/file_view.js apps/frappe/frappe/public/js/frappe/form/form.js Toggle Grid View Go to next record Rasteransicht wechseln Zum nächsten Datensatz wechseln
3439 apps/frappe/frappe/public/js/frappe/form/form.js DocType: Country Go to next record Time Format Zum nächsten Datensatz wechseln Zeitformat
3440 DocType: Country apps/frappe/frappe/integrations/doctype/paypal_settings/paypal_settings.py Time Format Invalid payment gateway credentials Zeitformat Ungültige Payment-Gateway-Anmeldeinformationen
apps/frappe/frappe/integrations/doctype/paypal_settings/paypal_settings.py Invalid payment gateway credentials Ungültige Payment-Gateway-Anmeldeinformationen
3441 DocType: Data Import This is the template file generated with only the rows having some error. You should use this file for correction and import. Dies ist die Vorlagendatei, die nur mit Fehlern in den Zeilen generiert wird. Sie sollten diese Datei zur Korrektur und zum Import verwenden.
3442 apps/frappe/frappe/config/users_and_permissions.py Set Permissions on Document Types and Roles Berechtigungen für Dokumenttypen und Rollen setzen
3443 DocType: Data Migration Run Remote ID Remote-ID
3493 DocType: DocField apps/frappe/frappe/integrations/doctype/google_calendar/google_calendar.js Translatable Syncing {0} of {1} Übersetzbar {0} von {1} synchronisieren
3494 apps/frappe/frappe/integrations/doctype/google_calendar/google_calendar.js DocType: Letter Head Syncing {0} of {1} Letter Head in HTML {0} von {1} synchronisieren Briefkopf in HTML
3495 DocType: Letter Head apps/frappe/frappe/desk/page/user_profile/user_profile.js Letter Head in HTML User Profile Briefkopf in HTML Benutzerprofil
apps/frappe/frappe/desk/page/user_profile/user_profile.js User Profile Benutzerprofil
3496 DocType: Web Form Web Form Web-Formular
3497 apps/frappe/frappe/public/js/frappe/form/controls/date.js Date {0} must be in format: {1} Das Datum {0} muss das folgende Format haben: {1}
3498 DocType: About Us Settings Org History Heading Überschrift zur Unternehmensgeschichte
3584 apps/frappe/frappe/www/printview.py Print Format {0} is disabled Address and Contacts Druckformat {0} ist deaktiviert Adresse und Kontaktinformationen
3585 DocType: Notification Address and Contacts Send days before or after the reference date Adresse und Kontaktinformationen Tage vor oder nach dem Stichtag senden
3586 DocType: Notification DocType: User Send days before or after the reference date Allow user to login only after this hour (0-24) Tage vor oder nach dem Stichtag senden Benutzer erlauben, sich erst nach dieser Stunde anzumelden (0-24)
DocType: User Allow user to login only after this hour (0-24) Benutzer erlauben, sich erst nach dieser Stunde anzumelden (0-24)
3587 apps/frappe/frappe/automation/doctype/assignment_rule/assignment_rule.js Assign one by one, in sequence Weisen Sie der Reihe nach eine nach der anderen zu
3588 DocType: Integration Request Subscription Notification Abonnementbenachrichtigung
3589 DocType: Customize Form Field Allow in Quick Entry Schnelleingabe zulassen
3795 DocType: Integration Request apps/frappe/frappe/public/js/frappe/ui/toolbar/awesome_bar.js Remote Calculate entfernt Berechnen
3796 apps/frappe/frappe/public/js/frappe/ui/toolbar/awesome_bar.js apps/frappe/frappe/printing/doctype/print_format/print_format.js Calculate Please select DocType first Berechnen Bitte zuerst DocType auswählen
3797 apps/frappe/frappe/printing/doctype/print_format/print_format.js apps/frappe/frappe/email/doctype/newsletter/newsletter.py Please select DocType first Confirm Your Email Bitte zuerst DocType auswählen Email-Adresse bestätigen
apps/frappe/frappe/email/doctype/newsletter/newsletter.py Confirm Your Email Email-Adresse bestätigen
3798 apps/frappe/frappe/www/login.html Or login with Oder melden Sie sich an mit
3799 DocType: Error Snapshot Locals Einheimische
3800 apps/frappe/frappe/desk/page/activity/activity_row.html Communicated via {0} on {1}: {2} Kommuniziert über {0} um {1}: {2}
4178
4179
4180
4181
4182
4183
4184
4185
4186
4187
4188
4189
4190
4191
4192
4193
4194
4195
4196
4197
4198
4199
4200

View file

@ -73,7 +73,7 @@ def enqueue(method, queue='default', timeout=None, event=None,
def enqueue_doc(doctype, name=None, method=None, queue='default', timeout=300,
now=False, **kwargs):
'''Enqueue a method to be run on a document'''
enqueue('frappe.utils.background_jobs.run_doc_method', doctype=doctype, name=name,
return enqueue('frappe.utils.background_jobs.run_doc_method', doctype=doctype, name=name,
doc_method=method, queue=queue, timeout=timeout, now=now, **kwargs)
def run_doc_method(doctype, name, doc_method, **kwargs):

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

@ -131,9 +131,9 @@ def create_auth_table():
frappe.db.create_auth_table()
def encrypt(pwd):
if len(pwd) > 100:
# encrypting > 100 chars will lead to truncation
frappe.throw(_('Password cannot be more than 100 characters long'))
if len(pwd) > 127:
# encrypting > 127 chars will lead to truncation
frappe.throw(_('Password cannot be more than 127 characters long'))
cipher_suite = Fernet(encode(get_encryption_key()))
cipher_text = cstr(cipher_suite.encrypt(encode(pwd)))

View file

@ -11,6 +11,7 @@ from frappe.website.utils import (get_shade, get_toc, get_next_link)
from frappe.modules import scrub
from frappe.www.printview import get_visible_columns
import frappe.exceptions
import frappe.integrations.utils
class ServerScriptNotEnabled(frappe.PermissionError): pass
@ -79,6 +80,8 @@ def get_safe_globals():
user=user,
csrf_token=frappe.local.session.data.csrf_token if getattr(frappe.local, "session", None) else ''
),
make_get_request = frappe.integrations.utils.make_get_request,
make_post_request = frappe.integrations.utils.make_post_request,
socketio_port=frappe.conf.socketio_port,
get_hooks=frappe.get_hooks,
),

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 %}
}