Merge branch 'develop' of github.com:frappe/frappe into mysql-syntax-error

This commit is contained in:
Gavin D'souza 2020-09-23 15:00:39 +05:30
commit e6eee1ff83
160 changed files with 2721 additions and 1878 deletions

34
.snyk
View file

@ -65,3 +65,37 @@ patch:
patched: '2020-04-30T23:02:32.330Z'
- quill-image-resize > lodash:
patched: '2020-08-24T23:06:37.710Z'
- node-sass > lodash:
patched: '2020-09-15T23:06:41.931Z'
- node-sass > sass-graph > lodash:
patched: '2020-09-15T23:06:41.931Z'
- node-sass > gaze > globule > lodash:
patched: '2020-09-15T23:06:41.931Z'
- snyk > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > @snyk/snyk-cocoapods-plugin > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-cpp-plugin > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-go-plugin > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-gradle-plugin > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-docker-plugin > snyk-nodejs-lockfile-parser > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-mvn-plugin > @snyk/java-call-graph-builder > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > @snyk/snyk-cocoapods-plugin > @snyk/cocoapods-lockfile-parser > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-php-plugin > @snyk/cli-interface > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-gradle-plugin > @snyk/cli-interface > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-mvn-plugin > @snyk/cli-interface > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > @snyk/dep-graph > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-nodejs-lockfile-parser > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'
- snyk > snyk-go-plugin > graphlib > lodash:
patched: '2020-09-16T23:06:38.881Z'

View file

@ -59,15 +59,18 @@ context('Recorder', () => {
cy.get('.title-text').should('contain', 'DocType');
cy.get('.list-count').should('contain', '20 of ');
cy.visit('/desk#recorder');
// temporarily commenting out theses tests as they seem to be
// randomly failing maybe due a backround event
cy.get('.list-row-container span').contains('/api/method/frappe').click();
// cy.visit('/desk#recorder');
cy.location('hash').should('contain', '#recorder/request/');
cy.get('form').should('contain', '/api/method/frappe');
// cy.get('.list-row-container span').contains('/api/method/frappe').click();
cy.get('#page-recorder .primary-action').should('contain', 'Stop').click();
cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click();
cy.location('hash').should('eq', '#recorder');
// cy.location('hash').should('contain', '#recorder/request/');
// cy.get('form').should('contain', '/api/method/frappe');
// cy.get('#page-recorder .primary-action').should('contain', 'Stop').click();
// cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click();
// cy.location('hash').should('eq', '#recorder');
});
});

View file

@ -146,7 +146,7 @@ class AutoRepeat(Document):
def make_new_document(self):
reference_doc = frappe.get_doc(self.reference_doctype, self.reference_document)
new_doc = frappe.copy_doc(reference_doc)
new_doc = frappe.copy_doc(reference_doc, ignore_no_copy = False)
self.update_doc(new_doc, reference_doc)
new_doc.insert(ignore_permissions = True)

View file

@ -4,7 +4,6 @@
from __future__ import unicode_literals
import frappe, json
import frappe.defaults
from frappe.model.document import Document
from frappe.desk.notifications import (delete_notification_count_for,
clear_notifications)

View file

@ -62,11 +62,11 @@
"label": "URLs"
}
],
"modified": "2019-11-07 13:21:19.395927",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Chat",
"name": "Chat Message",
"owner": "arjun@gmail.com",
"owner": "Administrator",
"permissions": [
{
"create": 1,

View file

@ -12,7 +12,6 @@ import click
# imports - module imports
import frappe
from frappe import _
from frappe.commands import get_site, pass_context
from frappe.commands.scheduler import _is_scheduler_enabled
from frappe.exceptions import SiteNotSpecifiedError

View file

@ -305,8 +305,6 @@ def import_doc(context, path, force=False):
@click.option('--submit-after-import', default=False, is_flag=True, help='Submit document after importing it')
@click.option('--ignore-encoding-errors', default=False, is_flag=True, help='Ignore encoding errors while coverting to unicode')
@click.option('--no-email', default=True, is_flag=True, help='Send email if applicable')
@pass_context
def import_csv(context, path, only_insert=False, submit_after_import=False, ignore_encoding_errors=False, no_email=True):
"Import CSV using data import"
@ -437,7 +435,7 @@ def jupyter(context):
os.mkdir(jupyter_notebooks_path)
bin_path = os.path.abspath('../env/bin')
print('''
Stating Jupyter notebook
Starting Jupyter notebook
Run the following in your first cell to connect notebook to frappe
```
import frappe

View file

@ -2,7 +2,7 @@
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"allow_rename": 1,
"autoname": "field:salutation",
"beta": 0,
"creation": "2017-04-10 12:17:58.071915",
@ -53,7 +53,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-04-10 12:55:18.855578",
"modified": "2020-09-14 12:55:18.855578",
"modified_by": "Administrator",
"module": "Contacts",
"name": "Salutation",
@ -129,4 +129,4 @@
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}
}

View file

@ -8,7 +8,7 @@ from frappe import _
import frappe.permissions
import re, csv, os
from frappe.utils.csvutils import UnicodeWriter
from frappe.utils import cstr, formatdate, format_datetime, parse_json, cint
from frappe.utils import cstr, formatdate, format_datetime, parse_json, cint, format_duration
from frappe.core.doctype.data_import_legacy.importer import get_data_keys
from six import string_types
from frappe.core.doctype.access_log.access_log import make_access_log
@ -330,6 +330,8 @@ class DataExporter:
value = formatdate(value)
elif fieldtype == "Datetime":
value = format_datetime(value)
elif fieldtype == "Duration":
value = format_duration(value, df.hide_days)
row[_column_start_end.start + i + 1] = value

View file

@ -8,6 +8,7 @@ from frappe.model import (
no_value_fields,
table_fields as table_fieldtypes,
)
from frappe.utils import flt, format_duration
from frappe.utils.csvutils import build_csv_response
from frappe.utils.xlsxutils import build_xlsx_response
@ -146,8 +147,13 @@ class Exporter:
if df.parent == doctype:
if df.is_child_table_field and df.child_table_df.fieldname != parentfield:
continue
row[i] = doc.get(df.fieldname, "")
value = doc.get(df.fieldname, None)
if df.fieldtype == "Duration":
value = flt(value or 0)
value = format_duration(value, df.hide_days)
row[i] = value
return rows
def get_data_as_docs(self):

View file

@ -1,5 +1,5 @@
Title ,Description ,Number ,another_number ,ID (Table Field 1) ,Child Title (Table Field 1) ,Child Description (Table Field 1) ,Child 2 Title (Table Field 2) ,Child 2 Date (Table Field 2) ,Child 2 Number (Table Field 2) ,Child Title (Table Field 1 Again) ,Child Date (Table Field 1 Again) ,Child Number (Table Field 1 Again) ,table_field_1_again.child_another_number
Test ,test description ,1 ,2 ,"" ,child title ,child description ,child title ,14-08-2019 ,4 ,child title again ,22-09-2020 ,5 , 7
, , , , ,child title 2 ,child description 2 ,title child ,30-10-2019 ,5 ,child title again 2 ,22-09-2021 , ,
Test 2 ,test description 2 ,1 ,2 , ,child mandatory title , ,title child man , , ,child mandatory again , , ,
Test 3 ,test description 3 ,4 ,5 ,"" ,child title asdf ,child description asdf ,child title asdf adsf ,15-08-2019 ,6 ,child title again asdf ,22-09-2022 ,9 , 71
Title ,Description ,Number ,Duration,another_number ,ID (Table Field 1),Child Title (Table Field 1),Child Description (Table Field 1),Child 2 Title (Table Field 2),Child 2 Date (Table Field 2),Child 2 Number (Table Field 2),Child Title (Table Field 1 Again),Child Date (Table Field 1 Again),Child Number (Table Field 1 Again),table_field_1_again.child_another_number
Test ,test description ,1,3h,2, ,child title ,child description ,child title ,14-08-2019,4,child title again ,22-09-2020,5,7
, , ,, , ,child title 2,child description 2,title child ,30-10-2019,5,child title again 2,22-09-2021, ,
Test 2,test description 2,1,4d 3h,2, ,child mandatory title , ,title child man , , ,child mandatory again , , ,
Test 3,test description 3,4,5d 5h 45m,5, ,child title asdf ,child description asdf ,child title asdf adsf ,15-08-2019,6,child title again asdf ,22-09-2022,9,71
Can't render this file because it contains an unexpected character in line 2 and column 54.

View file

@ -9,7 +9,7 @@ import timeit
import json
from datetime import datetime, date
from frappe import _
from frappe.utils import cint, flt, update_progress_bar, cstr
from frappe.utils import cint, flt, update_progress_bar, cstr, duration_to_seconds
from frappe.utils.csvutils import read_csv_content, get_csv_content_from_google_sheets
from frappe.utils.xlsxutils import (
read_xlsx_file_from_attached_file,
@ -664,6 +664,20 @@ class Row:
}
)
return
elif df.fieldtype == "Duration":
import re
is_valid_duration = re.match("^(?:(\d+d)?((^|\s)\d+h)?((^|\s)\d+m)?((^|\s)\d+s)?)$", value)
if not is_valid_duration:
self.warnings.append(
{
"row": self.row_number,
"col": col.column_number,
"field": df_as_json(df),
"message": _("Value {0} must be in the valid duration format: d h m s").format(
frappe.bold(value)
)
}
)
return value
@ -692,6 +706,8 @@ class Row:
value = flt(value)
elif df.fieldtype in ["Date", "Datetime"]:
value = self.get_date(value, col)
elif df.fieldtype == "Duration":
value = duration_to_seconds(value)
return value

View file

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils import getdate
from frappe.utils import getdate, format_duration
doctype_name = 'DocType for Import'
@ -24,6 +24,7 @@ class TestImporter(unittest.TestCase):
self.assertEqual(doc1.description, 'test description')
self.assertEqual(doc1.number, 1)
self.assertEqual(format_duration(doc1.duration), '3h')
self.assertEqual(doc1.table_field_1[0].child_title, 'child title')
self.assertEqual(doc1.table_field_1[0].child_description, 'child description')
@ -40,7 +41,10 @@ class TestImporter(unittest.TestCase):
self.assertEqual(doc1.table_field_1_again[1].child_date, getdate('2021-09-22'))
self.assertEqual(doc2.description, 'test description 2')
self.assertEqual(format_duration(doc2.duration), '4d 3h')
self.assertEqual(doc3.another_number, 5)
self.assertEqual(format_duration(doc3.duration), '5d 5h 45m')
def test_data_import_preview(self):
import_file = get_import_file('sample_import_file')
@ -48,7 +52,7 @@ class TestImporter(unittest.TestCase):
preview = data_import.get_preview_from_template()
self.assertEqual(len(preview.data), 4)
self.assertEqual(len(preview.columns), 15)
self.assertEqual(len(preview.columns), 16)
def test_data_import_without_mandatory_values(self):
import_file = get_import_file('sample_import_file_without_mandatory')
@ -146,6 +150,7 @@ def create_doctype_if_not_exists(doctype_name, force=False):
{'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'},
{'label': 'Description', 'fieldname': 'description', 'fieldtype': 'Small Text'},
{'label': 'Date', 'fieldname': 'date', 'fieldtype': 'Date'},
{'label': 'Duration', 'fieldname': 'duration', 'fieldtype': 'Duration'},
{'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'},
{'label': 'Number', 'fieldname': 'another_number', 'fieldtype': 'Int'},
{'label': 'Table Field 1', 'fieldname': 'table_field_1', 'fieldtype': 'Table', 'options': table_1_name},

View file

@ -15,7 +15,7 @@ from frappe import _
from frappe.utils.csvutils import getlink
from frappe.utils.dateutils import parse_date
from frappe.utils import cint, cstr, flt, getdate, get_datetime, get_url, get_absolute_url
from frappe.utils import cint, cstr, flt, getdate, get_datetime, get_url, get_absolute_url, duration_to_seconds
from six import string_types
@ -164,7 +164,8 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False,
d[fieldname] = get_datetime(_date + " " + _time)
else:
d[fieldname] = None
elif fieldtype == "Duration":
d[fieldname] = duration_to_seconds(cstr(d[fieldname]))
elif fieldtype in ("Image", "Attach Image", "Attach"):
# added file to attachments list
attachments.append(d[fieldname])

View file

@ -99,6 +99,10 @@ class DocType(Document):
if self.default_print_format and not self.custom:
frappe.throw(_('Standard DocType cannot have default print format, use Customize Form'))
if frappe.conf.get('developer_mode'):
self.owner = 'Administrator'
self.modified_by = 'Administrator'
def set_default_in_list_view(self):
'''Set default in-list-view for first 4 mandatory fields'''
if not [d.fieldname for d in self.fields if d.in_list_view]:
@ -234,6 +238,8 @@ class DocType(Document):
if not autoname and self.get("fields", {"fieldname":"naming_series"}):
self.autoname = "naming_series:"
elif self.autoname == "naming_series:" and not self.get("fields", {"fieldname":"naming_series"}):
frappe.throw(_("Invalid fieldname '{0}' in autoname").format(self.autoname))
# validate field name if autoname field:fieldname is used
# Create unique index on autoname field automatically.
@ -634,13 +640,15 @@ class DocType(Document):
if not name:
name = self.name
flags = {"flags": re.ASCII} if six.PY3 else {}
# a DocType name should not start or end with an empty space
if re.match("^[ \t\n\r]+|[ \t\n\r]+$", name, **flags):
frappe.throw(_("DocType's name should not start or end with whitespace"), frappe.NameError)
# a DocType's name should not start with a number or underscore
# and should only contain letters, numbers and underscore
if six.PY2:
is_a_valid_name = re.match("^(?![\W])[^\d_\s][\w ]+$", name)
else:
is_a_valid_name = re.match("^(?![\W])[^\d_\s][\w ]+$", name, flags = re.ASCII)
if not is_a_valid_name:
if not re.match("^(?![\W])[^\d_\s][\w ]+$", name, **flags):
frappe.throw(_("DocType's name should start with a letter and it can only consist of letters, numbers, spaces and underscores"), frappe.NameError)

View file

@ -0,0 +1,23 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Document Naming Rule', {
refresh: function(frm) {
frm.trigger('document_type');
},
document_type: (frm) => {
// update the select field options with fieldnames
if (frm.doc.document_type) {
frappe.model.with_doctype(frm.doc.document_type, () => {
let fieldnames = frappe.get_meta(frm.doc.document_type).fields
.filter((d) => {
return frappe.model.no_value_type.indexOf(d.fieldtype) === -1;
}).map((d) => {
return {label: `${d.label} (${d.fieldname})`, value: d.fieldname};
});
frappe.meta.get_docfield('Document Naming Rule Condition', 'field', frm.doc.name).options = fieldnames;
frm.refresh_field('conditions');
});
}
}
});

View file

@ -0,0 +1,104 @@
{
"actions": [],
"creation": "2020-09-07 12:48:48.334318",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"document_type",
"disabled",
"priority",
"section_break_3",
"conditions",
"naming_section",
"prefix",
"prefix_digits",
"counter"
],
"fields": [
{
"fieldname": "document_type",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Document Type",
"options": "DocType"
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
},
{
"fieldname": "prefix",
"fieldtype": "Data",
"label": "Prefix",
"mandatory_depends_on": "eval:doc.naming_by===\"Numbered\""
},
{
"fieldname": "counter",
"fieldtype": "Int",
"label": "Counter",
"read_only": 1
},
{
"default": "5",
"description": "Example: 00001",
"fieldname": "prefix_digits",
"fieldtype": "Int",
"label": "Digits",
"mandatory_depends_on": "eval:doc.naming_by===\"Numbered\""
},
{
"fieldname": "naming_section",
"fieldtype": "Section Break",
"label": "Naming"
},
{
"collapsible": 1,
"collapsible_depends_on": "conditions",
"fieldname": "section_break_3",
"fieldtype": "Section Break",
"label": "Rule Conditions"
},
{
"fieldname": "conditions",
"fieldtype": "Table",
"label": "Conditions",
"options": "Document Naming Rule Condition"
},
{
"description": "Rules with higher priority will be applied first.",
"fieldname": "priority",
"fieldtype": "Int",
"label": "Priority"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-09-21 10:23:34.401539",
"modified_by": "Administrator",
"module": "Core",
"name": "Document Naming Rule",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "document_type",
"track_changes": 1
}

View file

@ -0,0 +1,21 @@
# -*- 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
from frappe.utils.data import evaluate_filters
class DocumentNamingRule(Document):
def apply(self, doc):
'''
Apply naming rules for the given document. Will set `name` if the rule is matched.
'''
if self.conditions:
if not evaluate_filters(doc, [(d.field, d.condition, d.value) for d in self.conditions]):
return
counter = frappe.db.get_value(self.doctype, self.name, 'counter', for_update=True) or 0
doc.name = self.prefix + ('%0'+str(self.prefix_digits)+'d') % (counter + 1)
frappe.db.set_value(self.doctype, self.name, 'counter', counter + 1)

View file

@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
class TestDocumentNamingRule(unittest.TestCase):
def test_naming_rule_by_series(self):
naming_rule = frappe.get_doc(dict(
doctype = 'Document Naming Rule',
document_type = 'ToDo',
prefix = 'test-todo-',
prefix_digits = 5
)).insert()
todo = frappe.get_doc(dict(
doctype = 'ToDo',
description = 'Is this my name ' + frappe.generate_hash()
)).insert()
self.assertEqual(todo.name, 'test-todo-00001')
naming_rule.delete()
todo.delete()
def test_naming_rule_by_condition(self):
naming_rule = frappe.get_doc(dict(
doctype = 'Document Naming Rule',
document_type = 'ToDo',
prefix = 'test-high-',
prefix_digits = 5,
priority = 10,
conditions = [dict(
field = 'priority',
condition = '=',
value = 'High'
)]
)).insert()
# another rule
naming_rule_1 = frappe.copy_doc(naming_rule)
naming_rule_1.prefix = 'test-medium-'
naming_rule_1.conditions[0].value = 'Medium'
naming_rule_1.insert()
# default rule with low priority - should not get applied for rules
# with higher priority
naming_rule_2 = frappe.copy_doc(naming_rule)
naming_rule_2.prefix = 'test-low-'
naming_rule_2.priority = 0
naming_rule_2.conditions = []
naming_rule_2.insert()
todo = frappe.get_doc(dict(
doctype = 'ToDo',
priority = 'High',
description = 'Is this my name ' + frappe.generate_hash()
)).insert()
todo_1 = frappe.get_doc(dict(
doctype = 'ToDo',
priority = 'Medium',
description = 'Is this my name ' + frappe.generate_hash()
)).insert()
todo_2 = frappe.get_doc(dict(
doctype = 'ToDo',
priority = 'Low',
description = 'Is this my name ' + frappe.generate_hash()
)).insert()
try:
self.assertEqual(todo.name, 'test-high-00001')
self.assertEqual(todo_1.name, 'test-medium-00001')
self.assertEqual(todo_2.name, 'test-low-00001')
finally:
naming_rule.delete()
naming_rule_1.delete()
naming_rule_2.delete()
todo.delete()
todo_1.delete()
todo_2.delete()

View file

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

View file

@ -0,0 +1,49 @@
{
"actions": [],
"creation": "2020-09-08 10:17:54.366279",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"field",
"condition",
"value"
],
"fields": [
{
"fieldname": "field",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Field",
"reqd": 1
},
{
"fieldname": "condition",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Condition",
"options": "=\n!=\n>\n<\n>=\n<=",
"reqd": 1
},
{
"fieldname": "value",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Value",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-09-08 10:19:56.192949",
"modified_by": "Administrator",
"module": "Core",
"name": "Document Naming Rule Condition",
"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 DocumentNamingRuleCondition(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 TestDocumentNamingRuleCondition(unittest.TestCase):
pass

View file

@ -17,11 +17,11 @@
"unique": 1
}
],
"modified": "2019-06-30 13:24:13.732202",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Core",
"name": "Domain",
"owner": "makarand@erpnext.com",
"owner": "Administrator",
"permissions": [
{
"create": 1,

View file

@ -54,12 +54,12 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2017-05-04 11:05:54.750351",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Core",
"name": "Has Domain",
"name_case": "",
"owner": "makarand@erpnext.com",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,

View file

@ -31,7 +31,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Fieldtype",
"options": "Check\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nInt\nLink\nSelect\nTime",
"options": "Check\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nInt\nLink\nSelect\nTime",
"reqd": 1
},
{
@ -48,7 +48,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-08-17 14:32:17.174796",
"modified": "2020-09-03 10:52:03.895817",
"modified_by": "Administrator",
"module": "Core",
"name": "Report Column",

View file

@ -186,8 +186,8 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-07-28 15:49:54.019073",
"modified_by": "cave@aperture.com",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Data Migration",
"name": "Data Migration Plan",
"name_case": "",

View file

@ -800,12 +800,12 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-07-30 07:02:26.980372",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Data Migration",
"name": "Data Migration Run",
"name_case": "",
"owner": "faris@erpnext.com",
"owner": "Administrator",
"permissions": [
{
"amend": 0,

View file

@ -53,11 +53,11 @@
}
],
"links": [],
"modified": "2020-06-15 11:24:57.639430",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Desk",
"name": "Calendar View",
"owner": "faris@erpnext.com",
"owner": "Administrator",
"permissions": [
{
"create": 1,

View file

@ -120,8 +120,8 @@
"hide_toolbar": 1,
"in_create": 1,
"links": [],
"modified": "2020-05-31 22:31:12.886950",
"modified_by": "umair@erpnext.com",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Desk",
"name": "Notification Log",
"owner": "Administrator",

View file

@ -169,16 +169,14 @@ def get_comments(doctype, doc_name, frequency, user):
return timeline
def is_document_followed(doctype, doc_name, user):
docs = frappe.get_all(
return frappe.db.exists(
"Document Follow",
filters={
{
"ref_doctype": doctype,
"ref_docname": doc_name,
"user": user
},
limit=1
}
)
return len(docs)
@frappe.whitelist()
def get_follow_users(doctype, doc_name):

View file

@ -23,6 +23,8 @@ def savedocs(doc, action):
# update recent documents
run_onload(doc)
send_updated_docs(doc)
frappe.msgprint(frappe._("Saved"), indicator='green', alert=True)
except Exception:
frappe.errprint(frappe.utils.get_traceback())
raise
@ -36,6 +38,7 @@ def cancel(doctype=None, name=None, workflow_state_fieldname=None, workflow_stat
doc.set(workflow_state_fieldname, workflow_state)
doc.cancel()
send_updated_docs(doc)
frappe.msgprint(frappe._("Cancelled"), indicator='red', alert=True)
except Exception:
frappe.errprint(frappe.utils.get_traceback())

View file

@ -8,14 +8,13 @@ import os, json
from frappe import _
from frappe.modules import scrub, get_module_path
from frappe.utils import flt, cint, get_html_format, get_url_to_form
from frappe.utils import flt, cint, get_html_format, get_url_to_form, gzip_decompress, format_duration
from frappe.model.utils import render_include
from frappe.translate import send_translations
import frappe.desk.reportview
from frappe.permissions import get_role_permissions
from six import string_types, iteritems
from datetime import timedelta
from frappe.utils import gzip_decompress
from frappe.core.utils import ljust_list
def get_report_doc(report_name):
@ -67,7 +66,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
# Reordered columns
columns = json.loads(report.custom_columns)
result = reorder_data_for_custom_columns(columns, query_columns, result, report.report_type)
result = reorder_data_for_custom_columns(columns, query_columns, result)
result = add_data_to_custom_columns(columns, result)
@ -215,25 +214,19 @@ def add_data_to_custom_columns(columns, result):
return data
def reorder_data_for_custom_columns(custom_columns, columns, result, report_type):
def reorder_data_for_custom_columns(custom_columns, columns, result):
if not result:
return []
if report_type == 'Query Report':
# Assume list result for query reports
# Query report columns exclusively use Label
custom_column_labels = [col["label"] for col in custom_columns]
original_column_labels = [col.split(":")[0] for col in columns]
return get_columns_from_list(custom_column_labels, original_column_labels, result)
custom_column_names = [col["fieldname"] for col in custom_columns]
columns = [get_column_as_dict(col) for col in columns]
if isinstance(result[0], list) or isinstance(result[0], tuple):
# If the result is a list of lists
original_column_names = [col["fieldname"] for col in columns]
custom_column_names = [col["label"] for col in custom_columns]
original_column_names = [col["label"] for col in columns]
return get_columns_from_list(custom_column_names, original_column_names, result)
else:
# If the result is a list of dicts
return get_columns_from_dict(custom_column_names, result)
# columns do not need to be reordered if result is a list of dicts
return result
def get_columns_from_list(columns, target_columns, result):
reordered_result = []
@ -251,21 +244,6 @@ def get_columns_from_list(columns, target_columns, result):
return reordered_result
def get_columns_from_dict(columns, result):
reordered_result = []
for res in result:
r = {}
for col_name in columns:
try:
r[col_name] = res[col_name]
except KeyError:
pass
reordered_result.append(r)
return reordered_result
def get_prepared_report_result(report, filters, dn="", user=None):
latest_report_data = {}
doc = None
@ -360,6 +338,7 @@ def export_query():
columns = get_columns_dict(data.columns)
from frappe.utils.xlsxutils import make_xlsx
data['result'] = handle_duration_fieldtype_values(data.get('result'), data.get('columns'))
xlsx_data = build_xlsx_data(columns, data, visible_idx, include_indentation)
xlsx_file = make_xlsx(xlsx_data, "Query Report")
@ -367,6 +346,29 @@ def export_query():
frappe.response['filecontent'] = xlsx_file.getvalue()
frappe.response['type'] = 'binary'
def handle_duration_fieldtype_values(result, columns):
for i, col in enumerate(columns):
fieldtype = None
if isinstance(col, string_types):
col = col.split(":")
if len(col) > 1:
if col[1]:
fieldtype = col[1]
if "/" in fieldtype:
fieldtype, options = fieldtype.split("/")
else:
fieldtype = "Data"
else:
fieldtype = col.get("fieldtype")
if fieldtype == "Duration":
for entry in range(0, len(result)):
val_in_seconds = result[entry][i]
if val_in_seconds:
duration_val = format_duration(val_in_seconds)
result[entry][i] = duration_val
return result
def build_xlsx_data(columns, data, visible_idx, include_indentation):
result = [[]]
@ -384,12 +386,14 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation):
if isinstance(row, dict) and row:
for idx in range(len(data.columns)):
label = columns[idx]["label"]
fieldname = columns[idx]["fieldname"]
cell_value = row.get(fieldname, row.get(label, ""))
if cint(include_indentation) and 'indent' in row and idx == 0:
cell_value = (' ' * cint(row['indent'])) + cell_value
row_data.append(cell_value)
# check if column is not hidden
if not columns[idx].get("hidden"):
label = columns[idx]["label"]
fieldname = columns[idx]["fieldname"]
cell_value = row.get(fieldname, row.get(label, ""))
if cint(include_indentation) and 'indent' in row and idx == 0:
cell_value = (' ' * cint(row['indent'])) + cell_value
row_data.append(cell_value)
else:
row_data = row
@ -427,7 +431,7 @@ def add_total_row(result, columns, meta = None):
if i >= len(row): continue
cell = row.get(fieldname) if isinstance(row, dict) else row[i]
if fieldtype in ["Currency", "Int", "Float", "Percent"] and flt(cell):
if fieldtype in ["Currency", "Int", "Float", "Percent", "Duration"] and flt(cell):
total_row[i] = flt(total_row[i]) + flt(cell)
if fieldtype == "Percent" and i not in has_percent:
@ -638,31 +642,35 @@ def get_columns_dict(columns):
"""
columns_dict = frappe._dict()
for idx, col in enumerate(columns):
col_dict = frappe._dict()
# string
if isinstance(col, string_types):
col = col.split(":")
if len(col) > 1:
if "/" in col[1]:
col_dict["fieldtype"], col_dict["options"] = col[1].split("/")
else:
col_dict["fieldtype"] = col[1]
col_dict["label"] = col[0]
col_dict["fieldname"] = frappe.scrub(col[0])
# dict
else:
col_dict.update(col)
if "fieldname" not in col_dict:
col_dict["fieldname"] = frappe.scrub(col_dict["label"])
col_dict = get_column_as_dict(col)
columns_dict[idx] = col_dict
columns_dict[col_dict["fieldname"]] = col_dict
return columns_dict
def get_column_as_dict(col):
col_dict = frappe._dict()
# string
if isinstance(col, string_types):
col = col.split(":")
if len(col) > 1:
if "/" in col[1]:
col_dict["fieldtype"], col_dict["options"] = col[1].split("/")
else:
col_dict["fieldtype"] = col[1]
col_dict["label"] = col[0]
col_dict["fieldname"] = frappe.scrub(col[0])
# dict
else:
col_dict.update(col)
if "fieldname" not in col_dict:
col_dict["fieldname"] = frappe.scrub(col_dict["label"])
return col_dict
def get_user_match_filters(doctypes, user):
match_filters = {}

View file

@ -11,7 +11,7 @@ from frappe.model.db_query import DatabaseQuery
from frappe import _
from six import string_types, StringIO
from frappe.core.doctype.access_log.access_log import make_access_log
from frappe.utils import cstr
from frappe.utils import cstr, format_duration
@frappe.whitelist()
@ -167,6 +167,8 @@ def export_query():
for i, row in enumerate(ret):
data.append([i+1] + list(row))
data = handle_duration_fieldtype_values(doctype, data, db_query.fields)
if file_format_type == "CSV":
# convert to csv
@ -236,6 +238,29 @@ def get_labels(fields, doctype):
return labels
def handle_duration_fieldtype_values(doctype, data, fields):
for field in fields:
key = field.split(" as ")[0]
if key.startswith(('count(', 'sum(', 'avg(')): continue
if "." in key:
parenttype, fieldname = key.split(".")[0][4:-1], key.split(".")[1].strip("`")
else:
parenttype = doctype
fieldname = field.strip("`")
df = frappe.get_meta(parenttype).get_field(fieldname)
if df and df.fieldtype == 'Duration':
index = fields.index(field) + 1
for i in range(1, len(data)):
val_in_seconds = data[i][index]
if val_in_seconds:
duration_val = format_duration(val_in_seconds, df.hide_days)
data[i][index] = duration_val
return data
@frappe.whitelist()
def delete_items():
"""delete selected items"""

View file

@ -3,23 +3,7 @@
frappe.ui.form.on('Auto Email Report', {
refresh: function(frm) {
if(frm.doc.report_type !== 'Report Builder') {
if(frm.script_setup_for !== frm.doc.report && !frm.doc.__islocal) {
frappe.call({
method:"frappe.desk.query_report.get_script",
args: {
report_name: frm.doc.report
},
callback: function(r) {
frappe.dom.eval(r.message.script || "");
frm.script_setup_for = frm.doc.report;
frm.trigger('show_filters');
}
});
} else {
frm.trigger('show_filters');
}
}
frm.trigger('fetch_report_filters');
if(!frm.is_new()) {
frm.add_custom_button(__('Download'), function() {
var w = window.open(
@ -50,6 +34,27 @@ frappe.ui.form.on('Auto Email Report', {
},
report: function(frm) {
frm.set_value('filters', '');
frm.trigger('fetch_report_filters');
},
fetch_report_filters(frm) {
if (frm.doc.report
&& frm.doc.report_type !== 'Report Builder'
&& frm.script_setup_for !== frm.doc.report
) {
frappe.call({
method: "frappe.desk.query_report.get_script",
args: {
report_name: frm.doc.report
},
callback: function(r) {
frappe.dom.eval(r.message.script || "");
frm.script_setup_for = frm.doc.report;
frm.trigger('show_filters');
}
});
} else {
frm.trigger('show_filters');
}
},
show_filters: function(frm) {
var wrapper = $(frm.get_field('filters_display').wrapper);

View file

@ -1,181 +1,78 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2019-01-09 16:39:23.746535",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"creation": "2019-01-09 16:39:23.746535",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"ref_doctype",
"ref_docname",
"user"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "ref_doctype",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Doctype",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "ref_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Doctype",
"options": "DocType",
"reqd": 1,
"search_index": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "ref_docname",
"fieldtype": "Dynamic Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Document Name",
"length": 0,
"no_copy": 0,
"options": "ref_doctype",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "ref_docname",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Document Name",
"options": "ref_doctype",
"reqd": 1,
"search_index": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "user",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "User",
"length": 0,
"no_copy": 0,
"options": "User",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldname": "user",
"fieldtype": "Link",
"in_list_view": 1,
"label": "User",
"options": "User",
"reqd": 1,
"search_index": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 1,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-02-26 15:43:44.330348",
"modified_by": "Administrator",
"module": "Email",
"name": "Document Follow",
"name_case": "",
"owner": "Administrator",
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-09-17 09:19:28.496453",
"modified_by": "Administrator",
"module": "Email",
"name": "Document Follow",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1,
"write": 1
}
],
"quick_entry": 0,
"read_only": 1,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0,
"track_views": 0
],
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC"
}

View file

@ -173,8 +173,13 @@ def get_context(context):
subject = frappe.render_template(self.subject, context)
attachments = self.get_attachment(doc)
recipients, cc, bcc = self.get_list_of_recipients(doc, context)
users = recipients + cc + bcc
if not users:
return
notification_doc = {
'type': 'Alert',
@ -280,8 +285,6 @@ def get_context(context):
if self.send_to_all_assignees:
recipients = recipients + get_assignees(doc)
if not recipients and not cc and not bcc:
return None, None, None
return list(set(recipients)), list(set(cc)), list(set(bcc))
def get_receiver_list(self, doc, context):

View file

@ -43,6 +43,11 @@ app_include_css = [
"assets/css/report.min.css",
]
doctype_js = {
"Web Page": "public/js/frappe/utils/web_template.js",
"Website Settings": "public/js/frappe/utils/web_template.js"
}
web_include_js = [
"website_script.js"
]

View file

@ -1,29 +1,17 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
# called from wnf.py
# lib/wnf.py --install [rootpassword] [dbname] [source]
import json
import os
from __future__ import unicode_literals, print_function
from six.moves import input
import os, json, subprocess, shutil
import click
import frappe
import frappe.database
import importlib
from frappe import _
from frappe.model.sync import sync_for
from frappe.utils.fixtures import sync_fixtures
from frappe.website import render
from frappe.modules.utils import sync_customizations
from frappe.database import setup_database
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
def install_db(root_login="root", root_password=None, db_name=None, source_sql=None,
admin_password=None, verbose=True, force=0, site_config=None, reinstall=False,
db_password=None, db_type=None, db_host=None, db_port=None, no_mariadb_socket=False):
import frappe.database
from frappe.database import setup_database
if not db_type:
db_type = frappe.conf.db_type or 'mariadb'
@ -45,7 +33,13 @@ def install_db(root_login="root", root_password=None, db_name=None, source_sql=N
frappe.flags.in_install_db = False
def install_app(name, verbose=False, set_as_patched=True):
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
from frappe.utils.fixtures import sync_fixtures
from frappe.model.sync import sync_for
from frappe.modules.utils import sync_customizations
frappe.flags.in_install = name
frappe.flags.ignore_in_install = False
@ -65,7 +59,7 @@ def install_app(name, verbose=False, set_as_patched=True):
raise Exception("App not in apps.txt")
if name in installed_apps:
frappe.msgprint(_("App {0} already installed").format(name))
frappe.msgprint(frappe._("App {0} already installed").format(name))
return
print("\nInstalling {0}...".format(name))
@ -102,25 +96,31 @@ def install_app(name, verbose=False, set_as_patched=True):
frappe.flags.in_install = False
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()
post_install(rebuild_website)
if frappe.flags.in_install:
post_install(rebuild_website)
def remove_from_installed_apps(app_name):
installed_apps = frappe.get_installed_apps()
if app_name in installed_apps:
installed_apps.remove(app_name)
frappe.db.set_value("DefaultValue", {"defkey": "installed_apps"}, "defvalue", json.dumps(installed_apps))
frappe.db.set_global("installed_apps", json.dumps(installed_apps))
frappe.get_single("Installed Applications").update_versions()
frappe.db.commit()
if frappe.flags.in_install:
post_install()
def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False):
"""Remove app and all linked to the app's module with the app from a site."""
import click
# dont allow uninstall app if not installed unless forced
if not force:
@ -143,11 +143,12 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
frappe.flags.in_uninstall = True
drop_doctypes = []
# remove modules, doctypes, roles
for module_name in frappe.get_module_list(app_name):
for doctype in frappe.get_list("DocType", filters={"module": module_name},
fields=["name", "issingle"]):
print("removing DocType {0}...".format(doctype.name))
modules = (x.name for x in frappe.get_all("Module Def", filters={"app_name": app_name}))
for module_name in modules:
print("Deleting Module '{0}'".format(module_name))
for doctype in frappe.get_list("DocType", filters={"module": module_name}, fields=["name", "issingle"]):
print("* removing DocType '{0}'...".format(doctype.name))
if not dry_run:
frappe.delete_doc("DocType", doctype.name)
@ -155,35 +156,36 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
if not doctype.issingle:
drop_doctypes.append(doctype.name)
linked_doctypes = frappe.get_all("DocField", filters={"fieldtype": "Link", "options": "Module Def"}, fields=['parent'])
ordered_doctypes = ["Desk Page", "Report", "Page", "Web Form"]
doctypes_with_linked_modules = ordered_doctypes + [doctype.parent for doctype in linked_doctypes if doctype.parent not in ordered_doctypes]
for doctype in doctypes_with_linked_modules:
for record in frappe.get_list(doctype, filters={"module": module_name}):
print("removing {0} {1}...".format(doctype, record.name))
print("* removing {0} '{1}'...".format(doctype, record.name))
if not dry_run:
frappe.delete_doc(doctype, record.name)
print("removing Module {0}...".format(module_name))
print("* removing Module Def '{0}'...".format(module_name))
if not dry_run:
frappe.delete_doc("Module Def", module_name)
remove_from_installed_apps(app_name)
if not dry_run:
# drop tables after a commit
frappe.db.commit()
remove_from_installed_apps(app_name)
for doctype in set(drop_doctypes):
print("* dropping Table for '{0}'...".format(doctype))
frappe.db.sql("drop table `tab{0}`".format(doctype))
frappe.db.commit()
click.secho("Uninstalled App {0} from Site {1}".format(app_name, frappe.local.site), fg="green")
frappe.flags.in_uninstall = False
def post_install(rebuild_website=False):
from frappe.website import render
if rebuild_website:
render.clear_cache()
@ -191,6 +193,7 @@ def post_install(rebuild_website=False):
frappe.db.commit()
frappe.clear_cache()
def set_all_patches_as_completed(app):
patch_path = os.path.join(frappe.get_pymodule_path(app), "patches.txt")
if os.path.exists(patch_path):
@ -201,6 +204,7 @@ def set_all_patches_as_completed(app):
}).insert(ignore_permissions=True)
frappe.db.commit()
def init_singles():
singles = [single['name'] for single in frappe.get_all("DocType", filters={'issingle': True})]
for single in singles:
@ -210,6 +214,7 @@ def init_singles():
doc.flags.ignore_validate=True
doc.save()
def make_conf(db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None):
site = frappe.local.site
make_site_config(db_name, db_password, site_config, db_type=db_type, db_host=db_host, db_port=db_port)
@ -217,6 +222,7 @@ def make_conf(db_name=None, db_password=None, site_config=None, db_type=None, db
frappe.destroy()
frappe.init(site, sites_path=sites_path)
def make_site_config(db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None):
frappe.create_folder(os.path.join(frappe.local.site_path))
site_file = get_site_config_path()
@ -237,6 +243,7 @@ def make_site_config(db_name=None, db_password=None, site_config=None, db_type=N
with open(site_file, "w") as f:
f.write(json.dumps(site_config, indent=1, sort_keys=True))
def update_site_config(key, value, validate=True, site_config_path=None):
"""Update a value in site_config"""
if not site_config_path:
@ -266,9 +273,11 @@ def update_site_config(key, value, validate=True, site_config_path=None):
if hasattr(frappe.local, "conf"):
frappe.local.conf[key] = value
def get_site_config_path():
return os.path.join(frappe.local.site_path, "site_config.json")
def get_conf_params(db_name=None, db_password=None):
if not db_name:
db_name = input("Database Name: ")
@ -281,6 +290,7 @@ def get_conf_params(db_name=None, db_password=None):
return {"db_name": db_name, "db_password": db_password}
def make_site_dirs():
site_public_path = os.path.join(frappe.local.site_path, 'public')
site_private_path = os.path.join(frappe.local.site_path, 'private')
@ -296,6 +306,7 @@ def make_site_dirs():
if not os.path.exists(locks_dir):
os.makedirs(locks_dir)
def add_module_defs(app):
modules = frappe.get_module_list(app)
for module in modules:
@ -304,7 +315,10 @@ def add_module_defs(app):
d.module_name = module
d.save(ignore_permissions=True)
def remove_missing_apps():
import importlib
apps = ('frappe_subscription', 'shopping_cart')
installed_apps = json.loads(frappe.db.get_global("installed_apps") or "[]")
for app in apps:
@ -316,7 +330,10 @@ def remove_missing_apps():
installed_apps.remove(app)
frappe.db.set_global("installed_apps", json.dumps(installed_apps))
def extract_sql_gzip(sql_gz_path):
import subprocess
try:
# dvf - decompress, verbose, force
original_file = sql_gz_path
@ -328,7 +345,11 @@ def extract_sql_gzip(sql_gz_path):
return decompressed_file
def extract_tar_files(site_name, file_path, folder_name):
import subprocess
import shutil
# Need to do frappe.init to maintain the site locals
frappe.init(site=site_name)
abs_site_path = os.path.abspath(frappe.get_site_path())
@ -349,6 +370,7 @@ def extract_tar_files(site_name, file_path, folder_name):
return tar_path
def is_downgrade(sql_file_path, verbose=False):
"""checks if input db backup will get downgraded on current bench"""
from semantic_version import Version

View file

@ -97,7 +97,7 @@ def backup_to_dropbox(upload_db_backup=True):
if frappe.flags.create_new_backup:
backup = new_backup(ignore_files=True)
filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db))
site_config = os.path.join(get_backups_path(), os.path.basename(backup.site_config_backup_path))
site_config = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_conf))
else:
filename, site_config = get_latest_backup_file()

View file

@ -97,8 +97,8 @@
"label": "Push to Google Contacts"
}
],
"modified": "2019-09-13 15:53:19.569924",
"modified_by": "himanshu@erpnext.com",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Google Contacts",
"owner": "Administrator",

View file

@ -100,8 +100,8 @@
}
],
"issingle": 1,
"modified": "2019-08-21 17:33:28.516614",
"modified_by": "qwe@qwe.com",
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Google Drive",
"owner": "Administrator",

View file

@ -191,7 +191,7 @@ def upload_system_backup_to_google_drive():
backup = new_backup()
file_urls = []
file_urls.append(backup.backup_path_db)
file_urls.append(backup.site_config_backup_path)
file_urls.append(backup.backup_path_conf)
if account.file_backup:
file_urls.append(backup.backup_path_files)

View file

@ -118,7 +118,7 @@ def backup_to_s3():
backup = new_backup(ignore_files=False, backup_path_db=None,
backup_path_files=None, backup_path_private_files=None, force=True)
db_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db))
site_config = os.path.join(get_backups_path(), os.path.basename(backup.site_config_backup_path))
site_config = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_conf))
if backup_files:
files_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_files))
private_files = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_private_files))

View file

@ -23,7 +23,7 @@ from frappe.search.website_search import build_index_for_all_routes
def migrate(verbose=True, rebuild_website=False, skip_failing=False, skip_search_index=False):
'''Migrate all apps to the latest version, will:
'''Migrate all apps to the current version, will:
- run before migrate hooks
- run patches
- sync doctypes (schema)

View file

@ -335,6 +335,9 @@ class BaseDocument(object):
if frappe.db.is_primary_key_violation(e):
if self.meta.autoname=="hash":
# hash collision? try again
frappe.flags.retry_count = (frappe.flags.retry_count or 0) + 1
if frappe.flags.retry_count > 5:
raise
self.name = None
self.db_insert()
return

View file

@ -7,6 +7,7 @@ from frappe import _
from frappe.utils import now_datetime, cint, cstr
import re
from six import string_types
from frappe.model import log_types
def set_new_name(doc):
@ -35,7 +36,13 @@ def set_new_name(doc):
elif getattr(doc.meta, "issingle", False):
doc.name = doc.doctype
else:
elif getattr(doc.meta, "istable", False):
doc.name = make_autoname("hash", doc.doctype)
if not doc.name:
set_naming_from_document_naming_rule(doc)
if not doc.name:
doc.run_method("autoname")
if not doc.name and autoname:
@ -43,12 +50,15 @@ def set_new_name(doc):
# if the autoname option is 'field:' and no name was derived, we need to
# notify
if autoname.startswith("field:") and not doc.name:
if not doc.name and autoname.startswith("field:"):
fieldname = autoname[6:]
frappe.throw(_("{0} is required").format(doc.meta.get_label(fieldname)))
# at this point, we fall back to name generation with the hash option
if not doc.name or autoname == "hash":
if not doc.name and autoname == "hash":
doc.name = make_autoname("hash", doc.doctype)
if not doc.name:
doc.name = make_autoname("hash", doc.doctype)
doc.name = validate_name(
@ -76,6 +86,23 @@ def set_name_from_naming_options(autoname, doc):
elif "#" in autoname:
doc.name = make_autoname(autoname, doc=doc)
def set_naming_from_document_naming_rule(doc):
'''
Evaluate rules based on "Document Naming Series" doctype
'''
if doc.doctype in log_types:
return
try:
for d in frappe.get_all('Document Naming Rule',
dict(document_type=doc.doctype, disabled=0), order_by='priority desc'):
frappe.get_cached_doc('Document Naming Rule', d.name).apply(doc)
if doc.name:
break
except frappe.db.TableMissingError: # noqa: E722
# not yet bootstrapped
pass
def set_name_by_naming_series(doc):
"""Sets name by the `naming_series` property"""
if not doc.naming_series:

View file

@ -306,7 +306,10 @@ frappe.patches.v13_0.add_toggle_width_in_navbar_settings
frappe.patches.v13_0.rename_notification_fields
frappe.patches.v13_0.remove_duplicate_navbar_items
frappe.patches.v12_0.set_default_password_reset_limit
execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True)
frappe.patches.v13_0.set_route_for_blog_category
frappe.patches.v13_0.enable_custom_script
frappe.patches.v13_0.update_newsletter_content_type
frappe.patches.v13_0.delete_event_producer_and_consumer_keys
execute:frappe.db.set_value('Website Settings', 'Website Settings', {'navbar_template': 'Standard Navbar', 'footer_template': 'Standard Footer'})
frappe.patches.v13_0.delete_event_producer_and_consumer_keys
frappe.patches.v13_0.web_template_set_module

View file

@ -0,0 +1,15 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
"""Set default module for standard Web Template, if none."""
frappe.reload_doc('website', 'doctype', 'Web Template')
standard_templates = frappe.get_list('Web Template', {'standard': 1})
for template in standard_templates:
doc = frappe.get_doc('Web Template', template.name)
if not doc.module:
doc.module = 'Website'
doc.save()

View file

@ -243,6 +243,7 @@
"public/js/frappe/utils/energy_point_utils.js",
"public/js/frappe/utils/dashboard_utils.js",
"public/js/frappe/ui/chart.js",
"public/js/frappe/ui/datatable.js",
"public/js/frappe/ui/driver.js",
"public/js/frappe/barcode_scanner/index.js"
],

View file

@ -34,7 +34,7 @@ frappe.dom = {
},
remove_script_and_style: function(txt) {
const evil_tags = ["script", "style", "noscript", "title", "meta", "base", "head"];
const regex = new RegExp(evil_tags.map(tag => `<${tag}>.*<\\/${tag}>`).join('|'));
const regex = new RegExp(evil_tags.map(tag => `<${tag}>.*<\\/${tag}>`).join('|'), 's');
if (!regex.test(txt)) {
// no evil tags found, skip the DOM method entirely!
return txt;

View file

@ -44,5 +44,9 @@ frappe.ui.form.ControlMarkdownEditor = frappe.ui.form.ControlCode.extend({
.then(() => {
this.update_preview();
});
},
set_disp_area(value) {
this.disp_area && $(this.disp_area).text(value);
}
});

View file

@ -1265,7 +1265,7 @@ frappe.ui.form.Form = class FrappeForm {
set_df_property(fieldname, property, value, docname, table_field) {
var df;
if (!docname && !table_field){
if (!docname && !table_field) {
df = this.get_docfield(fieldname);
} else {
var grid = this.fields_dict[table_field].grid,
@ -1273,7 +1273,7 @@ frappe.ui.form.Form = class FrappeForm {
if (fname && fname.length)
df = frappe.meta.get_docfield(fname[0].parent, fieldname, docname);
}
if(df && df[property] != value) {
if (df && df[property] != value) {
df[property] = value;
refresh_field(fieldname, table_field);
}

View file

@ -770,6 +770,10 @@ export default class Grid {
as_dataurl: true,
allow_multiple: false,
on_success(file) {
if (file.file_obj.type !== "text/csv") {
let msg = __(`Your file could not be processed. It should be a standard CSV file.`);
frappe.throw(msg);
}
var data = frappe.utils.csv_to_array(frappe.utils.get_decoded_string(file.dataurl));
// row #2 contains fieldnames;
var fieldnames = data[2];

View file

@ -393,11 +393,16 @@ export default class GridRow {
// sync get_query
field.get_query = this.grid.get_field(df.fieldname).get_query;
var field_on_change_function = field.df.onchange;
field.df.onchange = function(e) {
field_on_change_function && field_on_change_function(e);
me.grid.grid_rows[this.doc.idx - 1].refresh_field(field.df.fieldname);
};
if (!field.df.onchange_modified) {
var field_on_change_function = field.df.onchange;
field.df.onchange = function(e) {
field_on_change_function && field_on_change_function(e);
me.grid.grid_rows[this.doc.idx - 1].refresh_field(this.df.fieldname);
};
field.df.onchange_modified = true;
}
field.refresh();
if(field.$input) {
field.$input

View file

@ -215,10 +215,6 @@ frappe.ui.form.save = function (frm, action, callback, btn) {
$(btn).prop("disabled", false);
frappe.ui.form.is_saving = false;
if (!r.exc) {
frappe.show_alert({message: __('Saved'), indicator: 'green'});
}
if (r) {
var doc = r.docs && r.docs[0];
if (doc) {

View file

@ -186,7 +186,7 @@ frappe.ui.form.Toolbar = Class.extend({
},
set_indicator: function() {
var indicator = frappe.get_indicator(this.frm.doc);
if (this.frm.save_disabled && [__('Saved'), __('Not Saved')].includes(indicator[0])) {
if (this.frm.save_disabled && indicator && [__('Saved'), __('Not Saved')].includes(indicator[0])) {
return;
}
if(indicator) {
@ -272,12 +272,12 @@ frappe.ui.form.Toolbar = Class.extend({
});
}
if (frappe.user_roles.includes("System Manager") && me.frm.meta.issingle === 0) {
if (frappe.user_roles.includes("System Manager")) {
let is_doctype_form = me.frm.doctype === 'DocType';
let doctype = is_doctype_form ? me.frm.docname : me.frm.doctype;
let is_doctype_custom = is_doctype_form ? me.frm.doc.custom : false;
if (doctype != 'DocType' && !is_doctype_custom) {
if (doctype != 'DocType' && !is_doctype_custom && me.frm.meta.issingle === 0) {
this.page.add_menu_item(__("Customize"), function() {
if (me.frm.meta && me.frm.meta.custom) {
frappe.set_route('Form', 'DocType', doctype);

View file

@ -31,7 +31,7 @@ $.extend(frappe.model, {
{fieldname:'docstatus', fieldtype:'Int', label:__('Document Status')},
],
numeric_fieldtypes: ["Int", "Float", "Currency", "Percent"],
numeric_fieldtypes: ["Int", "Float", "Currency", "Percent", "Duration"],
std_fields_table: [
{fieldname:'parent', fieldtype:'Data', label:__('Parent')},

View file

@ -1,6 +1,6 @@
frappe.provide('frappe.route');
frappe.route_history_queue = [];
const routes_to_skip = ['Form', 'social', 'setup-wizard'];
const routes_to_skip = ['Form', 'social', 'setup-wizard', 'recorder'];
const save_routes = frappe.utils.debounce(() => {
const routes = frappe.route_history_queue;
@ -30,7 +30,6 @@ function is_route_useful(route) {
if (!route[1]) {
return false;
} else if ((route[0] === 'List' && !route[2]) || routes_to_skip.includes(route[0])) {
return false;
} else {
return true;

View file

@ -0,0 +1,3 @@
import DataTable from "frappe-datatable";
frappe.DataTable = DataTable;

View file

@ -824,8 +824,14 @@ Object.assign(frappe.utils, {
};
},
get_formatted_duration(value, duration_options) {
get_formatted_duration(value, duration_options=null) {
let duration = '';
if (!duration_options) {
duration_options = {
hide_days: 0,
hide_seconds: 0
};
}
if (value) {
let total_duration = frappe.utils.seconds_to_duration(value, duration_options);

View file

@ -0,0 +1,71 @@
function open_web_template_values_editor(template, current_values = {}) {
return new Promise(resolve => {
frappe.model.with_doc("Web Template", template).then((doc) => {
let d = new frappe.ui.Dialog({
title: __("Edit Values"),
fields: get_fields(doc),
primary_action(values) {
d.hide();
resolve(values);
},
});
d.set_values(current_values);
d.show();
d.sections.forEach((sect) => {
let fields_with_value = sect.fields_list.filter(
(field) => current_values[field.df.fieldname]
);
if (fields_with_value.length) {
sect.collapse(false);
}
});
});
});
function get_fields(doc) {
let normal_fields = [];
let table_fields = [];
let current_table = null;
for (let df of doc.fields) {
if (current_table) {
current_table.fields = current_table.fields || [];
if (df.fieldtype != 'Table Break') {
current_table.fields.push(df);
} else {
table_fields.push(df);
current_table = df;
}
} else if (df.fieldtype != 'Table Break') {
normal_fields.push(df);
} else {
table_fields.push(df);
current_table = df;
}
}
let fields = [
...normal_fields,
...table_fields.map(tf => {
let data = current_values[tf.fieldname] || [];
return {
label: tf.label,
fieldname: tf.fieldname,
fieldtype: 'Table',
fields: tf.fields.map((df, i) => ({
...df,
in_list_view: i <= 1,
columns: tf.fields.length == 1 ? 10 : 5
})),
data,
get_data: () => data
};
})
];
return fields;
}
}

View file

@ -1327,6 +1327,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
return row
.slice(standard_column_count)
.map((cell, i) => {
if (cell.column.fieldtype === "Duration") {
cell.content = frappe.utils.get_formatted_duration(cell.content);
}
if (include_indentation && i===0) {
cell.content = ' '.repeat(row.meta.indent) + (cell.content || '');
}

View file

@ -89,11 +89,10 @@ export default class ShortcutWidget extends Widget {
const label = get_label();
const buttons = $(`<div class="small pill">${label}</div>`);
if (this.color) {
buttons.css("background-color", this.color);
buttons.css(
"color",
frappe.ui.color.get_contrast_color(this.color)
);
let bg_color = count ? this.color: '#EEEEEE';
let text_color = count ? frappe.ui.color.get_contrast_color(bg_color): '#8D99A6';
buttons.css("background-color", bg_color);
buttons.css("color", text_color);
}
buttons.appendTo(this.action_area);

View file

@ -33,7 +33,7 @@ h1 {
h2 {
font-size: $font-size-xl;
font-weight: bold;
font-weight: 700;
margin-bottom: 0.75rem;
@include media-breakpoint-up(sm) {
@ -44,3 +44,15 @@ h2 {
}
}
h3 {
font-size: $font-size-base;
font-weight: 600;
margin-bottom: 0.5rem;
@include media-breakpoint-up(sm) {
font-size: $font-size-lg;
}
@include media-breakpoint-up(md) {
font-size: $font-size-xl;
}
}

View file

@ -0,0 +1,28 @@
:root {
--gray-50: #{$gray-50};
--gray-100: #{$gray-100};
--gray-200: #{$gray-200};
--gray-300: #{$gray-300};
--gray-400: #{$gray-400};
--gray-500: #{$gray-500};
--gray-600: #{$gray-600};
--gray-700: #{$gray-700};
--gray-800: #{$gray-800};
--gray-900: #{$gray-900};
--black: #{$black};
--primary: #{$primary};
--primary-light: #{$primary-light};
--light: #{$light};
--font-size-xs: #{$font-size-xs};
--font-size-sm: #{$font-size-sm};
--font-size-base: #{$font-size-base};
--font-size-lg: #{$font-size-lg};
--font-size-xl: #{$font-size-xl};
--font-size-2xl: #{$font-size-2xl};
--font-size-3xl: #{$font-size-3xl};
--font-size-4xl: #{$font-size-4xl};
--font-size-5xl: #{$font-size-5xl};
--font-size-6xl: #{$font-size-6xl};
}

View file

@ -0,0 +1,82 @@
.web-footer {
padding: 5rem 0;
min-height: 140px;
}
.footer-logo {
min-width: 5rem;
height: 1.5rem;
object-fit: contain;
object-position: left;
}
.footer-child-item {
margin-top: 0.5rem;
}
.footer-link, .footer-child-item a {
font-size: $font-size-sm;
font-weight: 500;
color: $gray-700;
&:hover {
color: $primary;
text-decoration: none;
}
}
.footer-col-right {
@include media-breakpoint-up(sm) {
text-align: right;
}
}
.footer-col-left, .footer-col-right {
padding-top: 0.8rem;
padding-bottom: 1rem;
line-height: 2;
&:empty {
padding: 0;
}
}
.footer-col-left .footer-link {
margin-right: 1rem;
}
.footer-col-right .footer-link {
margin-right: 1rem;
@include media-breakpoint-up(sm) {
margin-right: 0;
margin-left: 1rem;
}
}
.footer-group {
margin-top: 2rem;
}
.footer-group-label {
color: $text-muted;
font-size: $font-size-sm;
margin-bottom: 0.5rem;
}
.footer-grouped-links {
margin-bottom: 2rem;
}
.footer-group-links {
display: flex;
flex-direction: column;
flex-wrap: wrap;
max-height: 10rem;
margin-bottom: 0;
}
.footer-info {
border-top: 1px solid $border-color;
color: $text-muted;
font-size: $font-size-sm;
}

View file

@ -10,6 +10,10 @@
margin-top: 0;
}
> :last-child {
margin-bottom: 0;
}
ul,
ol {
padding-left: 2.5rem;

View file

@ -0,0 +1,59 @@
.navbar-light {
border-bottom: 1px solid $border-color;
}
.navbar-brand {
img {
display: inline-block;
max-width: 150px;
max-height: 25px;
}
}
.navbar-cta {
@include media-breakpoint-up(lg) {
margin-left: 1rem;
}
}
.navbar.bg-dark {
.dropdown-menu {
font-size: 0.75rem;
background-color: $dark;
border-radius: 0;
}
.nav-link {
white-space: nowrap;
color: $light;
&:hover {
color: $primary;
}
}
.nav-item {
padding: 0rem 1rem;
}
.dropdown-item {
color: $light;
&:hover {
background-color: $dark;
color: $primary;
}
}
}
.navbar-light .navbar-nav .nav-link {
color: $gray-700;
font-size: $font-size-sm;
font-weight: 500;
&:hover,
&:focus, &.active {
color: $primary;
}
}

View file

@ -1,6 +1,6 @@
.hero-content {
.btn-primary {
margin-top: 1rem;
margin-top: 1rem;
margin-right: 0.5rem;
@include media-breakpoint-up(lg) {
@ -13,11 +13,14 @@
}
}
.hero-title, .hero-subtitle {
max-width: 42rem;
}
.hero-subtitle {
@extend .lead;
font-weight: 400;
color: $gray-600;
max-width: 42rem;
font-size: 1rem;
@include media-breakpoint-up(sm) {
@ -25,6 +28,17 @@
}
}
.hero.align-center {
h1, .hero-subtitle, .hero-buttons {
text-align: center;
}
.hero-subtitle {
margin-left: auto;
margin-right: auto;
}
}
.section-description {
max-width: 56rem;
margin-top: 0.5rem;
@ -35,6 +49,15 @@
}
}
.section-with-image.align-center {
text-align: center;
.section-description, .section-image {
margin-left: auto;
margin-right: auto;
}
}
.section-image {
margin-top: 2rem;
border-radius: 0.75rem;
@ -77,17 +100,29 @@
}
}
.section[data-section-template="Hero with Right Image"] {
overflow-x: hidden;
}
.hero-with-right-image {
position: relative;
display: flex;
flex-wrap: nowrap;
.hero-content {
display: flex;
align-items: center;
flex: 0 0 100%;
@include media-breakpoint-up(md) {
flex: 0 0 60%;
}
}
.hero-image {
width: auto;
display: none;
flex: 1;
object-fit: contain;
max-height: 36rem;
@ -108,7 +143,7 @@
}
}
.card {
.section-with-cards .card {
@include transition();
&:hover {
@ -356,10 +391,15 @@
}
}
.split-section-content {
.split-section-content.align-top {
margin-top: 2rem;
}
.split-section-content.align-middle {
margin-top: auto;
margin-bottom: auto;
}
.section-image-grid {
display: flex;
flex-wrap: wrap;
@ -409,3 +449,228 @@
}
}
}
/* Section with Collapsible Content */
.collapsible-items {
max-width: 46rem;
}
.collapsible-item {
padding: 1.75rem 0;
&:not(:last-child) {
border-bottom: 1px solid $border-color;
}
}
.collapsible-title {
display: flex;
justify-content: space-between;
align-items: center;
}
.collapsible-item a {
text-decoration: none;
}
.collapsible-item h3 {
margin-bottom: 0;
}
.collapsible-icon {
color: $gray-600;
flex-shrink: 0;
}
.collapsible-icon .vertical {
@include transition();
}
.collapsible-icon.is-opened .vertical {
opacity: 0;
}
.collapsible-content {
margin-top: 1rem;
margin-bottom: 0;
color: $gray-700;
}
.section-with-collapsible-content.align-center {
.section-title, .section-description {
text-align: center;
}
.section-description, .collapsible-items {
margin-left: auto;
margin-right: auto;
}
}
/* Section with Features */
.section-features {
display: grid;
&[data-columns="2"] {
grid-template-columns: repeat(1, 1fr);
gap: 2.5rem;
@include media-breakpoint-up(sm) {
gap: 3rem;
}
@include media-breakpoint-up(md) {
grid-template-columns: repeat(2, 1fr);
gap: 6rem;
}
.feature-title {
font-size: $font-size-xl;
font-weight: bold;
@include media-breakpoint-up(md) {
font-size: $font-size-2xl;
}
}
.feature-content {
font-size: $font-size-base;
margin-top: 1.75rem;
@include media-breakpoint-up(xl) {
font-size: $font-size-lg;
}
}
.feature-url {
margin-top: 1.75rem;
}
.feature-icon {
margin-bottom: 2rem;
width: 3.375rem;
height: 3.375rem;
object-fit: contain;
}
}
&[data-columns="3"] {
grid-template-columns: repeat(1, 1fr);
gap: 2rem;
@include media-breakpoint-up(sm) {
grid-template-columns: repeat(2, 1fr);
gap: 2.5rem;
}
@include media-breakpoint-up(md) {
grid-template-columns: repeat(3, 1fr);
gap: 4.875rem;
}
.feature-title {
font-size: $font-size-lg;
font-weight: 600;
@include media-breakpoint-up(md) {
font-size: $font-size-xl;
}
}
.feature-content {
font-size: $font-size-base;
margin-top: 1rem;
}
.feature-url {
margin-top: 1rem;
}
.feature-icon {
margin-bottom: 1.75rem;
width: 2.5rem;
height: 2.5rem;
object-fit: contain;
}
}
&[data-columns="4"] {
grid-template-columns: repeat(1, 1fr);
gap: 2rem;
@include media-breakpoint-up(sm) {
grid-template-columns: repeat(2, 1fr);
gap: 2.5rem;
}
@include media-breakpoint-up(md) {
grid-template-columns: repeat(3, 1fr);
gap: 3rem;
}
@include media-breakpoint-up(lg) {
grid-template-columns: repeat(4, 1fr);
gap: 3.75rem;
}
.feature-title {
font-size: $font-size-base;
font-weight: 600;
}
.feature-content {
font-size: $font-size-sm;
margin-top: 0.875rem;
}
.feature-url {
margin-top: 0.875rem;
font-size: $font-size-sm;
}
.feature-icon {
margin-bottom: 1.5rem;
width: 2.375rem;
height: 2.375rem;
object-fit: contain;
}
}
}
.section-title + .section-features, .section-description + .section-features {
&[data-columns="2"] {
margin-top: 3.75rem;
}
&[data-columns="3"] {
margin-top: 3rem;
}
&[data-columns="4"] {
margin-top: 2.5rem;
}
}
.section-feature {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.feature-title, .feature-content {
margin-bottom: 0;
}
.feature-url {
display: inline-block;
margin-top: auto;
}
/* Section with Embed */
.section-with-embed .embed-container {
margin-top: 2rem;
}

View file

@ -51,6 +51,14 @@ $dropdown-border-radius: 0.375rem !default;
$dropdown-item-padding-y: 0.5rem !default;
$dropdown-item-padding-x: 0.5rem !default;
$input-bg: $gray-100;
$input-focus-bg: $gray-200;
$input-focus-box-shadow: none;
$input-border-color: $gray-100;
$input-focus-border-color: $gray-200;
$input-border-radius: 0.375rem;
$custom-control-indicator-bg: white;
$grid-breakpoints: (
xs: 0,
sm: 576px,
@ -60,6 +68,34 @@ $grid-breakpoints: (
2xl: 1440px
) !default;
$spacers: (
0: 0,
1: 0.25rem,
2: 0.5rem,
3: 0.75rem,
4: 1rem,
5: 1.25rem,
6: 1.5rem,
8: 2rem,
10: 2.5rem,
12: 3rem,
14: 3.5rem,
16: 4rem,
18: 4.5rem,
20: 5rem,
22: 5.5rem,
24: 6rem,
28: 7rem,
32: 8rem,
36: 9rem,
40: 10rem,
44: 11rem,
48: 12rem,
52: 13rem,
56: 14rem,
64: 16rem,
);
@import '~bootstrap/scss/functions';
@import '~bootstrap/scss/variables';
@import "~bootstrap/scss/mixins";

View file

@ -1,5 +1,6 @@
@import '~quill/dist/quill.core';
@import 'variables';
@import 'css_variables';
@import 'frappe/public/css/font-awesome';
@import '~bootstrap/scss/bootstrap';
@import 'base';
@ -12,6 +13,8 @@
@import 'portal';
@import 'search';
@import 'doc';
@import 'navbar';
@import 'footer';
@import 'login';
.ql-editor.read-mode {
@ -61,29 +64,6 @@
}
}
.navbar-light {
border-bottom: 1px solid $border-color;
}
.navbar-light .navbar-nav .nav-link {
color: $gray-700;
font-size: $font-size-sm;
font-weight: 500;
&:hover,
&:focus, &.active {
color: $primary;
}
}
.navbar-brand {
img {
display: inline-block;
max-width: 150px;
max-height: 25px;
}
}
.dropdown-menu {
padding: 0.25rem;
}
@ -92,43 +72,13 @@
border-radius: $dropdown-border-radius;
}
.navbar.bg-dark {
.dropdown-menu {
font-size: 0.75rem;
background-color: $dark;
border-radius: 0;
}
.nav-link {
white-space: nowrap;
color: $light;
&:hover {
color: $primary;
}
}
.nav-item {
padding: 0rem 1rem;
}
.dropdown-item {
color: $light;
&:hover {
background-color: $dark;
color: $primary;
}
}
}
.input-dark {
background-color: $dark;
border-color: darken($primary, 40%);
color: $light;
}
.page-content-wrapper {
.main-column .page-content-wrapper {
margin: 2rem 0;
}
@ -163,68 +113,6 @@ a.card {
color: #d1d8dd !important;
}
// footer
.web-footer {
padding: 5rem 0;
min-height: 140px;
}
.footer-logo {
width: 5rem;
height: 2rem;
object-fit: contain;
object-position: left;
}
.footer-link, .footer-child-item a {
font-weight: 500;
color: $gray-700;
&:hover {
color: $primary;
text-decoration: none;
}
}
.footer-col-left, .footer-col-right {
padding-top: 0.8rem;
padding-bottom: 1rem;
line-height: 2;
}
.footer-col-right {
@include media-breakpoint-up(sm) {
text-align: right;
}
}
.footer-col-left .footer-link {
margin-right: 1rem;
}
.footer-col-right .footer-link {
margin-right: 1rem;
@include media-breakpoint-up(sm) {
margin-right: 0;
margin-left: 1rem;
}
}
.footer-group-label {
color: $text-muted;
}
.footer-parent-item {
margin-bottom: 0.5rem;
}
.footer-info {
border-top: 1px solid $border-color;
color: $text-muted;
font-size: $font-size-sm;
}
.no-underline {
text-decoration: none !important;
}
@ -356,3 +244,9 @@ h5.modal-title {
white-space: nowrap;
text-overflow: ellipsis;
}
.about-section {
padding-top: 1rem;
}
.about-footer {
padding-top: 1rem;
}

View file

@ -68,7 +68,13 @@
{%- endblock -%}
{%- block navbar -%}
{% include "templates/includes/navbar/navbar.html" %}
{{ web_block(
navbar_template or 'Standard Navbar',
values=_context_dict,
add_container=0,
add_top_padding=0,
add_bottom_padding=0,
) }}
{%- endblock -%}
{% block content %}
@ -76,7 +82,13 @@
{% endblock %}
{%- block footer -%}
{% include "templates/includes/footer/footer.html" %}
{{ web_block(
footer_template or 'Standard Footer',
values=_context_dict,
add_container=0,
add_top_padding=0,
add_bottom_padding=0
) }}
{%- endblock -%}
{% block base_scripts %}

View file

@ -1,7 +1,7 @@
{% from "frappe/templates/includes/macros.html" import square_image_with_fallback %}
<div class="media">
{{ square_image_with_fallback(src=blogger_info.avatar, size='small', alt=blogger_info.full_name, class='align-self-start mr-3 rounded') }}
{{ square_image_with_fallback(src=blogger_info.avatar, size='small', alt=blogger_info.full_name, class='align-self-start mr-4 rounded') }}
<div class="media-body">
<h5 class="mt-0">
<a href="/blog?blogger={{ blogger_info.name }}" class="text-dark">{{ blogger_info.full_name }}</a>
@ -10,4 +10,4 @@
<p class="text-muted">{{ blogger_info.bio }}</p>
{% endif %}
</div>
</div>
</div>

View file

@ -1,7 +1,7 @@
{% from "frappe/templates/includes/macros.html" import square_image_with_fallback %}
<div class="comment-row media">
{{ square_image_with_fallback(src=frappe.get_gravatar(comment.comment_email or comment.sender), size='extra-small', alt=comment.sender_full_name, class='align-self-start mr-3') }}
{{ square_image_with_fallback(src=frappe.get_gravatar(comment.comment_email or comment.sender), size='extra-small', alt=comment.sender_full_name, class='align-self-start mr-4') }}
<div class="media-body">
<div class="d-flex justify-content-between align-items-start">
<span class="font-weight-bold text-muted">

View file

@ -1,6 +1,6 @@
<div class="comment-view mb-4">
<div class="comment-view mb-6">
{% if comment_text %}
<div class="comment-header mb-4">{{ comment_text }}</div>
<div class="comment-header mb-6">{{ comment_text }}</div>
{% endif %}
{% if not comment_list %}
<div class="no-comment">

View file

@ -1,46 +1,12 @@
<footer class="web-footer">
<div class="container">
{%- if footer_logo -%}
<div>
<img src="{{ footer_logo }}" alt="Footer Logo" class="footer-logo">
</div>
{%- endif -%}
<div class="row">
<div class="text-left col-sm-6">
{% if footer_items -%}
<div class="row">
{% include ["templates/includes/footer/footer_grouped_links.html", "templates/includes/footer/footer_items.html"] %}
</div>
{% endif %}
</div>
{% include "templates/includes/footer/footer_logo_extension.html" %}
<div class="text-right col-sm-6">
{% block extension %}
{% include "templates/includes/footer/footer_extension.html" %}
{% endblock %}
</div>
</div>
{% if footer_items -%}
{% include "templates/includes/footer/footer_grouped_links.html" %}
{% endif %}
{% include "templates/includes/footer/footer_links.html" %}
<div class="footer-info">
<div class="row">
<div class="footer-col-left col-sm-6 col-12">
{% if copyright %}
&copy; {{ copyright }}
{% endif %}
{% if footer_address %}
{% if copyright %}<br>{% endif %}
{{ footer_address }}
{% endif %}
</div>
{# powered #}
<div class="footer-col-right col-sm-6 col-12 footer-powered">
{% block powered %}
{% include "templates/includes/footer/footer_powered.html" %}
{% endblock %}
</div>
</div>
</div>
{% include "templates/includes/footer/footer_info.html" %}
</div>
</footer>

View file

@ -1,28 +1,32 @@
{% for page in footer_items if page.child_items %}
<div class="col footer-group">
<div data-label="{{ page.label }}">
<span>
<div class="footer-group-label footer-parent-item">
{%- if page.icon -%}
<img src="{{ page.icon }}" alt="{{ page.label }}">
{%- else -%}
{{ page.label }}
{%- endif -%}
</div>
</span>
<ul class="list-unstyled">
{%- for child in page.child_items -%}
<li class="mt-2 footer-child-item" data-label='{{ child.label }}'>
<a href="{{ child.url | abs_url }}" {% if child.target %} target="_blank" {% endif %}>
{%- if child.icon -%}
<img src="{{ child.icon }}" alt="{{ child.label }}">
<div class="footer-grouped-links">
<div class="row">
{% for group in footer_items if group.child_items %}
{# 2 columns to every 5 links, so 5 links get 2 columns, 5-10 links get 4 columns, and so on #}
{%- set cols = frappe.utils.ceil((group.child_items | len) / 5) * 2 -%}
<div class="col-sm-{{ cols }} footer-group">
<div data-label="{{ group.label }}">
<h5 class="footer-group-label">
{%- if group.icon -%}
<img src="{{ group.icon }}" alt="{{ group.label }}">
{%- else -%}
{{ child.label }}
{{ group.label }}
{%- endif -%}
</a>
</li>
{%- endfor -%}
</ul>
</h5>
<ul class="footer-group-links list-unstyled">
{%- for child in group.child_items -%}
<li class="footer-child-item" data-label="{{ child.label }}">
<a href="{{ child.url | abs_url }}" {% if child.target %} target="_blank" {% endif %}>
{%- if child.icon -%}
<img src="{{ child.icon }}" alt="{{ child.label }}">
{%- else -%}
{{ child.label }}
{%- endif -%}
</a>
</li>
{%- endfor -%}
</ul>
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}

View file

@ -0,0 +1,19 @@
<div class="footer-info">
<div class="row">
<div class="footer-col-left col-sm-6 col-12">
{% if copyright %}
&copy; {{ copyright }}
{% endif %}
{% if footer_address %}
{% if copyright %}<br>{% endif %}
{{ footer_address }}
{% endif %}
</div>
{# powered #}
<div class="footer-col-right col-sm-6 col-12 footer-powered">
{% block powered %}
{% include "templates/includes/footer/footer_powered.html" %}
{% endblock %}
</div>
</div>
</div>

View file

@ -1,28 +0,0 @@
{% for page in footer_items %}
{% if not page.parent_label %}
<div class="col footer-group">
{% if not page.parent_label -%}
<div data-label='{{ page.label }}'>
<a {% if not page.child_items -%} href="{{ (page.url or '')|abs_url }}" {%- endif %}
{% if page.child_items %} onclick="return false;" {% endif %}
{{ page.target or ''}}>
{%- if page.child_items -%}
<div class="footer-group-label footer-parent-item">{{ page.label }}</div>
</a>
<ul class="list-unstyled">
{%- for child in page.child_items -%}
<li class="footer-child-item mt-2" data-label='{{ child.label }}'>
<a href="{{ child.url | abs_url }}"
{% if child.target %}target="_blank"{% endif %}>{{ child.label }}</a>
</li>
{%- endfor -%}
</ul>
{%- else -%}
<div class="footer-group-label">{{ page.label }}</div>
</a>
{%- endif -%}
</div>
{%- endif -%}
</div>
{% endif %}
{% endfor %}

View file

@ -10,15 +10,15 @@
<div class="footer-links">
<div class="row">
<div class="footer-col-left col-sm-6">
{% for item in footer_items if item.label and not (item.parent_label or item.child_items) and not item.right %}
{%- for item in footer_items if item.label and not (item.parent_label or item.child_items) and not item.right %}
{{ footer_link(item) }}
{% endfor %}
{% endfor -%}
</div>
<div class="footer-col-right col-sm-6">
{% for item in footer_items if item.label and not (item.parent_label or item.child_items) and item.right %}
{%- for item in footer_items if item.label and not (item.parent_label or item.child_items) and item.right %}
{{ footer_link(item) }}
{% endfor %}
{% endfor -%}
</div>
</div>
</div>

View file

@ -0,0 +1,16 @@
<div class="footer-logo-extension">
<div class="row">
<div class="text-left col-6">
{%- if footer_logo -%}
<div>
<img src="{{ footer_logo }}" alt="Footer Logo" class="footer-logo">
</div>
{%- endif -%}
</div>
<div class="text-right col-6">
{% block extension %}
{% include "templates/includes/footer/footer_extension.html" %}
{% endblock %}
</div>
</div>
</div>

View file

@ -90,7 +90,7 @@
</ul>
{%- if call_to_action -%}
<a class="btn btn-primary" href="{{ call_to_action_url | abs_url }}">
<a class="btn btn-primary navbar-cta" href="{{ call_to_action_url | abs_url }}">
{{ call_to_action }}
</a>
{%- endif -%}

View file

@ -1,5 +1,5 @@
{% for d in results %}
<div class="search-result-item mb-4">
<div class="search-result-item mb-6">
<a href="{{ d.route }}"><b>{{ d.title }}</b></a>
<p class="m-0">{{ d.preview }}</p>
</div>

View file

@ -4,7 +4,7 @@
<div class="col-sm-6">
<div class="search-result">
{% if title %}
<h3 class="mb-4">{{ title }}</h3>
<h3 class="mb-6">{{ title }}</h3>
{% endif %}
{% include "templates/includes/search_result.html" %}
@ -15,7 +15,7 @@
</div>
{%- endmacro %}
<div class="row mb-5">
<div class="row mb-12">
<div class="col-sm-6">
<form action="{{ route }}">
<div class="input-group">

View file

@ -7,15 +7,19 @@
web_block.css_class
]) -%}
{%- if web_template_type == 'Section' -%}
{%- if not web_block.hide_block -%}
<section class="section {{ classes }}" data-section-idx="{{ web_block.idx | e }}"
data-section-template="{{ web_block.web_template | e }}">
{%- if web_block.add_container -%}
<div class="container">
{%- endif -%}
{{ web_block.render() }}
{{ web_template_html }}
{%- if web_block.add_container -%}
</div>
{%- endif -%}
</section>
{%- endif -%}
{%- else -%}
{{ web_template_html }}
{%- endif -%}

View file

@ -14,7 +14,7 @@
{{ _(item.title or item.label) }}
</a>
{% else %}
<form action='{{ item.route }}' class="mr-3">
<form action='{{ item.route }}' class="mr-4">
<input name='q' class='form-control' type='text' style="outline: none"
placeholder="{{ _(item.title or item.label) }}">
</form>

View file

@ -621,28 +621,6 @@ def parse_json(val):
val = frappe._dict(val)
return val
def cast_fieldtype(fieldtype, value):
if fieldtype in ("Currency", "Float", "Percent"):
value = flt(value)
elif fieldtype in ("Int", "Check"):
value = cint(value)
elif fieldtype in ("Data", "Text", "Small Text", "Long Text",
"Text Editor", "Select", "Link", "Dynamic Link"):
value = cstr(value)
elif fieldtype == "Date":
value = getdate(value)
elif fieldtype == "Datetime":
value = get_datetime(value)
elif fieldtype == "Time":
value = to_timedelta(value)
return value
def get_db_count(*args):
"""
Pass a doctype or a series of doctypes to get the count of docs in them

View file

@ -68,6 +68,12 @@ class BackupGenerator:
dir = os.path.dirname(file_path)
os.makedirs(dir, exist_ok=True)
@property
def site_config_backup_path(self):
# For backwards compatibility
import click
click.secho("BackupGenerator.site_config_backup_path has been deprecated in favour of BackupGenerator.backup_path_conf", fg="yellow")
return getattr(self, "backup_path_conf", None)
def get_backup(self, older_than=24, ignore_files=False, force=False):
"""
@ -96,7 +102,7 @@ class BackupGenerator:
self.backup_path_files = last_file
self.backup_path_db = last_db
self.backup_path_private_files = last_private_file
self.site_config_backup_path = site_config_backup_path
self.backup_path_conf = site_config_backup_path
def set_backup_file_name(self):
#Generate a random name using today's date and a 8 digit random number

View file

@ -344,6 +344,11 @@ def format_datetime(datetime_string, format_string=None):
return formatted_datetime
def format_duration(seconds, hide_days=False):
"""Converts the given duration value in float(seconds) to duration format
example: converts 12885 to '3h 34m 45s' where 12885 = seconds in float
"""
total_duration = {
'days': math.floor(seconds / (3600 * 24)),
'hours': math.floor(seconds % (3600 * 24) / 3600),
@ -371,6 +376,41 @@ def format_duration(seconds, hide_days=False):
return duration
def duration_to_seconds(duration):
"""Converts the given duration formatted value to duration value in seconds
example: converts '3h 34m 45s' to 12885 (value in seconds)
"""
validate_duration_format(duration)
value = 0
if 'd' in duration:
val = duration.split('d')
days = val[0]
value += cint(days) * 24 * 60 * 60
duration = val[1]
if 'h' in duration:
val = duration.split('h')
hours = val[0]
value += cint(hours) * 60 * 60
duration = val[1]
if 'm' in duration:
val = duration.split('m')
mins = val[0]
value += cint(mins) * 60
duration = val[1]
if 's' in duration:
val = duration.split('s')
secs = val[0]
value += cint(secs)
return value
def validate_duration_format(duration):
import re
is_valid_duration = re.match("^(?:(\d+d)?((^|\s)\d+h)?((^|\s)\d+m)?((^|\s)\d+s)?)$", duration)
if not is_valid_duration:
frappe.throw(frappe._("Value {0} must be in the valid duration format: d h m s").format(frappe.bold(duration)))
def get_weekdays():
return ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
@ -411,6 +451,28 @@ def has_common(l1, l2):
"""Returns truthy value if there are common elements in lists l1 and l2"""
return set(l1) & set(l2)
def cast_fieldtype(fieldtype, value):
if fieldtype in ("Currency", "Float", "Percent"):
value = flt(value)
elif fieldtype in ("Int", "Check"):
value = cint(value)
elif fieldtype in ("Data", "Text", "Small Text", "Long Text",
"Text Editor", "Select", "Link", "Dynamic Link"):
value = cstr(value)
elif fieldtype == "Date":
value = getdate(value)
elif fieldtype == "Datetime":
value = get_datetime(value)
elif fieldtype == "Time":
value = to_timedelta(value)
return value
def flt(s, precision=None):
"""Convert to float (ignore commas)"""
if isinstance(s, string_types):
@ -731,6 +793,7 @@ def is_image(filepath):
return (guess_type(filepath)[0] or "").startswith("image/")
def get_thumbnail_base64_for_image(src):
from os.path import exists as file_exists
from PIL import Image
from frappe.core.doctype.file.file import get_local_image
from frappe import safe_decode, cache
@ -741,7 +804,14 @@ def get_thumbnail_base64_for_image(src):
if not src.startswith('/files') or '..' in src:
return
if src.endswith('.svg'):
return
def _get_base64():
file_path = frappe.get_site_path("public", src.lstrip("/"))
if not file_exists(file_path):
return
try:
image, unused_filename, extn = get_local_image(src)
except IOError:
@ -765,7 +835,7 @@ def image_to_base64(image, extn):
from io import BytesIO
buffered = BytesIO()
if extn.lower() == 'jpg':
if extn.lower() in ('jpg', 'jpe'):
extn = 'JPEG'
image.save(buffered, extn)
img_str = base64.b64encode(buffered.getvalue())
@ -1009,20 +1079,22 @@ def evaluate_filters(doc, filters):
if isinstance(filters, dict):
for key, value in iteritems(filters):
f = get_filter(None, {key:value})
if not compare(doc.get(f.fieldname), f.operator, f.value):
if not compare(doc.get(f.fieldname), f.operator, f.value, f.fieldtype):
return False
elif isinstance(filters, (list, tuple)):
for d in filters:
f = get_filter(None, d)
if not compare(doc.get(f.fieldname), f.operator, f.value):
if not compare(doc.get(f.fieldname), f.operator, f.value, f.fieldtype):
return False
return True
def compare(val1, condition, val2):
def compare(val1, condition, val2, fieldtype=None):
ret = False
if fieldtype:
val2 = cast_fieldtype(fieldtype, val2)
if condition in operator_map:
ret = operator_map[condition](val1, val2)
@ -1036,6 +1108,7 @@ def get_filter(doctype, f, filters_config=None):
"fieldname":
"operator":
"value":
"fieldtype":
}
"""
from frappe.model import default_fields, optional_fields
@ -1087,6 +1160,13 @@ def get_filter(doctype, f, filters_config=None):
f.doctype = df.options
break
try:
df = frappe.get_meta(f.doctype).get_field(f.fieldname)
except frappe.exceptions.DoesNotExistError:
df = None
f.fieldtype = df.fieldtype if df else None
return f
def make_filter_tuple(doctype, key, value):
@ -1295,4 +1375,4 @@ def validate_json_string(string):
try:
json.loads(string)
except (TypeError, ValueError):
raise frappe.ValidationError
raise frappe.ValidationError

View file

@ -22,7 +22,8 @@ def get_jenv():
jenv.globals.update({
'resolve_class': resolve_class,
'inspect': inspect,
'web_blocks': web_blocks
'web_blocks': web_blocks,
'web_block': web_block
})
frappe.local.jenv = jenv
@ -191,24 +192,34 @@ def inspect(var, render=True):
html = ""
return get_jenv().from_string(html).render(context)
def web_block(template, values, **kwargs):
options = {"template": template, "values": values}
options.update(kwargs)
return web_blocks([options])
def web_blocks(blocks):
from frappe import get_doc
from frappe import throw, _dict
from frappe.website.doctype.web_page.web_page import get_web_blocks_html
web_blocks = []
for block in blocks:
doc = {
if not block.get('template'):
throw('Web Template is not specified')
doc = _dict({
'doctype': 'Web Page Block',
'web_template': block['template'],
'web_template_values': block['values'],
'web_template_values': block.get('values', {}),
'add_top_padding': 1,
'add_bottom_padding': 1,
'add_container': 1,
'hide_block': 0,
'css_class': ''
}
})
doc.update(block)
web_blocks.append(get_doc(doc))
web_blocks.append(doc)
out = get_web_blocks_html(web_blocks)

View file

@ -31,10 +31,9 @@ def get_context(path, args=None):
if hasattr(frappe.local, 'response') and frappe.local.response.get('context'):
context.update(frappe.local.response.context)
# to be able to inspect the context in development
# to be able to inspect the context dict
# Use the macro "inspect" from macros.html
if frappe.conf.developer_mode:
context._context_dict = context
context._context_dict = context
context.developer_mode = frappe.conf.developer_mode

View file

@ -33,17 +33,14 @@
<!-- end blog content -->
</article>
{%- if enable_cta -%}
{{ web_blocks([
{
'template': "Section With Small CTA",
'values': cta,
'add_container': 0,
'add_top_padding': 0,
'add_bottom_padding': 0,
'css_class': "my-5"
}
])
}}
{{ web_block(
"Section With Small CTA",
values=cta,
add_container=0,
add_top_padding=0,
add_bottom_padding=0,
css_class="my-5"
) }}
{%- endif -%}
<div class="blog-footer">
<div>

View file

@ -4,34 +4,30 @@
{% block page_content %}
{{ web_blocks([
{
'template': "Hero",
'values': {
'title': blog_title or _("Blog"),
'subtitle': blog_introduction or '',
},
'add_container': 0,
'add_top_padding': 0,
'add_bottom_padding': 0,
'css_class': "py-5"
}
])
}}
{{ web_block("Hero",
values={
'title': blog_title or _("Blog"),
'subtitle': blog_introduction or '',
},
add_container=0,
add_top_padding=0,
add_bottom_padding=0,
css_class="py-5"
) }}
<div class="blog-list-content">
<div class="website-list" data-doctype="{{ doctype }}" data-txt="{{ txt or '[notxt]' | e }}">
<div id="blog-list" class="blog-list result row">
{% if not result -%}
<div class="text-muted" style="min-height: 300px;">
{{ no_result_message or _("Nothing to show") }}
</div>
{% else %}
{% for item in result %}
{{ item }}
{% endfor %}
{% endif %}
{% if not result -%}
<div class="text-muted" style="min-height: 300px;">
{{ no_result_message or _("Nothing to show") }}
</div>
{% else %}
<div id="blog-list" class="blog-list result row">
{% for item in result %}
{{ item }}
{% endfor %}
</div>
{% endif %}
<button class="btn btn-light btn-more btn {% if not show_more -%} hidden {%- endif %}">{{ _("Load More") }}</button>
</div>
</div>

View file

@ -18,7 +18,7 @@
<p><br><a href="/{{ category.route }}" class='text-muted small'>
{{ _("More articles on {0}").format(category.name) }}</a></p>
</article>
<div class="help-article-feedback mb-4">
<div class="help-article-feedback mb-6">
<hr />
<div class="feedback-view ">
<div class="text-muted small mr-2 mb-2">{{ _("Was this article helpful?") }}</div>

View file

@ -30,7 +30,7 @@ data-web-form="{{ name }}" data-web-form-doctype="{{ doc_type }}" data-login-req
{# web form list #}
<div class="web-form-wrapper" {{ container_attributes() }}></div>
<div id="list-filters" class="row"></div>
<div id="datatable" class="pt-3"></div>
<div id="datatable" class="pt-4"></div>
<div class="list-view-footer text-right"></div>
{% else %}
{# web form #}

View file

@ -36,6 +36,10 @@
<style>
{{ style or "" }}
</style>
{%- for style in page_builder_styles -%}
<style>{{ style }}</style>
{%- endfor -%}
{% endblock %}
{% block script %}

View file

@ -53,4 +53,24 @@ class TestWebPage(unittest.TestCase):
web_page.save()
self.assertTrue('html content' in get_page_content('/test-content-type'))
web_page.delete()
def test_dynamic_route(self):
web_page = frappe.get_doc(dict(
doctype = 'Web Page',
title = 'Test Dynamic Route',
published = 1,
dynamic_route = 1,
route = '/doctype-view/<doctype>',
content_type = 'HTML',
dymamic_template = 1,
main_section_html = '<div>{{ frappe.form_dict.doctype }}</div>'
)).insert()
try:
content = get_page_content('/doctype-view/DocField')
self.assertTrue('<div>DocField</div>' in content)
finally:
web_page.delete()

Some files were not shown because too many files have changed in this diff Show more