Merge branch 'develop' into commit-auto-updated-files
This commit is contained in:
commit
62384fc8f8
70 changed files with 1273 additions and 369 deletions
|
|
@ -2,6 +2,6 @@
|
|||
"baseUrl": "http://test_site_ui:8000",
|
||||
"projectId": "92odwv",
|
||||
"adminPassword": "admin",
|
||||
"defaultCommandTimeout": 10000,
|
||||
"defaultCommandTimeout": 20000,
|
||||
"pageLoadTimeout": 15000
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
context('Control Link', () => {
|
||||
beforeEach(() => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/desk#workspace/Website');
|
||||
cy.create_records({
|
||||
doctype: 'ToDo',
|
||||
description: 'this is a test todo for link'
|
||||
|
|
@ -30,7 +34,7 @@ context('Control Link', () => {
|
|||
|
||||
cy.get('.frappe-control[data-fieldname=link] input').focus().as('input');
|
||||
cy.wait('@search_link');
|
||||
cy.get('@input').type('todo for link');
|
||||
cy.get('@input').type('todo for link', { delay: 200 });
|
||||
cy.wait('@search_link');
|
||||
cy.get('.frappe-control[data-fieldname=link] ul').should('be.visible');
|
||||
cy.get('.frappe-control[data-fieldname=link] input').type('{enter}', { delay: 100 });
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
context('Relative Timeframe', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
cy.visit('/desk#workspace/Website');
|
||||
});
|
||||
before(() => {
|
||||
cy.login();
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ def load_desktop_data(bootinfo):
|
|||
from frappe.desk.desktop import get_desk_sidebar_items
|
||||
bootinfo.allowed_modules = get_modules_from_all_apps_for_user()
|
||||
bootinfo.allowed_workspaces = get_desk_sidebar_items(True)
|
||||
bootinfo.dashboards = frappe.get_list("Dashboard")
|
||||
bootinfo.dashboards = frappe.get_all("Dashboard")
|
||||
|
||||
def get_allowed_pages(cache=False):
|
||||
return get_user_pages_or_reports('Page', cache=cache)
|
||||
|
|
|
|||
|
|
@ -137,7 +137,6 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
|
|||
to_date = datetime.datetime.now()
|
||||
|
||||
doctype = chart.document_type
|
||||
unit_function = get_unit_function(doctype, chart.based_on, timegrain)
|
||||
datefield = chart.based_on
|
||||
aggregate_function = get_aggregate_function(chart.chart_type)
|
||||
value_field = chart.value_based_on or '1'
|
||||
|
|
@ -150,23 +149,18 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
|
|||
data = frappe.db.get_list(
|
||||
doctype,
|
||||
fields = [
|
||||
'extract(year from `tab{doctype}`.{datefield}) as _year'.format(doctype=doctype, datefield=datefield),
|
||||
'{} as _unit'.format(unit_function),
|
||||
'{} as _unit'.format(datefield),
|
||||
'{aggregate_function}({value_field})'.format(aggregate_function=aggregate_function, value_field=value_field),
|
||||
],
|
||||
filters = filters,
|
||||
group_by = '_year, _unit',
|
||||
order_by = '_year asc, _unit asc',
|
||||
group_by = '_unit',
|
||||
order_by = '_unit asc',
|
||||
as_list = True,
|
||||
ignore_ifnull = True
|
||||
)
|
||||
|
||||
result = get_result(data, timegrain, from_date, to_date)
|
||||
|
||||
# result given as year, unit -> convert it to end of period of that unit
|
||||
result = convert_to_dates(data, timegrain)
|
||||
|
||||
# add missing data points for periods where there was no result
|
||||
result = add_missing_values(result, timegrain, timespan, from_date, to_date)
|
||||
chart_config = {
|
||||
"labels": [formatdate(r[0].strftime('%Y-%m-%d')) for r in result],
|
||||
"datasets": [{
|
||||
|
|
@ -261,75 +255,22 @@ def get_aggregate_function(chart_type):
|
|||
}[chart_type]
|
||||
|
||||
|
||||
def convert_to_dates(data, timegrain):
|
||||
""" Converts individual dates within data to the end of period """
|
||||
result = []
|
||||
for d in data:
|
||||
if d[2] != 0:
|
||||
if timegrain == 'Daily':
|
||||
result.append([add_to_date('{:d}-01-01'.format(int(d[0])), days = d[1] - 1), d[2]])
|
||||
elif timegrain == 'Weekly':
|
||||
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), weeks = d[1] + 1), days = -1), d[2]])
|
||||
elif timegrain == 'Monthly':
|
||||
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=d[1]), days = -1), d[2]])
|
||||
elif timegrain == 'Quarterly':
|
||||
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=d[1] * 3), days = -1), d[2]])
|
||||
elif timegrain == 'Yearly':
|
||||
result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=12), days = -1), d[2]])
|
||||
result[-1][0] = getdate(result[-1][0])
|
||||
|
||||
return result
|
||||
|
||||
def get_unit_function(doctype, datefield, timegrain):
|
||||
unit_function = ''
|
||||
if timegrain=='Daily':
|
||||
if frappe.db.db_type == 'mariadb':
|
||||
unit_function = 'dayofyear(`tab{doctype}`.{datefield})'.format(
|
||||
doctype=doctype, datefield=datefield)
|
||||
else:
|
||||
unit_function = 'extract(doy from `tab{doctype}`.{datefield})'.format(
|
||||
doctype=doctype, datefield=datefield)
|
||||
|
||||
else:
|
||||
unit_function = 'extract({unit} from `tab{doctype}`.{datefield})'.format(
|
||||
unit = timegrain[:-2].lower(), doctype=doctype, datefield=datefield)
|
||||
|
||||
return unit_function
|
||||
|
||||
def add_missing_values(data, timegrain, timespan, from_date, to_date):
|
||||
# add missing intervals
|
||||
def get_result(data, timegrain, from_date, to_date):
|
||||
start_date = getdate(from_date)
|
||||
end_date = getdate(to_date)
|
||||
result = []
|
||||
|
||||
if timespan != 'All Time':
|
||||
first_expected_date = get_period_ending(from_date, timegrain)
|
||||
# fill out data before the first data point
|
||||
first_data_point_date = data[0][0] if data else getdate(add_to_date(to_date, days=1))
|
||||
while first_data_point_date > first_expected_date:
|
||||
result.append([first_expected_date, 0.0])
|
||||
first_expected_date = get_next_expected_date(first_expected_date, timegrain)
|
||||
while start_date <= end_date:
|
||||
next_date = get_next_expected_date(start_date, timegrain)
|
||||
result.append([next_date, 0.0])
|
||||
start_date = next_date
|
||||
|
||||
# fill data points and missing points
|
||||
for i, d in enumerate(data):
|
||||
result.append(d)
|
||||
|
||||
next_expected_date = get_next_expected_date(d[0], timegrain)
|
||||
|
||||
if i < len(data)-1:
|
||||
next_date = data[i+1][0]
|
||||
else:
|
||||
# already reached at end of data, see if we need any more dates
|
||||
next_date = getdate(nowdate())
|
||||
|
||||
# if next data point is earler than the expected date
|
||||
# need to fill out missing data points
|
||||
while next_date > next_expected_date:
|
||||
# fill missing value
|
||||
result.append([next_expected_date, 0.0])
|
||||
next_expected_date = get_next_expected_date(next_expected_date, timegrain)
|
||||
|
||||
# add date for the last period (if missing)
|
||||
if result and get_period_ending(to_date, timegrain) > result[-1][0]:
|
||||
result.append([get_period_ending(to_date, timegrain), 0.0])
|
||||
data_index = 0
|
||||
if data:
|
||||
for i, d in enumerate(result):
|
||||
while data_index < len(data) and getdate(data[data_index][0]) <= d[0]:
|
||||
d[1] += data[data_index][1]
|
||||
data_index += 1
|
||||
|
||||
return result
|
||||
|
||||
|
|
@ -358,17 +299,12 @@ def get_period_ending(date, timegrain):
|
|||
return getdate(date)
|
||||
|
||||
def get_week_ending(date):
|
||||
# fun fact: week ends on the day before 1st Jan of the year.
|
||||
# for 2019 it is Monday
|
||||
# week starts on monday
|
||||
from datetime import timedelta
|
||||
start = date - timedelta(days = date.weekday())
|
||||
end = start + timedelta(days=6)
|
||||
|
||||
week_of_the_year = int(date.strftime('%U'))
|
||||
|
||||
if week_of_the_year == 52:
|
||||
date = add_to_date(date, years=1)
|
||||
# first day of next week
|
||||
date = add_to_date('{}-01-01'.format(date.year), weeks = (week_of_the_year%52) + 1)
|
||||
# last day of this week
|
||||
return add_to_date(date, days=-1)
|
||||
return end
|
||||
|
||||
def get_month_ending(date):
|
||||
month_of_the_year = int(date.strftime('%m'))
|
||||
|
|
|
|||
|
|
@ -17,10 +17,9 @@ class TestDashboardChart(unittest.TestCase):
|
|||
self.assertEqual(get_period_ending('2019-04-10', 'Daily'),
|
||||
getdate('2019-04-10'))
|
||||
|
||||
# fun fact: week ends on the day before 1st Jan of the year.
|
||||
# for 2019 it is Monday
|
||||
# week starts on monday
|
||||
self.assertEqual(get_period_ending('2019-04-10', 'Weekly'),
|
||||
getdate('2019-04-15'))
|
||||
getdate('2019-04-14'))
|
||||
|
||||
self.assertEqual(get_period_ending('2019-04-10', 'Monthly'),
|
||||
getdate('2019-04-30'))
|
||||
|
|
@ -133,6 +132,34 @@ class TestDashboardChart(unittest.TestCase):
|
|||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_weekly_dashboard_chart(self):
|
||||
insert_test_records()
|
||||
|
||||
if frappe.db.exists('Dashboard Chart', 'Test Weekly Dashboard Chart'):
|
||||
frappe.delete_doc('Dashboard Chart', 'Test Weekly Dashboard Chart')
|
||||
|
||||
frappe.get_doc(dict(
|
||||
doctype = 'Dashboard Chart',
|
||||
chart_name = 'Test Weekly Dashboard Chart',
|
||||
chart_type = 'Sum',
|
||||
document_type = 'Communication',
|
||||
based_on = 'communication_date',
|
||||
value_based_on = 'rating',
|
||||
timespan = 'Select Date Range',
|
||||
time_interval = 'Weekly',
|
||||
from_date = datetime(2018, 12, 30),
|
||||
to_date = datetime(2019, 1, 15),
|
||||
filters_json = '[]',
|
||||
timeseries = 1
|
||||
)).insert()
|
||||
|
||||
result = get(chart_name ='Test Weekly Dashboard Chart', refresh = 1)
|
||||
|
||||
self.assertEqual(result.get('datasets')[0].get('values'), [200.0, 400.0, 0.0])
|
||||
self.assertEqual(result.get('labels'), [formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')])
|
||||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_group_by_chart_type(self):
|
||||
if frappe.db.exists('Dashboard Chart', 'Test Group By Dashboard Chart'):
|
||||
frappe.delete_doc('Dashboard Chart', 'Test Group By Dashboard Chart')
|
||||
|
|
@ -155,17 +182,16 @@ class TestDashboardChart(unittest.TestCase):
|
|||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_dashboard_with_single_doctype(self):
|
||||
if frappe.db.exists('Dashboard Chart', 'Test Single DocType In Dashboard Chart'):
|
||||
frappe.delete_doc('Dashboard Chart', 'Test Single DocType In Dashboard Chart')
|
||||
def insert_test_records():
|
||||
create_new_communication(datetime(2019, 1, 10), 100)
|
||||
create_new_communication(datetime(2019, 1, 6), 200)
|
||||
create_new_communication(datetime(2019, 1, 8), 300)
|
||||
|
||||
chart_doc = frappe.get_doc(dict(
|
||||
doctype = 'Dashboard Chart',
|
||||
chart_name = 'Test Single DocType In Dashboard Chart',
|
||||
chart_type = 'Count',
|
||||
document_type = 'System Settings',
|
||||
group_by_based_on = 'Created On',
|
||||
filters_json = '{}',
|
||||
))
|
||||
|
||||
self.assertRaises(frappe.ValidationError, chart_doc.insert)
|
||||
def create_new_communication(date, rating):
|
||||
communication = {
|
||||
'doctype': 'Communication',
|
||||
'subject': 'Test Communication',
|
||||
'rating': rating,
|
||||
'communication_date': date
|
||||
}
|
||||
frappe.get_doc(communication).insert()
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from frappe.modules import load_doctype_module
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_submitted_linked_docs(doctype, name, docs=None):
|
||||
def get_submitted_linked_docs(doctype, name, docs=None, linked=None):
|
||||
"""
|
||||
Get all nested submitted linked doctype linkinfo
|
||||
|
||||
|
|
@ -31,12 +31,26 @@ def get_submitted_linked_docs(doctype, name, docs=None):
|
|||
if not docs:
|
||||
docs = []
|
||||
|
||||
if not linked:
|
||||
linked = {}
|
||||
|
||||
linkinfo = get_linked_doctypes(doctype)
|
||||
linked_docs = get_linked_docs(doctype, name, linkinfo)
|
||||
|
||||
link_count = 0
|
||||
for link_doctype, link_names in linked_docs.items():
|
||||
if link_doctype not in linked:
|
||||
linked[link_doctype] = []
|
||||
|
||||
for link in link_names:
|
||||
if link['name'] == name:
|
||||
continue
|
||||
|
||||
if linked and name in linked[link_doctype]:
|
||||
continue
|
||||
|
||||
linked[link_doctype].append(link['name'])
|
||||
|
||||
docinfo = link.update({"doctype": link_doctype})
|
||||
validated_doc = validate_linked_doc(docinfo)
|
||||
|
||||
|
|
@ -47,7 +61,7 @@ def get_submitted_linked_docs(doctype, name, docs=None):
|
|||
if link.name in [doc.get("name") for doc in docs]:
|
||||
continue
|
||||
|
||||
links = get_submitted_linked_docs(link_doctype, link.name, docs)
|
||||
links = get_submitted_linked_docs(link_doctype, link.name, docs, linked)
|
||||
docs.append({
|
||||
"doctype": link_doctype,
|
||||
"name": link.name,
|
||||
|
|
|
|||
|
|
@ -62,8 +62,16 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
|
|||
ljust_list(res, 6)
|
||||
|
||||
if report.custom_columns:
|
||||
# Original query columns, needed to reorder data as per custom columns
|
||||
query_columns = columns
|
||||
# Reordered columns
|
||||
columns = json.loads(report.custom_columns)
|
||||
|
||||
if report.report_type == 'Query Report':
|
||||
result = reorder_data_for_custom_columns(columns, query_columns, result)
|
||||
|
||||
result = add_data_to_custom_columns(columns, result)
|
||||
|
||||
if custom_columns:
|
||||
result = add_data_to_custom_columns(custom_columns, result)
|
||||
|
||||
|
|
@ -208,6 +216,23 @@ def add_data_to_custom_columns(columns, result):
|
|||
|
||||
return data
|
||||
|
||||
def reorder_data_for_custom_columns(custom_columns, columns, result):
|
||||
reordered_result = []
|
||||
columns = [col.split(":")[0] for col in columns]
|
||||
|
||||
for res in result:
|
||||
r = []
|
||||
for col in custom_columns:
|
||||
try:
|
||||
idx = columns.index(col.get("label"))
|
||||
r.append(res[idx])
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
reordered_result.append(r)
|
||||
|
||||
return reordered_result
|
||||
|
||||
def get_prepared_report_result(report, filters, dn="", user=None):
|
||||
latest_report_data = {}
|
||||
doc = None
|
||||
|
|
|
|||
|
|
@ -437,7 +437,7 @@ class Meta(Document):
|
|||
|
||||
if not self.custom:
|
||||
for hook in frappe.get_hooks("override_doctype_dashboards", {}).get(self.name, []):
|
||||
data = frappe.get_attr(hook)(data=data)
|
||||
data = frappe._dict(frappe.get_attr(hook)(data=data))
|
||||
|
||||
return data
|
||||
|
||||
|
|
|
|||
|
|
@ -278,6 +278,8 @@ frappe.patches.v13_0.set_path_for_homepage_in_web_page_view
|
|||
frappe.patches.v13_0.migrate_translation_column_data
|
||||
frappe.patches.v13_0.set_read_times
|
||||
frappe.patches.v13_0.remove_web_view
|
||||
frappe.patches.v13_0.set_unique_for_page_view
|
||||
frappe.patches.v13_0.remove_tailwind_from_page_builder
|
||||
frappe.patches.v13_0.rename_onboarding
|
||||
frappe.patches.v13_0.email_unsubscribe
|
||||
execute:frappe.delete_doc("Web Template", "Section with Left Image", force=1)
|
||||
|
|
|
|||
6
frappe/patches/v13_0/set_unique_for_page_view.py
Normal file
6
frappe/patches/v13_0/set_unique_for_page_view.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc('website', 'doctype', 'web_page_view', force=True)
|
||||
site_url = frappe.utils.get_site_url(frappe.local.site)
|
||||
frappe.db.sql("""UPDATE `tabWeb Page View` set is_unique=1 where referrer LIKE '%{0}%'""".format(site_url))
|
||||
|
|
@ -250,6 +250,8 @@
|
|||
"public/less/form_grid.less"
|
||||
],
|
||||
"js/form.min.js": [
|
||||
"public/js/frappe/form/templates/address_list.html",
|
||||
"public/js/frappe/form/templates/contact_list.html",
|
||||
"public/js/frappe/form/templates/print_layout.html",
|
||||
"public/js/frappe/form/templates/users_in_sidebar.html",
|
||||
"public/js/frappe/form/templates/set_sharing.html",
|
||||
|
|
|
|||
|
|
@ -129,7 +129,8 @@ frappe.ui.form.ControlMultiSelectPills = frappe.ui.form.ControlAutocomplete.exte
|
|||
get_data() {
|
||||
let data;
|
||||
if(this.df.get_data) {
|
||||
data = this.df.get_data();
|
||||
let txt = this.$input.val();
|
||||
data = this.df.get_data(txt);
|
||||
if (data && data.then) {
|
||||
data.then((r) => {
|
||||
this.set_data(r);
|
||||
|
|
|
|||
|
|
@ -1,110 +1,62 @@
|
|||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// MIT License. See license.txt
|
||||
|
||||
frappe.ui.form.MultiSelectDialog = Class.extend({
|
||||
init: function(opts) {
|
||||
/* Options: doctype, target, setters, get_query, action */
|
||||
$.extend(this, opts);
|
||||
|
||||
frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
||||
constructor(opts) {
|
||||
/* Options: doctype, target, setters, get_query, action, add_filters_group, data_fields, primary_action_label */
|
||||
Object.assign(this, opts);
|
||||
var me = this;
|
||||
if(this.doctype!="[Select]") {
|
||||
frappe.model.with_doctype(this.doctype, function(r) {
|
||||
if (this.doctype != "[Select]") {
|
||||
frappe.model.with_doctype(this.doctype, function () {
|
||||
me.make();
|
||||
});
|
||||
} else {
|
||||
this.make();
|
||||
}
|
||||
},
|
||||
make: function() {
|
||||
let me = this;
|
||||
}
|
||||
|
||||
make() {
|
||||
let me = this;
|
||||
this.page_length = 20;
|
||||
this.start = 0;
|
||||
let fields = this.get_primary_filters();
|
||||
|
||||
let fields = [
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("Search Term"),
|
||||
fieldname: "search_term"
|
||||
},
|
||||
{
|
||||
fieldtype: "Column Break"
|
||||
}
|
||||
];
|
||||
let count = 0;
|
||||
if(!this.date_field) {
|
||||
this.date_field = "transaction_date";
|
||||
}
|
||||
|
||||
// setters can be defined as a dict or a list of fields
|
||||
// setters define the additional filters that get applied
|
||||
// for selection
|
||||
|
||||
// CASE 1: DocType name and fieldname is the same, example "customer" and "customer"
|
||||
// setters define the filters applied in the modal
|
||||
// if the fieldnames and doctypes are consistently named,
|
||||
// pass a dict with the setter key and value, for example
|
||||
// {customer: [customer_name]}
|
||||
|
||||
// CASE 2: if the fieldname of the target is different,
|
||||
// then pass a list of fields with appropriate fieldname
|
||||
|
||||
if($.isArray(this.setters)) {
|
||||
for (let df of this.setters) {
|
||||
fields.push(df, {fieldtype: "Column Break"});
|
||||
}
|
||||
} else {
|
||||
Object.keys(this.setters).forEach(function(setter) {
|
||||
fields.push({
|
||||
fieldtype: me.target.fields_dict[setter].df.fieldtype,
|
||||
label: me.target.fields_dict[setter].df.label,
|
||||
fieldname: setter,
|
||||
options: me.target.fields_dict[setter].df.options,
|
||||
default: me.setters[setter]
|
||||
});
|
||||
if (count++ < Object.keys(me.setters).length) {
|
||||
fields.push({fieldtype: "Column Break"});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Make results area
|
||||
fields = fields.concat([
|
||||
{
|
||||
"fieldname":"date_range",
|
||||
"label": __("Date Range"),
|
||||
"fieldtype": "DateRange",
|
||||
},
|
||||
{ fieldtype: "Section Break" },
|
||||
{ fieldtype: "HTML", fieldname: "results_area" },
|
||||
{ fieldtype: "Button", fieldname: "more_btn", label: __("More"),
|
||||
click: function(){
|
||||
me.start += 20;
|
||||
frappe.flags.auto_scroll = true;
|
||||
me.get_results();
|
||||
{
|
||||
fieldtype: "Button", fieldname: "more_btn", label: __("More"),
|
||||
click: () => {
|
||||
this.start += 20;
|
||||
this.get_results();
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
let doctype_plural = !this.doctype.endsWith('y') ? this.doctype + 's'
|
||||
: this.doctype.slice(0, -1) + 'ies';
|
||||
// Custom Data Fields
|
||||
if (this.data_fields) {
|
||||
fields.push({ fieldtype: "Section Break" });
|
||||
fields = fields.concat(this.data_fields);
|
||||
}
|
||||
|
||||
let doctype_plural = this.doctype.plural();
|
||||
|
||||
this.dialog = new frappe.ui.Dialog({
|
||||
title: __("Select {0}", [(this.doctype=='[Select]') ? __("value") : __(doctype_plural)]),
|
||||
title: __("Select {0}", [(this.doctype == '[Select]') ? __("value") : __(doctype_plural)]),
|
||||
fields: fields,
|
||||
primary_action_label: __("Get Items"),
|
||||
primary_action_label: this.primary_action_label || __("Get Items"),
|
||||
secondary_action_label: __("Make {0}", [me.doctype]),
|
||||
primary_action: function() {
|
||||
me.action(me.get_checked_values(), me.args);
|
||||
primary_action: function () {
|
||||
let filters_data = me.get_custom_filters();
|
||||
me.action(me.get_checked_values(), cur_dialog.get_values(), me.args, filters_data);
|
||||
},
|
||||
secondary_action: function(e) {
|
||||
secondary_action: function (e) {
|
||||
// If user wants to close the modal
|
||||
if (e) {
|
||||
frappe.route_options = {};
|
||||
if($.isArray(me.setters)) {
|
||||
if (Array.isArray(me.setters)) {
|
||||
for (let df of me.setters) {
|
||||
frappe.route_options[df.fieldname] = me.dialog.fields_dict[df.fieldname].get_value() || undefined;
|
||||
}
|
||||
} else {
|
||||
Object.keys(me.setters).forEach(function(setter) {
|
||||
Object.keys(me.setters).forEach(function (setter) {
|
||||
frappe.route_options[setter] = me.dialog.fields_dict[setter].get_value() || undefined;
|
||||
});
|
||||
}
|
||||
|
|
@ -114,6 +66,10 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
|
|||
}
|
||||
});
|
||||
|
||||
if (this.add_filters_group) {
|
||||
this.make_filter_area();
|
||||
}
|
||||
|
||||
this.$parent = $(this.dialog.body);
|
||||
this.$wrapper = this.dialog.fields_dict.results_area.$wrapper.append(`<div class="results"
|
||||
style="border: 1px solid #d1d8dd; border-radius: 3px; height: 300px; overflow: auto;"></div>`);
|
||||
|
|
@ -126,9 +82,89 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
|
|||
this.bind_events();
|
||||
this.get_results();
|
||||
this.dialog.show();
|
||||
},
|
||||
}
|
||||
|
||||
bind_events: function() {
|
||||
get_primary_filters() {
|
||||
let fields = [];
|
||||
|
||||
let columns = new Array(3);
|
||||
|
||||
// Hack for three column layout
|
||||
// To add column break
|
||||
columns[0] = [
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("Search"),
|
||||
fieldname: "search_term"
|
||||
}
|
||||
];
|
||||
columns[1] = [];
|
||||
columns[2] = [];
|
||||
|
||||
Object.keys(this.setters).forEach((setter, index) => {
|
||||
let df_prop = frappe.meta.docfield_map[this.doctype][setter];
|
||||
|
||||
// Index + 1 to start filling from index 1
|
||||
// Since Search is a standrd field already pushed
|
||||
columns[(index + 1) % 3].push({
|
||||
fieldtype: df_prop.fieldtype,
|
||||
label: df_prop.label,
|
||||
fieldname: setter,
|
||||
options: df_prop.options,
|
||||
default: this.setters[setter]
|
||||
});
|
||||
});
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal
|
||||
if (Object.seal) {
|
||||
Object.seal(columns);
|
||||
// now a is a fixed-size array with mutable entries
|
||||
}
|
||||
|
||||
fields = [
|
||||
...columns[0],
|
||||
{ fieldtype: "Column Break" },
|
||||
...columns[1],
|
||||
{ fieldtype: "Column Break" },
|
||||
...columns[2],
|
||||
{ fieldtype: "Section Break", fieldname: "primary_filters_sb" }
|
||||
];
|
||||
|
||||
if (this.add_filters_group) {
|
||||
fields.push(
|
||||
{
|
||||
fieldtype: 'HTML',
|
||||
fieldname: 'filter_area',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
make_filter_area() {
|
||||
this.filter_group = new frappe.ui.FilterGroup({
|
||||
parent: this.dialog.get_field('filter_area').$wrapper,
|
||||
doctype: this.doctype,
|
||||
on_change: () => {
|
||||
this.get_results();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get_custom_filters() {
|
||||
if (this.add_filters_group && this.filter_group) {
|
||||
return this.filter_group.get_filters().reduce((acc, filter) => {
|
||||
return Object.assign(acc, {
|
||||
[filter[1]]: [filter[2], filter[3]]
|
||||
});
|
||||
}, {});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
bind_events() {
|
||||
let me = this;
|
||||
|
||||
this.$results.on('click', '.list-item-container', function (e) {
|
||||
|
|
@ -136,48 +172,44 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
|
|||
$(this).find(':checkbox').trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
this.$results.on('click', '.list-item--head :checkbox', (e) => {
|
||||
this.$results.find('.list-item-container .list-row-check')
|
||||
.prop("checked", ($(e.target).is(':checked')));
|
||||
});
|
||||
|
||||
this.$parent.find('.input-with-feedback').on('change', (e) => {
|
||||
this.$parent.find('.input-with-feedback').on('change', () => {
|
||||
frappe.flags.auto_scroll = false;
|
||||
this.get_results();
|
||||
});
|
||||
|
||||
this.$parent.find('[data-fieldname="date_range"]').on('blur', (e) => {
|
||||
frappe.flags.auto_scroll = false;
|
||||
this.get_results();
|
||||
});
|
||||
|
||||
this.$parent.find('[data-fieldname="search_term"]').on('input', (e) => {
|
||||
this.$parent.find('[data-fieldtype="Data"]').on('input', () => {
|
||||
var $this = $(this);
|
||||
clearTimeout($this.data('timeout'));
|
||||
$this.data('timeout', setTimeout(function() {
|
||||
$this.data('timeout', setTimeout(function () {
|
||||
frappe.flags.auto_scroll = false;
|
||||
me.empty_list();
|
||||
me.get_results();
|
||||
}, 300));
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
get_checked_values: function() {
|
||||
get_checked_values() {
|
||||
// Return name of checked value.
|
||||
return this.$results.find('.list-item-container').map(function() {
|
||||
if ($(this).find('.list-row-check:checkbox:checked').length > 0 ) {
|
||||
return this.$results.find('.list-item-container').map(function () {
|
||||
if ($(this).find('.list-row-check:checkbox:checked').length > 0) {
|
||||
return $(this).attr('data-item-name');
|
||||
}
|
||||
}).get();
|
||||
},
|
||||
}
|
||||
|
||||
get_checked_items: function() {
|
||||
get_checked_items() {
|
||||
// Return checked items with all the column values.
|
||||
let checked_values = this.get_checked_values();
|
||||
return this.results.filter(res => checked_values.includes(res.name));
|
||||
},
|
||||
}
|
||||
|
||||
make_list_row: function(result={}) {
|
||||
make_list_row(result = {}) {
|
||||
var me = this;
|
||||
// Make a head row by default (if result not passed)
|
||||
let head = Object.keys(result).length === 0;
|
||||
|
|
@ -185,26 +217,17 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
|
|||
let contents = ``;
|
||||
let columns = ["name"];
|
||||
|
||||
if($.isArray(this.setters)) {
|
||||
for (let df of this.setters) {
|
||||
columns.push(df.fieldname);
|
||||
}
|
||||
} else {
|
||||
columns = columns.concat(Object.keys(this.setters));
|
||||
}
|
||||
columns.push("Date");
|
||||
columns = columns.concat(Object.keys(this.setters));
|
||||
|
||||
columns.forEach(function(column) {
|
||||
columns.forEach(function (column) {
|
||||
contents += `<div class="list-item__content ellipsis">
|
||||
${
|
||||
head ? `<span class="ellipsis">${__(frappe.model.unscrub(column))}</span>`
|
||||
|
||||
: (column !== "name" ? `<span class="ellipsis">${__(result[column])}</span>`
|
||||
: `<a href="${"#Form/"+ me.doctype + "/" + result[column]}" class="list-id ellipsis">
|
||||
${__(result[column])}</a>`)
|
||||
}
|
||||
head ? `<span class="ellipsis text-muted" title="${__(frappe.model.unscrub(column))}">${__(frappe.model.unscrub(column))}</span>`
|
||||
: (column !== "name" ? `<span class="ellipsis result-row" title="${__(result[column] || '')}">${__(result[column] || '')}</span>`
|
||||
: `<a href="${"#Form/" + me.doctype + "/" + result[column] || ''}" class="list-id ellipsis" title="${__(result[column] || '')}">
|
||||
${__(result[column] || '')}</a>`)}
|
||||
</div>`;
|
||||
})
|
||||
});
|
||||
|
||||
let $row = $(`<div class="list-item">
|
||||
<div class="list-item__content" style="flex: 0 0 10px;">
|
||||
|
|
@ -215,10 +238,12 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
|
|||
|
||||
head ? $row.addClass('list-item--head')
|
||||
: $row = $(`<div class="list-item-container" data-item-name="${result.name}"></div>`).append($row);
|
||||
return $row;
|
||||
},
|
||||
|
||||
render_result_list: function(results, more = 0, empty=true) {
|
||||
$(".modal-dialog .list-item--head").css("z-index", 0);
|
||||
return $row;
|
||||
}
|
||||
|
||||
render_result_list(results, more = 0, empty = true) {
|
||||
var me = this;
|
||||
var more_btn = me.dialog.fields_dict.more_btn.$wrapper;
|
||||
|
||||
|
|
@ -240,44 +265,44 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
|
|||
});
|
||||
|
||||
if (frappe.flags.auto_scroll) {
|
||||
this.$results.animate({scrollTop: me.$results.prop('scrollHeight')}, 500);
|
||||
this.$results.animate({ scrollTop: me.$results.prop('scrollHeight') }, 500);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
empty_list: function() {
|
||||
empty_list() {
|
||||
// Store all checked items
|
||||
let checked = this.get_checked_items().map(item => {
|
||||
return {
|
||||
...item,
|
||||
checked: true
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Remove **all** items
|
||||
this.$results.find('.list-item-container').remove();
|
||||
|
||||
// Rerender checked items
|
||||
this.render_result_list(checked, 0, false);
|
||||
},
|
||||
}
|
||||
|
||||
get_results: function() {
|
||||
get_results() {
|
||||
let me = this;
|
||||
let filters = this.get_query ? this.get_query().filters : {} || {};
|
||||
let filter_fields = [];
|
||||
|
||||
let filters = this.get_query ? this.get_query().filters : {};
|
||||
let filter_fields = [me.date_field];
|
||||
if($.isArray(this.setters)) {
|
||||
for (let df of this.setters) {
|
||||
filters[df.fieldname] = me.dialog.fields_dict[df.fieldname].get_value() || undefined;
|
||||
me.args[df.fieldname] = filters[df.fieldname];
|
||||
filter_fields.push(df.fieldname);
|
||||
}
|
||||
} else {
|
||||
Object.keys(this.setters).forEach(function(setter) {
|
||||
filters[setter] = me.dialog.fields_dict[setter].get_value() || undefined;
|
||||
Object.keys(this.setters).forEach(function (setter) {
|
||||
var value = me.dialog.fields_dict[setter].get_value();
|
||||
if (me.dialog.fields_dict[setter].df.fieldtype == "Data" && value) {
|
||||
filters[setter] = ["like", "%" + value + "%"];
|
||||
} else {
|
||||
filters[setter] = value || undefined;
|
||||
me.args[setter] = filters[setter];
|
||||
filter_fields.push(setter);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let date_val = this.dialog.fields_dict["date_range"].get_value();
|
||||
if(date_val) {
|
||||
filters[this.date_field] = ['between', date_val];
|
||||
}
|
||||
let filter_group = this.get_custom_filters();
|
||||
Object.assign(filters, filter_group);
|
||||
|
||||
let args = {
|
||||
doctype: me.doctype,
|
||||
|
|
@ -288,13 +313,13 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
|
|||
page_length: this.page_length + 1,
|
||||
query: this.get_query ? this.get_query().query : '',
|
||||
as_dict: 1
|
||||
}
|
||||
};
|
||||
frappe.call({
|
||||
type: "GET",
|
||||
method:'frappe.desk.search.search_widget',
|
||||
method: 'frappe.desk.search.search_widget',
|
||||
no_spinner: true,
|
||||
args: args,
|
||||
callback: function(r) {
|
||||
callback: function (r) {
|
||||
let more = 0;
|
||||
me.results = [];
|
||||
if (r.values.length) {
|
||||
|
|
@ -302,30 +327,13 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
|
|||
r.values.pop();
|
||||
more = 1;
|
||||
}
|
||||
r.values.forEach(function(result) {
|
||||
if(me.date_field in result) {
|
||||
result["Date"] = result[me.date_field]
|
||||
}
|
||||
r.values.forEach(function (result) {
|
||||
result.checked = 0;
|
||||
result.parsed_date = Date.parse(result["Date"]);
|
||||
me.results.push(result);
|
||||
});
|
||||
me.results.map( (result) => {
|
||||
result["Date"] = frappe.format(result["Date"], {"fieldtype":"Date"});
|
||||
})
|
||||
|
||||
me.results.sort((a, b) => {
|
||||
return a.parsed_date - b.parsed_date;
|
||||
});
|
||||
|
||||
// Preselect oldest entry
|
||||
if (me.start < 1 && r.values.length === 1) {
|
||||
me.results[0].checked = 1;
|
||||
}
|
||||
}
|
||||
me.render_result_list(me.results, more);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
}
|
||||
};
|
||||
22
frappe/public/js/frappe/form/templates/address_list.html
Normal file
22
frappe/public/js/frappe/form/templates/address_list.html
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<div class="clearfix"></div>
|
||||
{% for(var i=0, l=addr_list.length; i<l; i++) { %}
|
||||
<div class="address-box">
|
||||
<p class="h6">
|
||||
{%= i+1 %}. {%= addr_list[i].address_title %}{% if(addr_list[i].address_type!="Other") { %}
|
||||
<span class="text-muted">({%= __(addr_list[i].address_type) %})</span>{% } %}
|
||||
{% if(addr_list[i].is_primary_address) { %}
|
||||
<span class="text-muted">({%= __("Primary") %})</span>{% } %}
|
||||
{% if(addr_list[i].is_shipping_address) { %}
|
||||
<span class="text-muted">({%= __("Shipping") %})</span>{% } %}
|
||||
|
||||
<a href="#Form/Address/{%= encodeURIComponent(addr_list[i].name) %}" class="btn btn-default btn-xs pull-right"
|
||||
style="margin-top:-3px; margin-right: -5px;">
|
||||
{%= __("Edit") %}</a>
|
||||
</p>
|
||||
<p>{%= addr_list[i].display %}</p>
|
||||
</div>
|
||||
{% } %}
|
||||
{% if(!addr_list.length) { %}
|
||||
<p class="text-muted small">{%= __("No address added yet.") %}</p>
|
||||
{% } %}
|
||||
<p><button class="btn btn-xs btn-default btn-address">{{ __("New Address") }}</button></p>
|
||||
54
frappe/public/js/frappe/form/templates/contact_list.html
Normal file
54
frappe/public/js/frappe/form/templates/contact_list.html
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<div class="clearfix"></div>
|
||||
{% for(var i=0, l=contact_list.length; i<l; i++) { %}
|
||||
<div class="address-box">
|
||||
<p class="h6">
|
||||
{%= contact_list[i].first_name %} {%= contact_list[i].last_name %}
|
||||
{% if(contact_list[i].is_primary_contact) { %}
|
||||
<span class="text-muted">({%= __("Primary") %})</span>
|
||||
{% } %}
|
||||
{% if(contact_list[i].designation){ %}
|
||||
<span class="text-muted">– {%= contact_list[i].designation %}</span>
|
||||
{% } %}
|
||||
<a href="#Form/Contact/{%= encodeURIComponent(contact_list[i].name) %}"
|
||||
class="btn btn-xs btn-default pull-right"
|
||||
style="margin-top:-3px; margin-right: -5px;">
|
||||
{%= __("Edit") %}</a>
|
||||
</p>
|
||||
{% if (contact_list[i].phones || contact_list[i].email_ids) { %}
|
||||
<p>
|
||||
{% if(contact_list[i].phone) { %}
|
||||
{%= __("Phone") %}: {%= contact_list[i].phone %}<span class="text-muted"> ({%= __("Primary") %})</span><br>
|
||||
{% endif %}
|
||||
{% if(contact_list[i].mobile_no) { %}
|
||||
{%= __("Mobile No") %}: {%= contact_list[i].mobile_no %}<span class="text-muted"> ({%= __("Primary") %})</span><br>
|
||||
{% endif %}
|
||||
{% if(contact_list[i].phone_nos) { %}
|
||||
{% for(var j=0, k=contact_list[i].phone_nos.length; j<k; j++) { %}
|
||||
{%= __("Phone") %}: {%= contact_list[i].phone_nos[j].phone %}<br>
|
||||
{% } %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
{% if(contact_list[i].email_id) { %}
|
||||
{%= __("Email") %}: {%= contact_list[i].email_id %}<span class="text-muted"> ({%= __("Primary") %})</span><br>
|
||||
{% endif %}
|
||||
{% if(contact_list[i].email_ids) { %}
|
||||
{% for(var j=0, k=contact_list[i].email_ids.length; j<k; j++) { %}
|
||||
{%= __("Email") %}: {%= contact_list[i].email_ids[j].email_id %}<br>
|
||||
{% } %}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p>
|
||||
{% if (contact_list[i].address) { %}
|
||||
{%= __("Address") %}: {%= contact_list[i].address %}<br>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% } %}
|
||||
{% if(!contact_list.length) { %}
|
||||
<p class="text-muted small">{%= __("No contacts added yet.") %}</p>
|
||||
{% } %}
|
||||
<p><button class="btn btn-xs btn-default btn-contact">
|
||||
{{ __("New Contact") }}</button>
|
||||
</p>
|
||||
|
|
@ -114,8 +114,8 @@ export default {
|
|||
{label: "Time", slug: "time", sortable: true},
|
||||
],
|
||||
query: {
|
||||
sort: "time",
|
||||
order: "asc",
|
||||
sort: "duration",
|
||||
order: "desc",
|
||||
filters: {},
|
||||
pagination: {
|
||||
limit: 20,
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
<span class="octicon octicon-triangle-down"></span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-in-grid" v-if="showing == call.index">
|
||||
<div class="recorder-form-in-grid" v-if="showing == call.index">
|
||||
<div class="grid-form-heading" @click="showing = null">
|
||||
<div class="toolbar grid-header-toolbar">
|
||||
<span class="panel-title">SQL Query #<span class="grid-form-row-index">{{ call.index }}</span></span>
|
||||
|
|
@ -216,8 +216,8 @@ export default {
|
|||
{label: "Exact Copies", slug: "exact_copies", sortable: true},
|
||||
],
|
||||
query: {
|
||||
sort: "index",
|
||||
order: "asc",
|
||||
sort: "duration",
|
||||
order: "desc",
|
||||
pagination: {
|
||||
limit: 20,
|
||||
page: 1,
|
||||
|
|
|
|||
|
|
@ -823,3 +823,115 @@ if (!Array.prototype.uniqBy) {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Pluralize
|
||||
String.prototype.plural = function(revert) {
|
||||
const plural = {
|
||||
"(quiz)$": "$1zes",
|
||||
"^(ox)$": "$1en",
|
||||
"([m|l])ouse$": "$1ice",
|
||||
"(matr|vert|ind)ix|ex$": "$1ices",
|
||||
"(x|ch|ss|sh)$": "$1es",
|
||||
"([^aeiouy]|qu)y$": "$1ies",
|
||||
"(hive)$": "$1s",
|
||||
"(?:([^f])fe|([lr])f)$": "$1$2ves",
|
||||
"(shea|lea|loa|thie)f$": "$1ves",
|
||||
sis$: "ses",
|
||||
"([ti])um$": "$1a",
|
||||
"(tomat|potat|ech|her|vet)o$": "$1oes",
|
||||
"(bu)s$": "$1ses",
|
||||
"(alias)$": "$1es",
|
||||
"(octop)us$": "$1i",
|
||||
"(ax|test)is$": "$1es",
|
||||
"(us)$": "$1es",
|
||||
"([^s]+)$": "$1s",
|
||||
};
|
||||
|
||||
const singular = {
|
||||
"(quiz)zes$": "$1",
|
||||
"(matr)ices$": "$1ix",
|
||||
"(vert|ind)ices$": "$1ex",
|
||||
"^(ox)en$": "$1",
|
||||
"(alias)es$": "$1",
|
||||
"(octop|vir)i$": "$1us",
|
||||
"(cris|ax|test)es$": "$1is",
|
||||
"(shoe)s$": "$1",
|
||||
"(o)es$": "$1",
|
||||
"(bus)es$": "$1",
|
||||
"([m|l])ice$": "$1ouse",
|
||||
"(x|ch|ss|sh)es$": "$1",
|
||||
"(m)ovies$": "$1ovie",
|
||||
"(s)eries$": "$1eries",
|
||||
"([^aeiouy]|qu)ies$": "$1y",
|
||||
"([lr])ves$": "$1f",
|
||||
"(tive)s$": "$1",
|
||||
"(hive)s$": "$1",
|
||||
"(li|wi|kni)ves$": "$1fe",
|
||||
"(shea|loa|lea|thie)ves$": "$1f",
|
||||
"(^analy)ses$": "$1sis",
|
||||
"((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$":
|
||||
"$1$2sis",
|
||||
"([ti])a$": "$1um",
|
||||
"(n)ews$": "$1ews",
|
||||
"(h|bl)ouses$": "$1ouse",
|
||||
"(corpse)s$": "$1",
|
||||
"(us)es$": "$1",
|
||||
s$: "",
|
||||
};
|
||||
|
||||
const irregular = {
|
||||
move: "moves",
|
||||
foot: "feet",
|
||||
goose: "geese",
|
||||
sex: "sexes",
|
||||
child: "children",
|
||||
man: "men",
|
||||
tooth: "teeth",
|
||||
person: "people",
|
||||
};
|
||||
|
||||
const uncountable = [
|
||||
"sheep",
|
||||
"fish",
|
||||
"deer",
|
||||
"moose",
|
||||
"series",
|
||||
"species",
|
||||
"money",
|
||||
"rice",
|
||||
"information",
|
||||
"equipment",
|
||||
];
|
||||
|
||||
// save some time in the case that singular and plural are the same
|
||||
if (uncountable.indexOf(this.toLowerCase()) >= 0) return this;
|
||||
|
||||
// check for irregular forms
|
||||
let word;
|
||||
let pattern;
|
||||
let replace;
|
||||
for (word in irregular) {
|
||||
if (revert) {
|
||||
pattern = new RegExp(irregular[word] + "$", "i");
|
||||
replace = word;
|
||||
} else {
|
||||
pattern = new RegExp(word + "$", "i");
|
||||
replace = irregular[word];
|
||||
}
|
||||
if (pattern.test(this)) return this.replace(pattern, replace);
|
||||
}
|
||||
|
||||
let array;
|
||||
if (revert) array = singular;
|
||||
else array = plural;
|
||||
|
||||
// check for matches using regular expressions
|
||||
let reg;
|
||||
for (reg in array) {
|
||||
pattern = new RegExp(reg, "i");
|
||||
|
||||
if (pattern.test(this)) return this.replace(pattern, array[reg]);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
|
@ -1,23 +1,38 @@
|
|||
frappe.ui.form.on('Web Page Block', {
|
||||
frappe.ui.form.on("Web Page Block", {
|
||||
edit_values(frm, cdt, cdn) {
|
||||
let row = frm.selected_doc;
|
||||
frappe.model.with_doc('Web Template', row.web_template).then(doc => {
|
||||
frappe.model.with_doc("Web Template", row.web_template).then((doc) => {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __('Edit Values'),
|
||||
fields: doc.fields,
|
||||
title: __("Edit Values"),
|
||||
fields: doc.fields.map((df) => {
|
||||
if (df.fieldtype == "Section Break") {
|
||||
df.collapsible = 1;
|
||||
}
|
||||
return df;
|
||||
}),
|
||||
primary_action(values) {
|
||||
frappe.model.set_value(
|
||||
cdt,
|
||||
cdn,
|
||||
'web_template_values',
|
||||
"web_template_values",
|
||||
JSON.stringify(values)
|
||||
);
|
||||
d.hide();
|
||||
}
|
||||
},
|
||||
});
|
||||
let values = JSON.parse(row.web_template_values || '{}');
|
||||
let values = JSON.parse(row.web_template_values || "{}");
|
||||
d.set_values(values);
|
||||
d.show();
|
||||
|
||||
d.sections.forEach((sect) => {
|
||||
let fields_with_value = sect.fields_list.filter(
|
||||
(field) => values[field.df.fieldname]
|
||||
);
|
||||
|
||||
if (fields_with_value.length) {
|
||||
sect.collapse(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -1258,7 +1258,8 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
|
|||
},
|
||||
{
|
||||
label: __('Toggle Sidebar'),
|
||||
action: () => this.toggle_side_bar()
|
||||
action: () => this.toggle_side_bar(),
|
||||
shortcut: 'Ctrl+K',
|
||||
},
|
||||
{
|
||||
label: __('Pick Columns'),
|
||||
|
|
|
|||
|
|
@ -247,14 +247,23 @@
|
|||
}
|
||||
}
|
||||
|
||||
.form-in-grid {
|
||||
.base-grid() {
|
||||
background-color: white;
|
||||
z-index: 1021;
|
||||
position: relative;
|
||||
.transition(opacity .2s ease)
|
||||
}
|
||||
|
||||
.form-in-grid {
|
||||
overflow: hidden;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
.transition(opacity .2s ease)
|
||||
z-index: 1021;
|
||||
.base-grid();
|
||||
}
|
||||
|
||||
.recorder-form-in-grid {
|
||||
z-index: 0;
|
||||
.base-grid();
|
||||
}
|
||||
|
||||
.grid-row-open .form-in-grid {
|
||||
|
|
|
|||
|
|
@ -115,3 +115,10 @@
|
|||
border-radius: 0.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
// apply margin on first h1 if container is full width without top margin
|
||||
main:not(.my-5) .from-markdown {
|
||||
h1:first-child {
|
||||
margin-top: 5rem;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -250,3 +250,35 @@
|
|||
left: 16.67%;
|
||||
}
|
||||
}
|
||||
|
||||
.testimonial {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.testimonial-logo img {
|
||||
display: inline-block;
|
||||
max-width: 10rem;
|
||||
max-height: 2.5rem;
|
||||
}
|
||||
|
||||
.testimonial-content {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 2rem;
|
||||
max-width: 52rem;
|
||||
font-size: $font-size-2xl;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.testimonial-by {
|
||||
font-size: $font-size-lg;
|
||||
margin-top: 2rem;
|
||||
|
||||
&:before {
|
||||
content: '—'
|
||||
}
|
||||
}
|
||||
|
||||
.split-section-content {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
|
|
|||
18
frappe/public/scss/sidebar.scss
Normal file
18
frappe/public/scss/sidebar.scss
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
.web-sidebar {
|
||||
padding-top: 2rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.sidebar-item a {
|
||||
display: block;
|
||||
padding: 0.25rem 0;
|
||||
font-size: $font-size-sm;
|
||||
color: $gray-700;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sidebar-item a.active {
|
||||
color: $primary;
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
@import 'website-image';
|
||||
@import 'page-builder';
|
||||
@import 'markdown';
|
||||
@import 'sidebar';
|
||||
|
||||
.container {
|
||||
padding-left: 1.25rem;
|
||||
|
|
@ -55,6 +56,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
img {
|
||||
display: inline-block;
|
||||
max-width: 150px;
|
||||
max-height: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
|
@ -135,6 +144,7 @@ a.card {
|
|||
.footer-logo {
|
||||
width: 5rem;
|
||||
height: 2rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.footer-link, .footer-child-item a {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
<nav class="navbar navbar-light navbar-expand-lg">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{{ url_prefix }}{{ home_page or "/" }}">
|
||||
<span>{{ brand_html or (frappe.get_hooks("brand_html") or [_("Home")])[0] }}</span>
|
||||
{%- if brand_html -%}
|
||||
{{ brand_html }}
|
||||
{%- elif banner_image -%}
|
||||
<img src='{{ banner_image }}'>
|
||||
{%- else -%}
|
||||
<span>{{ (frappe.get_hooks("brand_html") or [_("Home")])[0] }}</span>
|
||||
{%- endif -%}
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button"
|
||||
data-toggle="collapse"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!-- post login tools -->
|
||||
{% if not only_static %}
|
||||
{% if not only_static and not hide_login %}
|
||||
|
||||
{% if frappe.session.user != 'Guest' %}
|
||||
<li class="nav-item dropdown logged-in" id="website-post-login" data-label="website-post-login" style="display: none">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<div class="web-sidebar">
|
||||
<div class="sidebar-items small">
|
||||
<div class="sidebar-items">
|
||||
<ul class="list-unstyled">
|
||||
{% if sidebar_title %}
|
||||
<li class="title">
|
||||
|
|
@ -7,9 +7,10 @@
|
|||
</li>
|
||||
{% endif %}
|
||||
{% for item in sidebar_items -%}
|
||||
<li class="sidebar-item my-2">
|
||||
<li class="sidebar-item">
|
||||
{% if item.type != 'input' %}
|
||||
<a href="{{ item.route }}" class="{{ 'text-dark' if pathname==item.route else 'text-muted' }}"
|
||||
{%- set item_route = item.route[1:] if item.route[0] == '/' else item.route -%}
|
||||
<a href="{{ item.route }}" class="{{ 'active' if pathname == item_route else '' }}"
|
||||
{% if item.target %}target="{{ item.target }}"{% endif %}>
|
||||
{{ _(item.title or item.label) }}
|
||||
</a>
|
||||
|
|
@ -23,7 +24,7 @@
|
|||
{%- endfor %}
|
||||
{% if frappe.user != 'Guest' %}
|
||||
<li class="sidebar-item">
|
||||
<a href="/me" class="text-muted">{{ _("My Account") }}</a>
|
||||
<a href="/me">{{ _("My Account") }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
|
@ -33,8 +34,8 @@
|
|||
<script>
|
||||
frappe.ready(function() {
|
||||
$('.sidebar-item a').each(function(index) {
|
||||
const active_class = 'text-dark font-weight-bold'
|
||||
const non_active_class = 'text-muted'
|
||||
const active_class = 'active'
|
||||
const non_active_class = ''
|
||||
if(this.href.trim() == window.location) {
|
||||
$(this).removeClass(non_active_class).addClass(active_class);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -48,17 +48,27 @@ id="page-{{ name or route | e }}" data-path="{{ pathname | e }}"
|
|||
{%- if source_content_type %}source-content-type="{{ source_content_type }}"{%- endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro sidebar() %}
|
||||
<div class="sidebar-column col-sm-{{ columns }}">
|
||||
{% block page_sidebar %}
|
||||
{% include "templates/includes/web_sidebar.html" %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% if show_sidebar %}
|
||||
<div class="container">
|
||||
<div class="row" {{ container_attributes() }}>
|
||||
<div class="pt-4 col-sm-2 border-right sidebar-column d-none d-sm-block">
|
||||
{% block page_sidebar %}
|
||||
{% include "templates/includes/web_sidebar.html" %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div class="col-sm-10 main-column">
|
||||
{%- set columns = sidebar_columns or 2 -%}
|
||||
{%- if not sidebar_right -%}
|
||||
{{ sidebar() }}
|
||||
{%- endif -%}
|
||||
<div class="main-column col-sm-{{ 12 - columns }}">
|
||||
{{ main_content() }}
|
||||
</div>
|
||||
{%- if sidebar_right -%}
|
||||
{{ sidebar() }}
|
||||
{%- endif -%}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
|
|
|||
|
|
@ -481,7 +481,7 @@ def watch(path, handler=None, debug=True):
|
|||
observer.join()
|
||||
|
||||
def markdown(text, sanitize=True, linkify=True):
|
||||
html = frappe.utils.md_to_html(text)
|
||||
html = text if is_html(text) else frappe.utils.md_to_html(text)
|
||||
|
||||
if sanitize:
|
||||
html = html.replace("<!-- markdown -->", "")
|
||||
|
|
|
|||
|
|
@ -116,12 +116,12 @@ def execute_job(site, method, event, job_name, kwargs, user=None, is_async=True,
|
|||
is_async=is_async, retry=retry+1)
|
||||
|
||||
else:
|
||||
frappe.log_error(method_name)
|
||||
frappe.log_error(title=method_name)
|
||||
raise
|
||||
|
||||
except:
|
||||
frappe.db.rollback()
|
||||
frappe.log_error(method_name)
|
||||
frappe.log_error(title=method_name)
|
||||
frappe.db.commit()
|
||||
print(frappe.get_traceback())
|
||||
raise
|
||||
|
|
|
|||
|
|
@ -3,18 +3,18 @@
|
|||
|
||||
"""This module handles the On Demand Backup utility"""
|
||||
|
||||
from __future__ import unicode_literals, print_function
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
#Imports
|
||||
from frappe import _
|
||||
import os, frappe
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
import frappe
|
||||
from frappe import _, conf
|
||||
from frappe.utils import cstr, get_url, now_datetime
|
||||
|
||||
#Global constants
|
||||
verbose = 0
|
||||
from frappe import conf
|
||||
#-------------------------------------------------------------------------------
|
||||
_verbose = False
|
||||
|
||||
|
||||
class BackupGenerator:
|
||||
"""
|
||||
This class contains methods to perform On Demand Backup
|
||||
|
|
@ -23,7 +23,8 @@ class BackupGenerator:
|
|||
If specifying db_file_name, also append ".sql.gz"
|
||||
"""
|
||||
def __init__(self, db_name, user, password, backup_path_db=None, backup_path_files=None,
|
||||
backup_path_private_files=None, db_host="localhost", db_port=3306):
|
||||
backup_path_private_files=None, db_host="localhost", db_port=3306, verbose=False):
|
||||
global _verbose
|
||||
self.db_host = db_host
|
||||
self.db_port = db_port or 3306
|
||||
self.db_name = db_name
|
||||
|
|
@ -32,6 +33,8 @@ class BackupGenerator:
|
|||
self.backup_path_files = backup_path_files
|
||||
self.backup_path_db = backup_path_db
|
||||
self.backup_path_private_files = backup_path_private_files
|
||||
self.verbose = verbose
|
||||
_verbose = verbose
|
||||
|
||||
def get_backup(self, older_than=24, ignore_files=False, force=False):
|
||||
"""
|
||||
|
|
@ -103,7 +106,7 @@ class BackupGenerator:
|
|||
cmd_string = """tar -cf %s %s""" % (backup_path, files_path)
|
||||
err, out = frappe.utils.execute_in_shell(cmd_string)
|
||||
|
||||
if verbose:
|
||||
if self.verbose:
|
||||
print('Backed up files', os.path.abspath(backup_path))
|
||||
|
||||
def take_dump(self):
|
||||
|
|
@ -159,21 +162,22 @@ def get_backup():
|
|||
recipient_list = odb.send_email()
|
||||
frappe.msgprint(_("Download link for your backup will be emailed on the following email address: {0}").format(', '.join(recipient_list)))
|
||||
|
||||
def scheduled_backup(older_than=6, ignore_files=False, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, force=False):
|
||||
def scheduled_backup(older_than=6, ignore_files=False, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, force=False, verbose=False):
|
||||
"""this function is called from scheduler
|
||||
deletes backups older than 7 days
|
||||
takes backup"""
|
||||
odb = new_backup(older_than, ignore_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, force=force)
|
||||
odb = new_backup(older_than, ignore_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, force=force, verbose=verbose)
|
||||
return odb
|
||||
|
||||
def new_backup(older_than=6, ignore_files=False, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, force=False):
|
||||
def new_backup(older_than=6, ignore_files=False, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, force=False, verbose=False):
|
||||
delete_temp_backups(older_than = frappe.conf.keep_backups_for_hours or 24)
|
||||
odb = BackupGenerator(frappe.conf.db_name, frappe.conf.db_name,\
|
||||
frappe.conf.db_password,
|
||||
backup_path_db=backup_path_db, backup_path_files=backup_path_files,
|
||||
backup_path_private_files=backup_path_private_files,
|
||||
db_host = frappe.db.host,
|
||||
db_port = frappe.db.port)
|
||||
db_port = frappe.db.port,
|
||||
verbose=verbose)
|
||||
odb.get_backup(older_than, ignore_files, force=force)
|
||||
return odb
|
||||
|
||||
|
|
@ -202,20 +206,22 @@ def is_file_old(db_file_name, older_than=24):
|
|||
file_datetime = datetime.fromtimestamp\
|
||||
(os.stat(db_file_name).st_ctime)
|
||||
if datetime.today() - file_datetime >= timedelta(hours = older_than):
|
||||
if verbose: print("File is old")
|
||||
if _verbose:
|
||||
print("File is old")
|
||||
return True
|
||||
else:
|
||||
if verbose: print("File is recent")
|
||||
if _verbose:
|
||||
print("File is recent")
|
||||
return False
|
||||
else:
|
||||
if verbose: print("File does not exist")
|
||||
if _verbose:
|
||||
print("File does not exist")
|
||||
return True
|
||||
|
||||
def get_backup_path():
|
||||
backup_path = frappe.utils.get_site_path(conf.get("backup_path", "private/backups"))
|
||||
return backup_path
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
def backup(with_files=False, backup_path_db=None, backup_path_files=None, quiet=False):
|
||||
"Backup"
|
||||
odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, force=True)
|
||||
|
|
@ -225,6 +231,7 @@ def backup(with_files=False, backup_path_db=None, backup_path_files=None, quiet=
|
|||
"backup_path_private_files": odb.backup_path_private_files
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
"""
|
||||
is_file_old db_name user password db_host
|
||||
|
|
|
|||
|
|
@ -190,6 +190,10 @@ def get_first_day(dt, d_years=0, d_months=0):
|
|||
def get_first_day_of_week(dt):
|
||||
return dt - datetime.timedelta(days=dt.weekday())
|
||||
|
||||
def get_last_day_of_week(dt):
|
||||
dt = get_first_day_of_week(dt)
|
||||
return dt + datetime.timedelta(days=6)
|
||||
|
||||
def get_last_day(dt):
|
||||
"""
|
||||
Returns last day of the month using:
|
||||
|
|
@ -348,7 +352,7 @@ def flt(s, precision=None):
|
|||
if precision is not None:
|
||||
num = rounded(num, precision)
|
||||
except Exception:
|
||||
num = 0
|
||||
num = 0.0
|
||||
|
||||
return num
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import frappe
|
|||
import frappe.defaults
|
||||
import datetime
|
||||
from frappe.utils import get_datetime
|
||||
from frappe.utils import add_to_date, getdate
|
||||
from frappe.utils.data import get_last_day_of_week
|
||||
from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_period_ending
|
||||
from six import string_types
|
||||
|
||||
# global values -- used for caching
|
||||
|
|
@ -73,3 +76,30 @@ def datetime_in_user_format(date_time):
|
|||
date_time = get_datetime(date_time)
|
||||
from frappe.utils import formatdate
|
||||
return formatdate(date_time.date()) + " " + date_time.strftime("%H:%M")
|
||||
|
||||
def get_dates_from_timegrain(from_date, to_date, timegrain="Daily"):
|
||||
from_date = getdate(from_date)
|
||||
to_date = getdate(to_date)
|
||||
|
||||
days = months = years = 0
|
||||
if "Daily" == timegrain:
|
||||
days = 1
|
||||
elif "Weekly" == timegrain:
|
||||
days = 7
|
||||
elif "Monthly" == timegrain:
|
||||
months = 1
|
||||
elif "Quarterly" == timegrain:
|
||||
months = 3
|
||||
|
||||
if "Weekly" == timegrain:
|
||||
dates = [get_last_day_of_week(from_date)]
|
||||
else:
|
||||
dates = [get_period_ending(from_date, timegrain)]
|
||||
|
||||
while getdate(dates[-1]) < getdate(to_date):
|
||||
if "Weekly" == timegrain:
|
||||
date = get_last_day_of_week(add_to_date(dates[-1], years=years, months=months, days=days))
|
||||
else:
|
||||
date = get_period_ending(add_to_date(dates[-1], years=years, months=months, days=days), timegrain)
|
||||
dates.append(date)
|
||||
return dates
|
||||
|
|
@ -21,7 +21,8 @@ def get_jenv():
|
|||
jenv.globals.update(get_jenv_customization('methods'))
|
||||
jenv.globals.update({
|
||||
'resolve_class': resolve_class,
|
||||
'inspect': inspect
|
||||
'inspect': inspect,
|
||||
'web_blocks': web_blocks
|
||||
})
|
||||
|
||||
frappe.local.jenv = jenv
|
||||
|
|
@ -189,3 +190,30 @@ def inspect(var, render=True):
|
|||
else:
|
||||
html = ""
|
||||
return get_jenv().from_string(html).render(context)
|
||||
|
||||
def web_blocks(blocks):
|
||||
from frappe import get_doc
|
||||
from frappe.website.doctype.web_page.web_page import get_web_blocks_html
|
||||
|
||||
web_blocks = []
|
||||
for block in blocks:
|
||||
doc = {
|
||||
'doctype': 'Web Page Block',
|
||||
'web_template': block['template'],
|
||||
'web_template_values': block['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))
|
||||
|
||||
out = get_web_blocks_html(web_blocks)
|
||||
|
||||
html = out.html
|
||||
for script in out.scripts:
|
||||
html += '<script>{}</script>'.format(script)
|
||||
|
||||
return html
|
||||
|
|
|
|||
36
frappe/website/dashboard_fixtures.py
Normal file
36
frappe/website/dashboard_fixtures.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import frappe
|
||||
|
||||
def get_data():
|
||||
return frappe._dict({
|
||||
"dashboards": get_dashboards(),
|
||||
"charts": get_charts(),
|
||||
"number_cards": None,
|
||||
})
|
||||
|
||||
def get_dashboards():
|
||||
return [{
|
||||
"name": "Website",
|
||||
"dashboard_name": "Website",
|
||||
"charts": [
|
||||
{ "chart": "Website Analytics", "width": "Full" }
|
||||
]
|
||||
}]
|
||||
|
||||
def get_charts():
|
||||
return [{
|
||||
"chart_name": "Website Analytics",
|
||||
"chart_type": "Report",
|
||||
"custom_options": "{\"type\": \"line\", \"lineOptions\": {\"regionFill\": 1}, \"axisOptions\": {\"shortenYAxisNumbers\": 1}, \"tooltipOptions\": {}}",
|
||||
"doctype": "Dashboard Chart",
|
||||
"filters_json": "{}",
|
||||
"group_by_type": "Count",
|
||||
"is_custom": 1,
|
||||
"is_public": 1,
|
||||
"name": "Website Analytics",
|
||||
"number_of_groups": 0,
|
||||
"report_name": "Website Analytics",
|
||||
"time_interval": "Yearly",
|
||||
"timeseries": 0,
|
||||
"timespan": "Last Year",
|
||||
"type": "Line"
|
||||
}]
|
||||
|
|
@ -27,7 +27,11 @@
|
|||
}
|
||||
],
|
||||
"category": "Modules",
|
||||
"charts": [],
|
||||
"charts": [
|
||||
{
|
||||
"chart_name": "Website Analytics"
|
||||
}
|
||||
],
|
||||
"creation": "2020-03-02 14:13:51.089373",
|
||||
"developer_mode_only": 0,
|
||||
"disable_user_customization": 0,
|
||||
|
|
@ -37,7 +41,7 @@
|
|||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"label": "Website",
|
||||
"modified": "2020-04-26 13:03:49.094728",
|
||||
"modified": "2020-05-05 18:17:13.232473",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Website",
|
||||
|
|
|
|||
|
|
@ -48,3 +48,58 @@ frappe.ui.form.on('Web Page', {
|
|||
frappe.utils.set_meta_tag(frm.doc.route);
|
||||
}
|
||||
});
|
||||
|
||||
frappe.tour['Web Page'] = [
|
||||
{
|
||||
fieldname: "title",
|
||||
title: __("Title of the page"),
|
||||
description: __("This title will be used as the title of the webpage as well as in meta tags"),
|
||||
},
|
||||
{
|
||||
fieldname: "published",
|
||||
title: __("Makes the page public"),
|
||||
description: __("Checking this will publish the page on your website and it'll be visible to everyone."),
|
||||
},
|
||||
{
|
||||
fieldname: "route",
|
||||
title: __("URL of the page"),
|
||||
description: __("This will be automatically generated when you publish the page, you can also enter a route yourself if you wish"),
|
||||
},
|
||||
{
|
||||
fieldname: "content_type",
|
||||
title: __("Content type for building the page"),
|
||||
description: `${__('You can select one from the following,')} <br>
|
||||
<ul>
|
||||
<li><b>${__('Rich Text')}</b>: ${__('Standard rich text editor with controls')}</li>
|
||||
<li><b>${__('Markdown')}</b>: ${__('Github flavoured markdown syntax')}</li>
|
||||
<li><b>${__('HTML')}</b>: ${__('HTML with jinja support')}</li>
|
||||
<li><b>${__('Page Builder')}</b>: ${__('Frappe page builder using components')}</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
{
|
||||
fieldname: "insert_code",
|
||||
title: __("Client Script"),
|
||||
description: __("Checking this will show a text area where you can write custom javascript that will run on this page."),
|
||||
},
|
||||
{
|
||||
fieldname: "meta_title",
|
||||
title: __("Meta title for SEO"),
|
||||
description: __("By default the title is used as meta title, adding a value here will override it."),
|
||||
},
|
||||
{
|
||||
fieldname: "meta_title",
|
||||
title: __("Meta Title"),
|
||||
description: __("By default the title is used as meta title, adding a value here will override it."),
|
||||
},
|
||||
{
|
||||
fieldname: "meta_description",
|
||||
title: __("Meta Description"),
|
||||
description: __("The meta description is an HTML attribute that provides a brief summary of a web page. Search engines such as Google often display the meta description in search results, which can influence click-through rates.")
|
||||
},
|
||||
{
|
||||
fieldname: "meta_image",
|
||||
title: __("Meta Image"),
|
||||
description: __("The meta image is unique image representing the content of the page. Images for this Card should be at least 280px in width, and at least 150px in height.")
|
||||
},
|
||||
];
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"creation": "2020-04-15 22:54:46.009703",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
|
|
@ -9,7 +10,9 @@
|
|||
"referrer",
|
||||
"browser",
|
||||
"browser_version",
|
||||
"date"
|
||||
"is_unique",
|
||||
"time_zone",
|
||||
"user_agent"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -39,15 +42,24 @@
|
|||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Date",
|
||||
"set_only_once": 1
|
||||
"fieldname": "is_unique",
|
||||
"fieldtype": "Data",
|
||||
"label": "Is Unique"
|
||||
},
|
||||
{
|
||||
"fieldname": "time_zone",
|
||||
"fieldtype": "Data",
|
||||
"label": "Time Zone"
|
||||
},
|
||||
{
|
||||
"fieldname": "user_agent",
|
||||
"fieldtype": "Data",
|
||||
"label": "User Agent"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-15 23:31:27.517793",
|
||||
"modified": "2020-05-05 14:11:24.718770",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Web Page View",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
"depends_on": "eval:!doc.standard",
|
||||
"fieldname": "template",
|
||||
"fieldtype": "Code",
|
||||
"in_list_view": 1,
|
||||
"label": "Template",
|
||||
"options": "HTML"
|
||||
},
|
||||
|
|
@ -35,7 +34,7 @@
|
|||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-04-17 14:05:28.499020",
|
||||
"modified": "2020-05-15 17:50:51.856135",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Web Template",
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ frappe.ui.form.on('Website Settings', {
|
|||
if (!frm.doc.banner_image) {
|
||||
frappe.msgprint(__("Select a Brand Image first."));
|
||||
}
|
||||
frm.set_value("brand_html", "<img src='"+ frm.doc.banner_image
|
||||
+"' style='max-width: 150px;'>");
|
||||
frm.set_value("brand_html", "<img src='"+ frm.doc.banner_image + "'>");
|
||||
},
|
||||
|
||||
onload_post_render: function(frm) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"favicon",
|
||||
"top_bar",
|
||||
"navbar_search",
|
||||
"hide_login",
|
||||
"top_bar_items",
|
||||
"call_to_action",
|
||||
"call_to_action_url",
|
||||
|
|
@ -344,6 +345,12 @@
|
|||
"fieldname": "call_to_action_url",
|
||||
"fieldtype": "Data",
|
||||
"label": "Call To Action URL"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "hide_login",
|
||||
"fieldtype": "Check",
|
||||
"label": "Hide Login"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
|
|
@ -351,7 +358,7 @@
|
|||
"issingle": 1,
|
||||
"links": [],
|
||||
"max_attachments": 10,
|
||||
"modified": "2020-05-11 07:14:37.302357",
|
||||
"modified": "2020-05-15 14:12:32.907352",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Website Settings",
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ def get_website_settings():
|
|||
})
|
||||
|
||||
settings = frappe.get_single("Website Settings")
|
||||
for k in ["banner_html", "brand_html", "copyright", "twitter_share_via",
|
||||
for k in ["banner_html", "banner_image", "brand_html", "copyright", "twitter_share_via",
|
||||
"facebook_share", "google_plus_one", "twitter_share", "linked_in_share",
|
||||
"disable_signup", "hide_footer_signup", "head_html", "title_prefix",
|
||||
"navbar_search", "enable_view_tracking", "footer_logo", "call_to_action", "call_to_action_url"]:
|
||||
|
|
@ -156,6 +156,8 @@ def get_website_settings():
|
|||
if settings.favicon and settings.favicon != "attach_files:":
|
||||
context["favicon"] = settings.favicon
|
||||
|
||||
context["hide_login"] = settings.hide_login
|
||||
|
||||
return context
|
||||
|
||||
def get_items(parentfield):
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@
|
|||
"options": "Color"
|
||||
},
|
||||
{
|
||||
"default": "300,600",
|
||||
"default": "wght@300;400;500;600;700;800",
|
||||
"fieldname": "font_properties",
|
||||
"fieldtype": "Data",
|
||||
"label": "Font Properties"
|
||||
|
|
@ -170,7 +170,7 @@
|
|||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-05-11 16:01:04.654990",
|
||||
"modified": "2020-05-16 18:36:22.203519",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Website Theme",
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/website",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"modified": "2020-04-30 20:23:06.438314",
|
||||
"modified": "2020-05-13 12:32:35.269025",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Website",
|
||||
|
|
@ -27,6 +27,9 @@
|
|||
},
|
||||
{
|
||||
"step": "Enable Website Tracking"
|
||||
},
|
||||
{
|
||||
"step": "Web Page Tour"
|
||||
}
|
||||
],
|
||||
"subtitle": "Blogs, website view tracking, and more.",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_mandatory": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2020-04-30 19:06:10.750976",
|
||||
"modified_by": "Administrator",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_mandatory": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2020-04-30 19:06:10.694419",
|
||||
"modified_by": "Administrator",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_mandatory": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2020-04-30 20:22:50.778590",
|
||||
"modified_by": "Administrator",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_mandatory": 1,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2020-04-30 19:06:10.578218",
|
||||
"modified_by": "Administrator",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"action": "Show Form Tour",
|
||||
"creation": "2020-05-13 12:32:15.966570",
|
||||
"docstatus": 0,
|
||||
"doctype": "Onboarding Step",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_mandatory": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2020-05-13 12:32:15.966570",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Web Page Tour",
|
||||
"owner": "Administrator",
|
||||
"reference_document": "Web Page",
|
||||
"title": "Learn about Web Pages"
|
||||
}
|
||||
0
frappe/website/report/website_analytics/__init__.py
Normal file
0
frappe/website/report/website_analytics/__init__.py
Normal file
32
frappe/website/report/website_analytics/website_analytics.js
Normal file
32
frappe/website/report/website_analytics/website_analytics.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright (c) 2016, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
frappe.query_reports["Website Analytics"] = {
|
||||
"filters": [
|
||||
{
|
||||
fieldname: "from_date",
|
||||
label: __("From Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.add_days(frappe.datetime.now_date(true), -100),
|
||||
},
|
||||
{
|
||||
fieldname:"to_date",
|
||||
label: __("To Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.now_date(true),
|
||||
},
|
||||
{
|
||||
fieldname: "range",
|
||||
label: __("Range"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ "value": "Daily", "label": __("Daily") },
|
||||
{ "value": "Weekly", "label": __("Weekly") },
|
||||
{ "value": "Monthly", "label": __("Monthly") },
|
||||
],
|
||||
default: "Daily",
|
||||
reqd: 1
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"add_total_row": 0,
|
||||
"creation": "2020-04-17 13:04:45.770148",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2020-04-17 16:10:30.168312",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Website Analytics",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Web Page View",
|
||||
"report_name": "Website Analytics",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "System Manager"
|
||||
},
|
||||
{
|
||||
"role": "Website Manager"
|
||||
}
|
||||
]
|
||||
}
|
||||
224
frappe/website/report/website_analytics/website_analytics.py
Normal file
224
frappe/website/report/website_analytics/website_analytics.py
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
# Copyright (c) 2013, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from datetime import datetime
|
||||
from frappe.utils import getdate
|
||||
from frappe.utils.dateutils import get_dates_from_timegrain
|
||||
|
||||
def execute(filters=None):
|
||||
return WebsiteAnalytics(filters).run()
|
||||
|
||||
class WebsiteAnalytics(object):
|
||||
def __init__(self, filters=None):
|
||||
self.filters = frappe._dict(filters or {})
|
||||
|
||||
if not self.filters.to_date:
|
||||
self.filters.to_date = datetime.now()
|
||||
|
||||
if not self.filters.from_date:
|
||||
self.filters.from_date = frappe.utils.add_days(self.filters.to_date, -7)
|
||||
|
||||
if not self.filters.range:
|
||||
self.filters.range = "Daily"
|
||||
|
||||
self.filters.to_date = frappe.utils.add_days(self.filters.to_date, 1)
|
||||
self.query_filters = {'creation': ['between', [self.filters.from_date, self.filters.to_date]]}
|
||||
|
||||
def run(self):
|
||||
columns = self.get_columns()
|
||||
data = self.get_data()
|
||||
chart = self.get_chart_data()
|
||||
summary = self.get_report_summary()
|
||||
|
||||
return columns, data[:250], None, chart, summary
|
||||
|
||||
def get_columns(self):
|
||||
return [
|
||||
{
|
||||
"fieldname": "path",
|
||||
"label": "Page",
|
||||
"fieldtype": "Data",
|
||||
"width": 300
|
||||
},
|
||||
{
|
||||
"fieldname": "count",
|
||||
"label": "Page Views",
|
||||
"fieldtype": "Int",
|
||||
"width": 150
|
||||
},
|
||||
{
|
||||
"fieldname": "unique_count",
|
||||
"label": "Unique Visitors",
|
||||
"fieldtype": "Int",
|
||||
"width": 150
|
||||
}
|
||||
]
|
||||
|
||||
def get_data(self):
|
||||
pg_query = """
|
||||
SELECT
|
||||
path,
|
||||
COUNT(*) as count,
|
||||
COUNT(CASE WHEN CAST(is_unique as Integer) = 1 THEN 1 END) as unique_count
|
||||
FROM `tabWeb Page View`
|
||||
WHERE coalesce("tabWeb Page View".creation, '0001-01-01') BETWEEN %s AND %s
|
||||
GROUP BY path
|
||||
ORDER BY count desc
|
||||
"""
|
||||
|
||||
mariadb_query = """
|
||||
SELECT
|
||||
path,
|
||||
COUNT(*) as count,
|
||||
COUNT(CASE WHEN is_unique = 1 THEN 1 END) as unique_count
|
||||
FROM `tabWeb Page View`
|
||||
WHERE creation BETWEEN %s AND %s
|
||||
GROUP BY path
|
||||
ORDER BY count desc
|
||||
"""
|
||||
|
||||
data = frappe.db.multisql({
|
||||
"mariadb": mariadb_query,
|
||||
"postgres": pg_query
|
||||
}, (self.filters.from_date, self.filters.to_date))
|
||||
return data
|
||||
|
||||
def _get_query_for_mariadb(self):
|
||||
filters_range = self.filters.range
|
||||
field = 'creation'
|
||||
date_format = '%Y-%m-%d'
|
||||
|
||||
if filters_range == "Weekly":
|
||||
field = 'ADDDATE(creation, INTERVAL 1-DAYOFWEEK(creation) DAY)'
|
||||
|
||||
elif filters_range == "Monthly":
|
||||
date_format = '%Y-%m-01'
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
DATE_FORMAT({0}, %s) as date,
|
||||
COUNT(*) as count,
|
||||
COUNT(CASE WHEN is_unique = 1 THEN 1 END) as unique_count
|
||||
FROM `tabWeb Page View`
|
||||
WHERE creation BETWEEN %s AND %s
|
||||
GROUP BY DATE_FORMAT({0}, %s)
|
||||
ORDER BY creation
|
||||
""".format(field)
|
||||
|
||||
values = (date_format, self.filters.from_date, self.filters.to_date, date_format)
|
||||
|
||||
return query, values
|
||||
|
||||
def _get_query_for_postgres(self):
|
||||
filters_range = self.filters.range
|
||||
field = 'creation'
|
||||
granularity = 'day'
|
||||
|
||||
if filters_range == "Weekly":
|
||||
granularity = 'week'
|
||||
|
||||
elif filters_range == "Monthly":
|
||||
granularity = 'day'
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
DATE_TRUNC(%s, {0}) as date,
|
||||
COUNT(*) as count,
|
||||
COUNT(CASE WHEN CAST(is_unique as Integer) = 1 THEN 1 END) as unique_count
|
||||
FROM "tabWeb Page View"
|
||||
WHERE coalesce("tabWeb Page View".{0}, '0001-01-01') BETWEEN %s AND %s
|
||||
GROUP BY date_trunc(%s, {0})
|
||||
ORDER BY date
|
||||
""".format(field)
|
||||
|
||||
values = (granularity, self.filters.from_date, self.filters.to_date, granularity)
|
||||
|
||||
return query, values
|
||||
|
||||
def get_chart_data(self):
|
||||
current_dialect = frappe.db.db_type or 'mariadb'
|
||||
|
||||
if current_dialect == 'mariadb':
|
||||
query, values = self._get_query_for_mariadb()
|
||||
else:
|
||||
query, values = self._get_query_for_postgres()
|
||||
|
||||
self.chart_data = frappe.db.sql(query, values=values, as_dict=1)
|
||||
|
||||
return self.prepare_chart_data(self.chart_data)
|
||||
|
||||
def prepare_chart_data(self, data):
|
||||
date_range = get_dates_from_timegrain(self.filters.from_date, self.filters.to_date, self.filters.range)
|
||||
if self.filters.range == "Monthly":
|
||||
date_range = [frappe.utils.add_days(dd, 1) for dd in date_range]
|
||||
|
||||
labels = []
|
||||
total_dataset = []
|
||||
unique_dataset = []
|
||||
|
||||
def get_data_for_date(date):
|
||||
for item in data:
|
||||
item_date = getdate(item.get("date"))
|
||||
if item_date == date:
|
||||
return item
|
||||
return {'count': 0, 'unique_count': 0}
|
||||
|
||||
|
||||
for date in date_range:
|
||||
labels.append(date.strftime("%b %d %Y"))
|
||||
match = get_data_for_date(date)
|
||||
total_dataset.append(match.get('count', 0))
|
||||
unique_dataset.append(match.get('unique_count', 0))
|
||||
|
||||
chart = {
|
||||
"data": {
|
||||
'labels': labels,
|
||||
'datasets': [
|
||||
{
|
||||
'name': "Total Views",
|
||||
'type': 'line',
|
||||
'values': total_dataset
|
||||
},
|
||||
{
|
||||
'name': "Unique Visits",
|
||||
'type': 'line',
|
||||
'values': unique_dataset
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "axis-mixed",
|
||||
'lineOptions': {
|
||||
'regionFill': 1,
|
||||
},
|
||||
'axisOptions': {
|
||||
'xIsSeries': 1
|
||||
},
|
||||
'colors': ['#7cd6fd', '#5e64ff']
|
||||
}
|
||||
|
||||
return chart
|
||||
|
||||
|
||||
def get_report_summary(self):
|
||||
total_count = 0
|
||||
unique_count = 0
|
||||
for data in self.chart_data:
|
||||
unique_count += data.get('unique_count')
|
||||
total_count += data.get('count')
|
||||
|
||||
report_summary = [
|
||||
{
|
||||
"value": total_count,
|
||||
"label": "Total Page Views",
|
||||
"datatype": "Int",
|
||||
},
|
||||
{
|
||||
"value": unique_count,
|
||||
"label": "Unique Page Views",
|
||||
"datatype": "Int",
|
||||
},
|
||||
|
||||
]
|
||||
return report_summary
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
</a>
|
||||
{%- endif -%}
|
||||
{%- if secondary_action -%}
|
||||
<a class="ml-3 btn btn-lg btn-primary-light" href="{{ secondary_action }}">
|
||||
<a class="ml-2 ml-lg-3 btn btn-lg btn-primary-light" href="{{ secondary_action }}">
|
||||
{{ secondary_action_label }}
|
||||
</a>
|
||||
{%- endif -%}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
</a>
|
||||
{%- endif -%}
|
||||
{%- if secondary_action -%}
|
||||
<a class="ml-3 btn btn-lg btn-primary-light" href="{{ secondary_action }}">
|
||||
<a class="ml-2 ml-lg-3 btn btn-lg btn-primary-light" href="{{ secondary_action }}">
|
||||
{{ secondary_action_label }}
|
||||
</a>
|
||||
{%- endif -%}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
{%- set card_size = card_size or 'Small' -%}
|
||||
<div class="{{ resolve_class({'mt-4': title}) }}">
|
||||
<div class="row mt-n4">
|
||||
{%- for index in ['1', '2', '3', '4', '5', '6', '7', '8'] -%}
|
||||
{%- for index in ['1', '2', '3', '4', '5', '6', '7', '8', '9'] -%}
|
||||
{%- set title = values['card_' + index + '_title'] -%}
|
||||
{%- set content = values['card_' + index + '_content'] -%}
|
||||
{%- set url = values['card_' + index + '_url'] -%}
|
||||
|
|
|
|||
|
|
@ -213,10 +213,34 @@
|
|||
"fieldtype": "Data",
|
||||
"label": "URL",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_9",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Card 9",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_9_title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_9_content",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Content",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "card_9_url",
|
||||
"fieldtype": "Data",
|
||||
"label": "URL",
|
||||
"reqd": 0
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"modified": "2020-04-29 22:40:03.362229",
|
||||
"modified": "2020-05-15 13:52:02.984001",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Section with Cards",
|
||||
"owner": "Administrator",
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
<div class="section-with-left-image row">
|
||||
<div class="col-12 col-sm-6">
|
||||
<img class="w-full rounded-xl" src="{{ image }}" alt="{{ title }}" />
|
||||
</div>
|
||||
<div class="py-4 col-12 col-sm-6">
|
||||
<h2>{{ title }}</h2>
|
||||
<p>{{ content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<div class="split-section-with-image row">
|
||||
{%- if not image_on_right -%}
|
||||
<div class="col-12 col-sm-6">
|
||||
{{ frappe.render_template('templates/includes/image_with_blur.html', {
|
||||
"src": image,
|
||||
"alt": title,
|
||||
"class": "split-section-image"
|
||||
}) }}
|
||||
</div>
|
||||
{%- endif -%}
|
||||
<div class="split-section-content col-12 col-sm-6">
|
||||
<h2>{{ title }}</h2>
|
||||
<p>{{ content }}</p>
|
||||
</div>
|
||||
{%- if image_on_right -%}
|
||||
<div class="col-12 col-sm-6">
|
||||
{{ frappe.render_template('templates/includes/image_with_blur.html', {
|
||||
"src": image,
|
||||
"alt": title,
|
||||
"class": "split-section-image"
|
||||
}) }}
|
||||
</div>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
|
|
@ -1,28 +1,37 @@
|
|||
{
|
||||
"creation": "2020-04-18 12:58:28.670489",
|
||||
"creation": "2020-05-17 06:37:27.461722",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Template",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Title"
|
||||
"label": "Title",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "content",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Content"
|
||||
"label": "Content",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Image"
|
||||
"label": "Image",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "image_on_right",
|
||||
"fieldtype": "Check",
|
||||
"label": "Image on Right",
|
||||
"reqd": 0
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"modified": "2020-04-18 12:58:28.670489",
|
||||
"modified": "2020-05-17 06:37:27.461722",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Section with Left Image",
|
||||
"name": "Split Section with Image",
|
||||
"owner": "Administrator",
|
||||
"standard": 1,
|
||||
"template": ""
|
||||
0
frappe/website/web_template/testimonial/__init__.py
Normal file
0
frappe/website/web_template/testimonial/__init__.py
Normal file
13
frappe/website/web_template/testimonial/testimonial.html
Normal file
13
frappe/website/web_template/testimonial/testimonial.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<div class="testimonial">
|
||||
<div class="testimonial-logo">
|
||||
<img src="{{ logo }}" alt="{{ name }}">
|
||||
</div>
|
||||
<div class="testimonial-content">
|
||||
<span>“</span>
|
||||
{{ content }}
|
||||
<span>”</span>
|
||||
</div>
|
||||
<div class="testimonial-by">
|
||||
{{ name }}
|
||||
</div>
|
||||
</div>
|
||||
31
frappe/website/web_template/testimonial/testimonial.json
Normal file
31
frappe/website/web_template/testimonial/testimonial.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"creation": "2020-05-16 19:52:11.590319",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Template",
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "logo",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Logo",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Name",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
"fieldname": "content",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Content",
|
||||
"reqd": 0
|
||||
}
|
||||
],
|
||||
"idx": 0,
|
||||
"modified": "2020-05-16 19:52:11.590319",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Testimonial",
|
||||
"owner": "Administrator",
|
||||
"standard": 1
|
||||
}
|
||||
|
|
@ -15,7 +15,9 @@ html, body {
|
|||
}
|
||||
{% include "templates/styles/card_style.css" %}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
window.is_404 = true;
|
||||
</script>
|
||||
<div class='page-card'>
|
||||
<div class='page-card-head'>
|
||||
<span class='indicator darkgrey'>{{_("Page Missing or Moved")}}</span>
|
||||
|
|
@ -29,4 +31,4 @@ html, body {
|
|||
background-color: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
@ -14,7 +14,7 @@ ga('send', 'pageview');
|
|||
{%- endif %}
|
||||
|
||||
{% if enable_view_tracking %}
|
||||
if (navigator.doNotTrack != 1) {
|
||||
if (navigator.doNotTrack != 1 && !window.is_404) {
|
||||
frappe.ready(() => {
|
||||
let browser = frappe.utils.get_browser();
|
||||
frappe.call("frappe.website.doctype.web_page_view.web_page_view.make_view_log", {
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ python-dateutil==2.8.1
|
|||
pytz==2019.3
|
||||
PyYAML==5.3.1
|
||||
rauth==0.7.3
|
||||
redis>=3.0
|
||||
redis==3.5.1
|
||||
requests-oauthlib==1.3.0
|
||||
requests==2.23.0
|
||||
RestrictedPython==5.0
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue