Merge branch 'develop' of github.com:frappe/frappe into mysql-syntax-error
This commit is contained in:
commit
e6eee1ff83
160 changed files with 2721 additions and 1878 deletions
34
.snyk
34
.snyk
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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.
|
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
0
frappe/core/doctype/document_naming_rule/__init__.py
Normal file
0
frappe/core/doctype/document_naming_rule/__init__.py
Normal 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');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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) {
|
||||
|
||||
// }
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
15
frappe/patches/v13_0/web_template_set_module.py
Normal file
15
frappe/patches/v13_0/web_template_set_module.py
Normal 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()
|
||||
|
|
@ -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"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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')},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
3
frappe/public/js/frappe/ui/datatable.js
Normal file
3
frappe/public/js/frappe/ui/datatable.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import DataTable from "frappe-datatable";
|
||||
|
||||
frappe.DataTable = DataTable;
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
71
frappe/public/js/frappe/utils/web_template.js
Normal file
71
frappe/public/js/frappe/utils/web_template.js
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 || '');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
28
frappe/public/scss/css_variables.scss
Normal file
28
frappe/public/scss/css_variables.scss
Normal 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};
|
||||
}
|
||||
82
frappe/public/scss/footer.scss
Normal file
82
frappe/public/scss/footer.scss
Normal 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;
|
||||
}
|
||||
|
|
@ -10,6 +10,10 @@
|
|||
margin-top: 0;
|
||||
}
|
||||
|
||||
> :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 2.5rem;
|
||||
|
|
|
|||
59
frappe/public/scss/navbar.scss
Normal file
59
frappe/public/scss/navbar.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
© {{ 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>
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
19
frappe/templates/includes/footer/footer_info.html
Normal file
19
frappe/templates/includes/footer/footer_info.html
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<div class="footer-info">
|
||||
<div class="row">
|
||||
<div class="footer-col-left col-sm-6 col-12">
|
||||
{% if copyright %}
|
||||
© {{ 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>
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
16
frappe/templates/includes/footer/footer_logo_extension.html
Normal file
16
frappe/templates/includes/footer/footer_logo_extension.html
Normal 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>
|
||||
|
|
@ -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 -%}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 -%}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 #}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@
|
|||
<style>
|
||||
{{ style or "" }}
|
||||
</style>
|
||||
|
||||
{%- for style in page_builder_styles -%}
|
||||
<style>{{ style }}</style>
|
||||
{%- endfor -%}
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue