Merge branch 'develop' into commit-auto-updated-files

This commit is contained in:
Suraj Shetty 2020-05-22 13:14:52 +05:30 committed by GitHub
commit 62384fc8f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 1273 additions and 369 deletions

View file

@ -2,6 +2,6 @@
"baseUrl": "http://test_site_ui:8000",
"projectId": "92odwv",
"adminPassword": "admin",
"defaultCommandTimeout": 10000,
"defaultCommandTimeout": 20000,
"pageLoadTimeout": 15000
}

View file

@ -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 });

View file

@ -1,7 +1,6 @@
context('Relative Timeframe', () => {
beforeEach(() => {
cy.login();
cy.visit('/desk#workspace/Website');
});
before(() => {
cy.login();

View file

@ -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)

View file

@ -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'))

View file

@ -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()

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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)

View 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))

View file

@ -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",

View file

@ -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);

View file

@ -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);
}
});
},
});
}
};

View 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>

View 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">&ndash; {%= 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>

View file

@ -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,

View file

@ -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,

View file

@ -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;
};

View file

@ -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);
}
});
});
}
});
},
});

View file

@ -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'),

View file

@ -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 {

View file

@ -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;
}
}

View file

@ -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;
}

View 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;
}

View file

@ -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 {

View file

@ -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"

View file

@ -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">

View file

@ -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 {

View file

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

View file

@ -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 -->", "")

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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"
}]

View file

@ -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",

View file

@ -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.")
},
];

View file

@ -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",

View file

@ -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",

View file

@ -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) {

View file

@ -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",

View file

@ -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):

View file

@ -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",

View file

@ -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.",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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"
}

View 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
}
]
};

View file

@ -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"
}
]
}

View 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

View file

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

View file

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

View file

@ -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'] -%}

View file

@ -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",

View file

@ -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>

View file

@ -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>

View file

@ -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": ""

View 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>

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

View file

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

View file

@ -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", {

View file

@ -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